vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbacknächstes Kapitel


Woche 2

Tag 11



Vererbung

Zu den Grundzügen der menschlichen Intelligenz gehören das Auffinden, Erkennen und Erzeugen von Beziehungen zwischen Begriffen. Wir konstruieren Hierarchien, Matrizen, Netzwerke und andere Zwischenverbindungen, um die Wechselwirkungen zwischen den Dingen zu erläutern und zu verstehen. C++ fängt diese Abstraktionsvorgänge in Vererbungshierarchien ein. Heute lernen Sie,

Was ist Vererbung?

Was ist ein Hund? Wenn Sie sich Ihr Haustier ansehen, was stellen Sie fest? Ich sehe vier Beine auf der Suche nach Futter, meine Mutter sieht vor allem Hundehaare. Ein Biologe sieht eine Vernetzung von miteinander in Wechselwirkung stehenden Organen, ein Physiker sieht Atome und wirkende Kräfte und ein Systematiker sieht einen Vertreter der Familie Canis familiaris.

Im Moment interessiert uns hier letzterer Standpunkt. Ein Hund ist ein Säugetier, ein Säugetier ist eine Tierart und so weiter. Systematiker gliedern die Lebewesen in Reich, Abteilung, Stamm, Klasse, Ordnung, Familie, Gattung und Art.

Die Hierarchie des Systematikers richtet eine »ist-ein«-Beziehung ein. Ein Hund ist ein Raubtier. Überall begegnen uns »ist-ein«-Beziehungen: ein Toyota ist ein Auto, das wiederum ein Fortbewegungsmittel ist. Ein Pudding ist ein Nachtisch, der wieder ein Nahrungsmittel ist.

Auf diese Weise schaffen wir systematische Kategorien, die von oben nach unten eine zunehmende Spezialisierung aufweisen. Zum Beispiel ist ein Auto eine spezielle Art von Fortbewegungsmittel.

Vererbung und Ableitung

Der Begriff Hund erbt - das heißt, erhält automatisch - alle Merkmale eines Säugetiers. Von einem Säugetier ist bekannt, daß es sich bewegt und atmet - alle Säugetiere bewegen sich und atmen per Definition. Der Vorstellung vom Hund hinzu fügt man nun das Bellen, Schwanzwedeln, Fressen meines fertigen, überarbeiteten Manuskripts, Bellen, wenn ich versuche zu schlafen ... Entschuldigung, wo war ich stehengeblieben?

Hunde lassen sich weiter einteilen in Arbeitshunde, Jagdhunde und Terrier, und wir können Jagdhunde weiter untergliedern in Retriever, Spaniel usw. Und letztlich können diese noch weiter unterteilt werden. Zum Beispiel läßt sich ein Retriever weiter spezialisieren in Golden Retriever und Labrador.

Ein Golden Retriever ist eine Art von Retriever, der zu den Jagdhunden gehört, demzufolge ist er eine Art von Hund, also auch eine Art von Säugetier, also auch eine Art von Tier und auch eine Art der Lebewesen. Diese Hierarchie zeigt Abbildung 11.1 In der dabei eingesetzten Modelliersprache weisen die Pfeile von den spezialisierteren zu den allgemeineren Typen.

C++ versucht, diese Beziehungen durch die Definition von Klassen darzustellen, die sich von einer anderen Klasse ableiten. Die Ableitung ist eine Möglichkeit, eine »ist- ein«-Beziehung auszudrücken. Man leitet eine neue Klasse Dog (Hund) von der Klasse Mammal (Säugetier) ab. Dabei muß man nicht explizit feststellen, daß sich Hunde bewegen, da sie diese Eigenschaft von Mammal erben. Da eine Dog-Klasse von einer Mammal- Klasse erbt, bewegt sich Dog automatisch.

Abbildung 11.1:  Hierarchie von Tieren

Eine Klasse, die eine existierende Klasse um neue Funktionalität erweitert, bezeichnet man als von dieser Originalklasse abgeleitet. Die Originalklasse heißt Basisklasse der neuen Klasse.

Wenn man die Klasse Dog von der Klasse Mammal ableitet, dann ist Mammal die Basisklasse von Dog. Abgeleitete Klassen sind Obermengen ihrer Basisklassen. Genau wie ein Hund der Vorstellung von einem Säugetier bestimmte Merkmale hinzufügt, erweitert die Klasse Dog die Klasse Mammal um bestimmte Methoden oder Daten.

Normalerweise verfügt eine Basisklasse über mehrere abgeleitete Klassen. Da Hunde, Katzen und Pferde zu den Säugetieren gehören, leiten sich ihre Klassen von der Klasse Mammal ab.

Das Tierreich

Um die Behandlung von Ableitung und Vererbung einfacher zu gestalten, konzentriert sich dieses Kapitel auf die Beziehungen zwischen einer Reihe von Klassen, die Tiere darstellen. Nehmen wir an, daß die Simulation eines Bauernhofs als Software für Kinder zu entwickeln sei.

Mit der Zeit schafft man einen ganzen Satz von Tieren auf dem Bauernhof. Dazu gehören Pferde, Kühe, Hunde, Katzen und Schafe. Man erzeugt Methoden für diese Klassen, damit diese sich so verhalten, wie es ein Kind erwartet. Da uns aber weniger an einer realistischen Verhaltensweise der Tiere als vielmehr an dem Prinzip der Vererbung gelegen ist, begnügen wir uns mit einfacheren Methoden, die durch Ausgaben auf den Bildschirm anzeigen, daß sie aufgerufen wurden.

Im Englischen bezeichnet man Methoden, die skizzenhaft zur Erzeugung des Klassengerüsts aufgesetzt und erst später implementiert werden, als Stub-Routinen. Wenn Sie möchten, können Sie den in diesem Kapitel vorgestellten Minimalcode so erweitern, daß sich die Tiere realistischer verhalten.

Die Syntax der Ableitung

Will man von der Vererbung Gebrauch machen, gibt man die Basisklasse, von der abgeleitet wird, direkt bei der Deklaration der neuen Klasse an. Zu diesem Zwecke setzt man hinter den Klassennamen einen Doppelpunkt, dann den Typ der Ableitung (zum Beispiel public) und schließlich den Namen der Basisklasse, von der sich die neue Klasse ableitet. Dazu folgendes Beispiel:

class Dog : public Mammal

Auf den Typ der Ableitung gehen wir weiter hinten in diesem Kapitel ein. Momentan verwenden wir immer public. Die Basisklasse muß bereits vorher deklariert worden sein, da man sonst einen Compiler-Fehler erhält. Listing 11.1 deklariert eine Dog-Klasse für Hunde, die von einer Mammal-Klasse (für Säugetiere) abgeleitet ist.

Listing 11.1: Einfache Vererbung

1:     // Listing 11.1 Einfache Vererbung
2:
3: #include <iostream.h>
4: enum BREED { GOLDEN, CAIRN, DANDIE, SHETLAND, DOBERMAN, LAB };
5:
6: class Mammal
7: {
8: public:
9: // Konstruktoren
10: Mammal();
11: ~Mammal();
12:
13: // Zugriffsfunktionen
14: int GetAge()const;
15: void SetAge(int);
16: int GetWeight() const;
17: void SetWeight();
18:
19: // Andere Methoden
20: void Speak() const;
21: void Sleep() const;
22:
23:
24: protected:
25: int itsAge;
26: int itsWeight;
27: };
28:
29: class Dog : public Mammal
30: {
31: public:
32:
33: // Konstruktoren
34: Dog();
35: ~Dog();
36:
37: // Zugriffsfunktionen
38: BREED GetBreed() const;
39: void SetBreed(BREED);
40:
41: // Andere Methoden
42: WagTail();
43: BegForFood();
44:
45: protected:
46: BREED itsBreed;
47: };

Dieses Programm liefert keine Ausgaben, da es sich nur um einen Satz von Deklarationen ohne die zugehörigen Implementierungen handelt. Trotzdem enthält dieses Listing interessante Details.

Die Zeilen 6 bis 27 deklarieren die Klasse Mammal. In diesem Beispiel leitet sich Mammal von keiner anderen Klasse ab. Normalerweise wäre das aber der Fall - das heißt, Säugetiere gehören zur Klasse der Wirbeltiere. In einem C++-Programm kann man nur einen Bruchteil der Informationen darstellen, die man über ein gegebenes Objekt hat. Die Realität ist zu komplex, um sie vollständig wiederzugeben, so daß jede C++-Hierarchie eine etwas willkürliche Darstellung der verfügbaren Daten ist. Die Kunst eines guten Entwurfs besteht in einer einigermaßen wahrheitsgetreuen Widerspiegelung der Realität.

Die Hierarchie muß an irgendeiner Stelle beginnen. Im Beispielprogramm ist das die Klasse Mammal. Aufgrund dieser Entscheidung finden wir hier einige Elementvariablen, die vielleicht in einer höheren Basisklasse besser aufgehoben wären. Beispielsweise haben mit Sicherheit alle Tiere ein Alter und ein Gewicht, so daß man bei einer Ableitung der Klasse Mammal von Animal diese Attribute erben könnte. Im Beispiel erscheinen die Attribute jedoch in der Klasse Mammal.

Um das Programm möglichst einfach und übersichtlich zu halten, wurden in die Klasse Mammal lediglich sechs Methoden aufgenommen - vier Zugriffsmethoden sowie Speak() (Sprechen) und Sleep() (Schlafen).

Aus der Syntax in Zeile 29 geht hervor, daß die Klasse Dog von Mammal erbt. Jedes Dog- Objekt verfügt über drei Elementvariablen: itsAge, itsWeight und itsBreed. Beachten Sie, daß in der Klassendeklaration von Dog die Elementvariablen itsAge und itsWeight nicht aufgeführt sind. Dog-Objekte erben diese Variablen sowie alle Methoden von der Klasse Mammal. Ausgenommen hiervon sind der Kopieroperator, die Konstruktoren und der Destruktor.

Private und Protected

Sicherlich haben Sie das neue Zugriffsschlüsselwort protected in den Zeilen 24 und 45 von Listing 11.1 bemerkt. Bisher haben wir Klassendaten als private deklariert. Für abgeleitete Klassen sind private Elemente allerdings nicht verfügbar. Man könnte zwar itsAge und itsWeight als public deklarieren, das ist aber nicht wünschenswert. Andere Klassen sollen nämlich nicht auf diese Datenelemente direkt zugreifen können.

Man braucht also eine Kennzeichnung, die folgendes aussagt: »Mache diese Elemente sichtbar zu dieser Klasse und zu Klassen, die sich von dieser Klasse ableiten.« Genau das bewirkt das Schlüsselwort protected (geschützt). Geschützte Datenelemente und Funktionen sind für abgeleitete Klassen vollständig sichtbar, sonst aber privat.

Insgesamt gibt es drei Spezifizierer für den Zugriff: public, protected und private. Kommt in einer Funktion ein Objekt einer bestimmten Klasse vor, kann die Funktion auf alle öffentlichen (public) Datenelemente und Elementfunktionen dieser Klasse zugreifen. Die Elementfunktionen können wiederum auf alle privaten (private) Datenelemente und Funktionen ihrer eigenen Klasse und alle geschützten (protected) Datenelemente und Funktionen einer beliebigen Klasse, von der sie sich ableiten, zugreifen.

Demzufolge kann die Funktion Dog::WagTail() auf die privaten Daten itsBreed und auf die geschützten Daten in der Klasse Mammal zugreifen.

Selbst wenn sich in der Hierarchie andere Klassen zwischen Mammal und Dog befinden (beispielsweise DomesticAnimals, Haustiere), kann die Klasse Dog weiterhin auf die geschützten Elemente von Mammal zugreifen. Das setzt allerdings voraus, daß die dazwischenliegenden Klassen mit öffentlicher Vererbung arbeiten. Auf die private Vererbung kommen wir in Kapitel 15, »Vererbung - weiterführende Themen«, zu sprechen.

Listing 11.2 demonstriert, wie man Objekte vom Typ Dog erzeugt und auf die Daten und Funktionen dieses Typs zugreift.

Listing 11.2: Ein abgeleitetes Objekt

1:     // Listing 11.2 Ein abgeleitetes Objekt verwenden
2:
3: #include <iostream.h>
4: enum BREED { GOLDEN, CAIRN, DANDIE, SHETLAND, DOBERMAN, LAB };
5:
6: class Mammal
7: {
8: public:
9: // Konstruktoren
10: Mammal():itsAge(2), itsWeight(5){}
11: ~Mammal(){}
12:
13: // Zugriffsfunktionen
14: int GetAge()const { return itsAge; }
15: void SetAge(int age) { itsAge = age; }
16: int GetWeight() const { return itsWeight; }
17: void SetWeight(int weight) { itsWeight = weight; }
18:
19: // Andere Methoden
20: void Speak()const { cout << "Saeugetier, gib Laut!\n"; }
21: void Sleep()const { cout << "Psst. Ich schlafe.\n"; }
22:
23:
24: protected:
25: int itsAge;
26: int itsWeight;
27: };
28:
29: class Dog : public Mammal
30: {
31: public:
32:
33: // Konstruktoren
34: Dog():itsBreed(GOLDEN){}
35: ~Dog(){}
36:
37: // Zugriffsfunktionen
38: BREED GetBreed() const { return itsBreed; }
39: void SetBreed(BREED breed) { itsBreed = breed; }
40:
41: // Andere Methoden
42: void WagTail() { cout << "Schwanzwedeln...\n"; }
43: void BegForFood() { cout << "Um Futter betteln...\n"; }
44:
45: private:
46: BREED itsBreed;
47: };
48:
49: int main()
50: {
51: Dog fido;
52: fido.Speak();
53: fido.WagTail();
54: cout << "Fido ist " << fido.GetAge() << " Jahre alt.\n";
55: return 0;
56: }

Saeugetier, gib Laut!
Schwanzwedeln...
Fido ist 2 Jahre alt.

Die Zeilen 6 bis 27 deklarieren die Klasse Mammal (um Platz zu sparen sind alle Funktionen inline definiert). In den Zeilen 29 bis 47 wird die Klasse Dog als abgeleitete Klasse von Mammal deklariert. Aufgrund dieser Deklarationen verfügen alle Dog-Objekte über ein Alter (age) ein Gewicht (weight) und eine Rasse (breed).

Zeile 51 deklariert das Dog-Objekt Fido. Das Objekt Fido erbt sowohl alle Attribute eines Mammal-Objekts als auch alle Attribute eines Dog-Objekts. Daher weiß Fido, wie man mit dem Schwanz wedelt (WagTail()), aber auch, wie man spricht (Speak()) und schläft (Sleep()).

Konstruktoren und Destruktoren

Dog-Objekte sind Mammal-Objekte. Das ist das Wesen einer »ist-ein«-Beziehung. Beim Erzeugen von Fido wird zuerst dessen Konstruktor aufgerufen, der ein Mammal-Objekt erzeugt. Dann folgt der Aufruf des Dog-Konstruktors, der die Konstruktion des Dog-Objekts vervollständigt. Da Fido keine Parameter übernimmt, wird in diesem Fall der Standardkonstruktor aufgerufen. Fido existiert erst, nachdem dieses Objekt vollständig erzeugt wurde, das heißt, sowohl der Mammal-Teil als auch der Dog-Teil aufgebaut sind. Daher ist der Aufruf beider Konstruktoren erforderlich.

Beim Zerstören des Fido-Objekts wird zuerst der Dog-Destruktor und dann der Destruktor für den Mammal-Teil von Fido aufgerufen. Jeder Destruktor hat die Möglichkeit, seinen eigenen Teil von Fido aufzuräumen. Denken Sie daran, hinter Ihrem Hund sauberzumachen! Listing 11.3 demonstriert die Abläufe.

Listing 11.3: Aufgerufene Konstruktoren und Destruktoren

1:     // Listing 11.3 Aufgerufene Konstruktoren und Destruktoren.
2:
3: #include <iostream.h>
4: enum BREED { GOLDEN, CAIRN, DANDIE, SHETLAND, DOBERMAN, LAB };
5:
6: class Mammal
7: {
8: public:
9: // Konstruktoren
10: Mammal();
11: ~Mammal();
12:
13: // Zugriffsfunktionen
14: int GetAge() const { return itsAge; }
15: void SetAge(int age) { itsAge = age; }
16: int GetWeight() const { return itsWeight; }
17: void SetWeight(int weight) { itsWeight = weight; }
18:
19: // Andere Methoden
20: void Speak() const { cout << "Saeugetier, gib Laut!\n"; }
21: void Sleep() const { cout << "Psst. Ich schlafe.\n"; }
22:
23:
24: protected:
25: int itsAge;
26: int itsWeight;
27: };
28:
29: class Dog : public Mammal
30: {
31: public:
32:
33: // Konstruktoren
34: Dog();
35: ~Dog();
36:
37: // Zugriffsfunktionen
38: BREED GetBreed() const { return itsBreed; }
39: void SetBreed(BREED breed) { itsBreed = breed; }
40:
41: // Andere Methoden
42: void WagTail() const { cout << "Schwanzwedeln...\n"; }
43: void BegForFood() const { cout << "Um Futter betteln...\n"; }
44:
45: private:
46: BREED itsBreed;
47: };
48:
49: Mammal::Mammal():
50: itsAge(1),
51: itsWeight(5)
52: {
53: cout << "Mammal-Konstruktor...\n";
54: }
55:
56: Mammal::~Mammal()
57: {
58: cout << "Mammal-Destruktor...\n";
59: }
60:
61: Dog::Dog():
62: itsBreed(GOLDEN)
63: {
64: cout << "Dog-Konstruktor...\n";
65: }
66:
67: Dog::~Dog()
68: {
69: cout << "Dog-Destruktor...\n";
70: }
71: int main()
72: {
73: Dog fido;
74: fido.Speak();
75: fido.WagTail();
76: cout << "Fido ist " << fido.GetAge() << " Jahre alt.\n";
77: return 0;
78: }

Mammal-Konstruktor...
Dog-Konstruktor...
Saeugetier, gib Laut!
Schwanzwedeln...
Fido ist 1 Jahr alt.
Dog-Destruktor...
Mammal-Destruktor...

Listing 11.3 entspricht weitgehend Listing 11.2, enthält aber eigene Implementierungen für die Konstruktoren und Destruktoren, die uns über die Aufrufe der Methoden informieren. Als erstes erfolgt der Aufruf des Konstruktors von Mammal. Daran schließt sich der Aufruf des Konstruktors von Dog an. Damit existiert das Dog-Objekt vollständig, und man kann dessen Methoden aufrufen. Verliert das Fido-Objekt den Gültigkeitsbereich, wird der Destruktor von Dog und daran anschließend der Destruktor von Mammal aufgerufen.

Argumente an Basisklassenkonstruktoren übergeben

Vielleicht möchten Sie den Konstruktor von Mammal überladen, um ein bestimmtes Alter übergeben zu können, vielleicht möchten Sie den Konstruktor von Dog überladen, um eine bestimmte Rasse vorzugeben. Dies wirft etliche Fragen aus. Wie lassen sich für ein Dog-Objekt die Parameter für Alter und Gewicht an die richtigen Konstruktoren von Mammal übergeben? Was macht man, wenn Dog das Gewicht initialisieren soll, aber nicht Mammal?

Die Initialisierung der Basisklasse kann während der Klasseninitialisierung vorgenommen werden. Man hängt dazu den Namen der Basisklasse mit den von der Basisklasse erwarteten Parameter an den Konstruktor der abgeleiteten Klasse an. Listing 11.4 zeigt dazu ein Beispiel.

Listing 11.4: Konstruktoren in abgeleiteten Klassen überladen

1:     // Listing 11.4 Konstruktoren in abgeleiteten Klassen ueberladen
2:
3: #include <iostream.h>
4: enum BREED { GOLDEN, CAIRN, DANDIE, SHETLAND, DOBERMAN, LAB };
5:
6: class Mammal
7: {
8: public:
9: // Konstruktoren
10: Mammal();
11: Mammal(int age);
12: ~Mammal();
13:
14: // Zugriffsfunktionen
15: int GetAge() const { return itsAge; }
16: void SetAge(int age) { itsAge = age; }
17: int GetWeight() const { return itsWeight; }
18: void SetWeight(int weight) { itsWeight = weight; }
19:
20: // Andere Methoden
21: void Speak() const { cout << "Saeugetier, gib Laut!\n"; }
22: void Sleep() const { cout << "Psst. Ich schlafe.\n"; }
23:
24:
25: protected:
26: int itsAge;
27: int itsWeight;
28: };
29:
30: class Dog : public Mammal
31: {
32: public:
33:
34: // Konstruktoren
35: Dog();
36: Dog(int age);
37: Dog(int age, int weight);
38: Dog(int age, BREED breed);
39: Dog(int age, int weight, BREED breed);
40: ~Dog();
41:
42: // Zugriffsfunktionen
43: BREED GetBreed() const { return itsBreed; }
44: void SetBreed(BREED breed) { itsBreed = breed; }
45:
46: // Andere Methoden
47: void WagTail() const { cout << "Schwanzwedeln...\n"; }
48: void BegForFood() const { cout << "Um Futter betteln...\n"; }
49:
50: private:
51: BREED itsBreed;
52: };
53:
54: Mammal::Mammal():
55: itsAge(1),
56: itsWeight(5)
57: {
58: cout << "Mammal-Konstruktor...\n";
59: }
60:
61: Mammal::Mammal(int age):
62: itsAge(age),
63: itsWeight(5)
64: {
65: cout << "Mammal(int)-Konstruktor...\n";
66: }
67:
68: Mammal::~Mammal()
69: {
70: cout << "Mammal-Destruktor...\n";
71: }
72:
73: Dog::Dog():
74: Mammal(),
75: itsBreed(GOLDEN)
76: {
77: cout << "Dog-Konstruktor...\n";
78: }
79:
80: Dog::Dog(int age):
81: Mammal(age),
82: itsBreed(GOLDEN)
83: {
84: cout << "Dog(int)-Konstruktor...\n";
85: }
86:
87: Dog::Dog(int age, int weight):
88: Mammal(age),
89: itsBreed(GOLDEN)
90: {
91: itsWeight = weight;
92: cout << "Dog(int, int)-Konstruktor...\n";
93: }
94:
95: Dog::Dog(int age, int weight, BREED breed):
96: Mammal(age),
97: itsBreed(breed)
98: {
99: itsWeight = weight;
100: cout << "Dog(int, int, BREED)-Konstruktor...\n";
101: }
102:
103: Dog::Dog(int age, BREED breed):
104: Mammal(age),
105: itsBreed(breed)
106: {
107: cout << "Dog(int, BREED)-Konstruktor...\n";
108: }
109:
110: Dog::~Dog()
111: {
112: cout << "Dog-Destruktor...\n";
113: }
114: int main()
115: {
116: Dog fido;
117: Dog rover(5);
118: Dog buster(6,8);
119: Dog yorkie (3,GOLDEN);
120: Dog dobbie (4,20,DOBERMAN);
121: fido.Speak();
122: rover.WagTail();
123: cout << "Yorkie ist " << yorkie.GetAge() << " Jahre alt.\n";
124: cout << "Dobbie wiegt " << dobbie.GetWeight() << " Pfund.\n";
125: cout << dobbie.GetWeight() << " Pfund\n";
126: return 0;
127: }

Die Numerierung der Ausgabezeilen gehört nicht zur tatsächlich erzeugten Ausgabe, sondern dient nur der Bezugnahme im Analyseteil.

1:  Mammal-Konstruktor...
2: Dog-Konstruktor...
3: Mammal(int)-Konstruktor...
4: Dog(int)-Konstruktor...
5: Mammal(int)-Konstruktor...
6: Dog(int, int)-Konstruktor...
7: Mammal(int)-Konstruktor...
8: Dog(int, BREED)-Konstruktor...
9: Mammal(int)-Konstruktor...
10: Dog(int, int, BREED)-Konstruktor...
11: Saeugetier, gib Laut!
12: Schwanzwedeln...
13: Yorkie ist 3 Jahre alt.
14: Dobbie wiegt 20 Pfund.
15: Dog-Destruktor...
16: Mammal-Destruktor...
17: Dog-Destruktor...
18: Mammal-Destruktor...
19: Dog-Destruktor...
20: Mammal-Destruktor...
21: Dog-Destruktor...
22: Mammal-Destruktor...
23: Dog-Destruktor...
24: Mammal-Destruktor...

Die Zeile 11 überlädt den Konstruktor von Mammal, um das Alter des Säugetiers als ganze Zahl zu übernehmen. Die Implementierung in den Zeilen 61 bis 66 initialisiert itsAge mit dem an den Konstruktor übergebenen Wert sowie itsWeight mit dem Wert 5.

In den Zeilen 35 bis 39 sind die fünf überladenen Konstruktoren von Dog deklariert. Der erste ist der Standardkonstruktor. Der zweite übernimmt das Alter, wobei es sich um den gleichen Parameter handelt, den auch der Konstruktor von Mammal übernimmt. Der dritte Konstruktor übernimmt sowohl Alter als auch Gewicht, der vierte Alter und Rasse, und im fünften Konstruktor finden wir Parameter für Alter, Gewicht und Rasse.

Beachten Sie in Zeile 74, daß der Standardkonstruktor von Dog den Standardkonstruktor von Mammal aufruft. Obwohl das nicht zwingend erforderlich ist, dient es der Dokumentation, daß man den Basiskonstruktor aufrufen möchte, der keine Parameter übernimmt. Der Basiskonstruktor wird in jedem Fall aufgerufen, das hier gezeigte Verfahren verdeutlicht aber Ihre Absichten explizit.

In den Zeilen 80 bis 85 steht die Implementierung des Dog-Konstruktors, der eine ganze Zahl übernimmt. In dessen Initialisierungsphase (Zeilen 81 und 82) initialisiert Dog zuerst seine Basisklasse, inklusive Übergabe eines Parameters, und dann seine Rasse (itsBreed).

Der nächste Dog-Konstruktor ist in den Zeilen 87 bis 93 zu finden. Dieser übernimmt zwei Parameter. Wieder erfolgt die Initialisierung der Basisklasse durch Aufruf des passenden Konstruktors. Dieses Mal findet aber auch die Zuweisung von weight an die Variable itsWeight der Basisklasse von Dog statt. Beachten Sie, daß die Zuweisung an eine Variable der Basisklasse in der Initialisierungsphase nicht möglich ist, da Mammal keinen Konstruktor hat, der einen entsprechenden Parameter übernimmt. Man muß die Zuweisung daher innerhalb des Rumpfes des Konstruktors von Dog erledigen.

Sehen Sie sich die verbleibenden Konstruktoren an, um sich mit deren Arbeitsweise vertraut zu machen. Achten Sie darauf, was man initialisieren kann und was noch im Rumpf des Konstruktors zuzuweisen ist.

Die Numerierung der Ausgabezeilen dient lediglich der Bezugnahme in dieser Analyse. Die beiden ersten Zeilen der Ausgabe repräsentieren die Instantiierung von Fido mit Hilfe des Standardkonstruktors.

In den Ausgabezeilen 3 und 4 dokumentiert sich die Erzeugung von rover, in den Zeilen 5 und 6 von buster. Beachten Sie den Aufruf des Mammal-Konstruktors mit einem Integer-Wert als Parameter, während der Dog-Konstruktor zwei Integer-Werte übernimmt.

Nach dem Erstellen aller Objekte werden diese verwendet und verlieren anschließend ihren Gültigkeitsbereich. Beim Zerstören der einzelnen Objekte findet zuerst der Aufruf des Dog-Destruktors und danach des Mammal-Destruktors statt. Insgesamt sind es jeweils fünf Aufrufe.

Funktionen überschreiben

Ein Dog-Objekt hat Zugriff auf alle Elementfunktionen der Klasse Mammal sowie alle Elementfunktionen (wie WagTail()), die die Deklaration der Dog-Klasse gegebenenfalls hinzufügt. Die Dog-Klasse kann auch Funktionen der Basisklasse überschreiben. Eine Funktion zu überschreiben, bedeutet, die Implementierung einer Funktion der Basisklasse in einer abgeleiteten Klasse zu ändern. Wenn man ein Objekt der abgeleiteten Klasse erstellt, wird die korrekte Funktion aufgerufen.

Erzeugt eine abgeleitete Klasse eine Funktion mit demselben Rückgabetyp und derselben Signatur wie eine Elementfunktion in der Basisklasse, aber mit einer neuen Implementierung, spricht man vom Überschreiben dieser Methode.

Die überschriebene Funktion muß bezüglich Rückgabetyp und Signatur mit der Funktion in der Basisklasse übereinstimmen. Als Signatur bezeichnet man alles, was - abgesehen vom Rückgabetyp - zu einem Funktionsprototyp gehört: Name, Parameterliste und - falls verwendet - das Schlüsselwort const.

Listing 11.5 zeigt, was passiert, wenn die Dog-Klasse die Methode Speak() in Mammal überschreibt. Um Platz zu sparen, wurde auf die Zugriffsfunktionen der Klassen verzichtet.

Listing 11.5: Eine Methode der Basisklasse in einer abgeleiteten Klasse überschreiben

1:     // Listing 11.5 Eine Methode der Basisklasse in einer abgeleiteten 
2: // Klasse ueberschreiben
3: #include <iostream.h>
4: enum BREED { GOLDEN, CAIRN, DANDIE, SHETLAND, DOBERMAN, LAB };
5:
6: class Mammal
7: {
8: public:
9: // Konstruktoren
10: Mammal() { cout << "Mammal-Konstruktor...\n"; }
11: ~Mammal() { cout << "Mammal-Destruktor...\n"; }
12:
13: // Andere Methoden
14: void Speak()const { cout << "Saeugetier, gib Laut!\n"; }
15: void Sleep()const { cout << "Psst. Ich schlafe.\n"; }
16:
17:
18: protected:
19: int itsAge;
20: int itsWeight;
21: };
22:
23: class Dog : public Mammal
24: {
25: public:
26:
27: // Konstruktoren
28: Dog(){ cout << "Dog-Konstruktor...\n"; }
29: ~Dog(){ cout << "Dog-Destruktor...\n"; }
30:
31: // Andere Methoden
32: void WagTail() const { cout << "Schwanzwedeln...\n"; }
33: void BegForFood() const { cout << "Um Futter betteln...\n"; }
34: void Speak()const { cout << "Wuff!\n"; }
35:
36: private:
37: BREED itsBreed;
38: };
39:
40: int main()
41: {
42: Mammal bigAnimal;
43: Dog fido;
44: bigAnimal.Speak();
45: fido.Speak();
46: return 0;
47: }

Mammal-Konstruktor...
Mammal-Konstruktor...
Dog-Konstruktor...
Saeugetier, gib Laut!
Wuff!
Dog-Destruktor...
Mammal-Destruktor...
Mammal-Destruktor...

In Zeile 34 überschreibt die Dog-Klasse die Methode Speak(). Dadurch gibt das Dog- Objekt »Wuff!« aus, wenn man die Methode Speak() aufruft. Zeile 42 erzeugt das Mammal -Objekt bigAnimal, das die erste Ausgabezeile produziert, wenn der Mammal-Konstruktor aufgerufen wird. Zeile 43 erzeugt das Dog-Objekt Fido, das die beiden nächsten Ausgabezeilen durch den Aufruf der Mammal- und Dog-Konstruktoren auf den Bildschirm bringt.

Das Mammal-Objekt ruft in Zeile 44 seine Methode Speak() auf. In Zeile 45 ruft dann das Dog-Objekt seine Speak()-Methode auf. Die Ausgabe zeigt, daß die korrekten Methoden aufgerufen wurden. Schließlich verlieren die beiden Objekte ihren Gültigkeitsbereich, und es folgen die Aufrufe der Destruktoren.

Überladen und Überschreiben

Diese beiden Verfahren führen ähnliche Aufgaben aus. Wenn man eine Methode überlädt, erzeugt man mehrere Methoden mit dem gleichen Namen, aber unterschiedlichen Signaturen. Überschreibt man eine Methode, erzeugt man eine Methode in einer abgeleiteten Klasse mit dem gleichen Namen wie die Methode in der Basisklasse und mit der gleichen Signatur.

Die Methode der Basisklasse verbergen

Im vorhergehenden Listing verbirgt die Methode Speak() der Klasse Dog die Methode der Basisklasse. Dies ist zwar beabsichtigt, kann aber zu unerwarteten Ergebnissen führen. Wenn Mammal über eine überladene Methode Move() verfügt, und Dog diese Methode überschreibt, verbirgt die Dog-Methode alle Mammal-Methoden mit diesem Namen.

Wenn Mammal die Methode Move() mit drei Methoden überlädt - eine ohne Parameter, eine mit einem Integer-Parameter und eine mit einem Integer-Parameter und einem Parameter für die Richtung - und Dog einfach die Methode Move() ohne Parameter überschreibt, kann man über ein Dog-Objekt nicht mehr ohne weiteres auf die beiden anderen Methoden zugreifen. Listing 11.6 verdeutlicht dieses Problem.

Listing 11.6: Methoden verbergen

1:     // Listing 11.6 Methoden verbergen
2:
3: #include <iostream.h>
4:
5: class Mammal
6: {
7: public:
8: void Move() const { cout << "Saeugetier geht einen Schritt.\n"; }
9: void Move(int distance) const
10: {
11: cout << "Saeugetier geht ";
12: cout << distance <<" Schritte.\n";
13: }
14: protected:
15: int itsAge;
16: int itsWeight;
17: };
18:
19: class Dog : public Mammal
20: {
21: public:
22: // Hier kann eine Warnung erfolgen, dass Sie eine Funktion verbergen!
23: void Move() const { cout << "Hund geht 5 Schritte.\n"; }
24: };
25:
26: int main()
27: {
28: Mammal bigAnimal;
29: Dog fido;
30: bigAnimal.Move();
31: bigAnimal.Move(2);
32: fido.Move();
33: // fido.Move(10);
34: return 0;
35: }

Saeugetier geht einen Schritt.
Saeugetier geht 2 Schritte.
Hund geht 5 Schritte.

Diese Klassen verzichten auf alle überflüssigen Methoden und Daten. In den Zeilen 8 und 9 deklariert die Mammal-Klasse die überladenen Move()-Methoden. In Zeile 23 überschreibt Dog die parameterlose Version von Move(). Die Methoden werden in den Zeilen 30 bis 32 aufgerufen, und die Ausgabe spiegelt ihre Ausführung wider.

Zeile 33 ist auskommentiert, da sie einen Compiler-Fehler hervorruft. Die Dog-Klasse könnte normalerweise die Methode Move(int) aufrufen, wenn sie nicht die parameterlose Version von Move() überschrieben hätte. Nun ist aber diese Methode überschrieben. Möchte man beide Methoden verwenden, muß man auch beide Methoden überschreiben. Andernfalls bleibt die Methode, die nicht überschrieben wurde, verborgen. Das erinnert an die Regel, daß der Compiler keinen Standardkonstruktor bereitstellt, wenn man irgendeinen Konstruktor erzeugt.

Die Regel besagt: Sobald Sie eine überladene Methode überschreiben, sind alle anderen überladenen Versionen dieser Methode verdeckt. Wenn Sie dies nicht wünschen, müssen Sie alle Versionen überschreiben.

Es ist ein häufiger Fehler, daß man das Schlüsselwort const vergißt und so die Methode einer Basisklasse verdeckt, anstatt sie zu überschreiben. Das Schlüsselwort const gehört zur Signatur. Läßt man es weg, ändert man die Signatur und verbirgt damit die Methode, statt sie - wie eigentlich beabsichtigt - zu überschreiben.

Überschreiben und Verbergen

Im nächsten Abschnitt werde ich auf virtuelle Methoden eingehen. Mit dem Überschreiben einer virtuellen Methode wird die Polymorphie unterstützt - verbirgt man eine virtuelle Methode, behindert man die Polymorphie.

Die Basismethode aufrufen

Eine überschriebene Basismethode kann man weiterhin aufrufen, wenn man den vollständigen Namen der Methode angibt. Dazu schreibt man den Basisnamen, zwei Doppelpunkte und dann den Methodennamen wie im folgenden Beispiel:

Mammal::Move()

Somit kann man Zeile 32 in Listing 11.6 folgendermaßen neu schreiben:

32:     fido.Mammal::Move(10);

Diese Anweisung ruft explizit die Mammal-Methode auf. Listing 11.7 belegt dies an einem vollständigen Beispiel.

Listing 11.7: Die Basismethode einer überschriebenen Methode aufrufen

1:     // Listing 11.7 Basismethode einer ueberschriebenen Methode aufrufen
2:
3: #include <iostream.h>
4:
5: class Mammal
6: {
7: public:
8: void Move() const { cout << "Saeugetier geht einen Schritt. \n"; }
9: void Move(int distance) const
10: {
11: cout << "Saeugetier geht " << distance;
12: cout << " Schritte.\n";
13: }
14:
15: protected:
16: int itsAge;
17: int itsWeight;
18: };
19:
20: class Dog : public Mammal
21: {
22: public:
23: void Move()const;
24:
25: };
26:
27: void Dog::Move() const
28: {
29: cout << "In Move von Dog...\n";
30: Mammal::Move(3);
31: }
32:
33: int main()
34: {
35: Mammal bigAnimal;
36: Dog fido;
37: bigAnimal.Move(2);
38: fido.Mammal::Move(6);
39: return 0;
40: }

Saeugetier geht 2 Schritte.
Saeugetier geht 6 Schritte.

Zeile 35 erzeugt das Mammal-Objekt bigAnimal. Zeile 36 erzeugt das Dog-Objekt Fido. Die Anweisung in Zeile 37 ruft die Methode Move() des Mammal-Objekts mit Übergabe eines int-Wertes auf.

Der Programmierer möchte Move(int) auf dem Dog-Objekt aufrufen, hatte aber ein Problem. Dog überschreibt die Methode Move(), überlädt sie aber nicht und stellt keine Version bereit, die einen int übernimmt. Das läßt sich durch den expliziten Aufruf der Methode Move(int) der Basisklasse in Zeile 38 lösen.

Was Sie tun sollten

... und was nicht

Erweitern Sie die Funktionalität ausgetesteter Klassen durch Ableiten.

Ändern Sie das Verhalten von bestimmten Funktionen in der abgeleiteten Klasse durch Überschreiben der Methoden der Basisklasse.

Achten Sie darauf, Funktionen der Basisklasse nicht ungewollt durch Änderung der Signatur der Funktion beim Überschreiben zu verdecken.

Virtuelle Methoden

In diesem Kapitel wurde die Tatsache betont, daß ein Dog-Objekt ein Mammal-Objekt ist. Bisher hat das nur bedeutet, daß das Dog-Objekt die Attribute (Daten) und Fähigkeiten (Methoden) seiner Basisklasse geerbt hat. In C++ geht die »ist-ein«-Beziehung allerdings noch weiter.

C++ erweitert die Polymorphie dahingehend, daß sich abgeleitete Klassenobjekte an Zeiger auf die Basisklassen zuweisen lassen. Deshalb kann man schreiben:

Mammal* pMammal = new Dog;

Diese Anweisung erzeugt ein neues Dog-Objekt auf dem Heap. Der von new zurückgegebene Zeiger auf dieses Objekt wird einem Zeiger auf Mammal zugewiesen. Das ist durchaus sinnvoll, da ein Hund ein Säugetier ist.

Wir haben es hier mit dem Wesen der Polymorphie zu tun. Man kann zum Beispiel viele unterschiedliche Typen von Fenstern erzeugen - etwa Dialogfelder, Fenster mit Bildlaufleisten und Listenfelder - und jedem von ihnen eine virtuelle Methode zeichnen() (Zeichnen) spendieren. Die Methode zeichnen() läßt sich ohne Beachtung des eigentlichen Laufzeittyps des referenzierten Objekts aufrufen, wenn man einen Zeiger auf ein Fenster erzeugt und diesem Zeiger Dialogfelder oder andere abgeleitete Typen zuweist. Es wird immer die richtige zeichnen()-Funktion aufgerufen.

Über diesen Zeiger kann man jede Methode in Mammal aufrufen. Wünschenswert wäre aber vor allem, daß für ein Dog-Objekt auch die in Dog überschriebenen Methoden aufgerufen werden. Virtuelle Elementfunktionen erlauben genau das. Listing 11.8 illustriert deren Arbeitsweise und stellt Aufrufe von nicht virtuellen Methoden dagegen.

Listing 11.8: Virtuelle Methoden

1:     // Listing 11.8 Einsatz virtueller Methoden
2:
3: #include <iostream.h>
4:
5: class Mammal
6: {
7: public:
8: Mammal():itsAge(1) { cout << "Mammal-Konstruktor...\n"; }
9: virtual ~Mammal() { cout << "Mammal-Destruktor...\n"; }
10: void Move() const { cout << "Saeugetier geht einen Schritt.\n"; }
11: virtual void Speak() const { cout << "Saeugetier spricht!\n"; }
12: protected:
13: int itsAge;
14:
15: };
16:
17: class Dog : public Mammal
18: {
19: public:
20: Dog() { cout << "Dog-Konstruktor...\n"; }
21: virtual ~Dog() { cout << "Dog-Destruktor...\n"; }
22: void WagTail() { cout << "Schwanzwedeln...\n"; }
23: void Speak()const { cout << "Wuff!\n"; }
24: void Move()const { cout << "Hund geht 5 Schritte...\n"; }
25: };
26:
27: int main()
28: {
29:
30: Mammal *pDog = new Dog;
31: pDog->Move();
32: pDog->Speak();
33:
34: return 0;
35: }

Mammal-Konstruktor...
Dog-Konstruktor...
Saeugetier geht einen Schritt.
Wuff!

Zeile 11 stellt für Mammal eine virtuelle Methode - Speak() - bereit. Der Designer dieser Klasse zeigt durch die virtual-Deklaration an, daß er damit rechnet, daß diese Klasse als Basisklasse für eine andere Klasse verwendet wird und die betreffende Funktion wahrscheinlich in der abgeleiteten Klasse überschrieben wird.

Zeile 30 erzeugt einen Zeiger auf Mammal, weist ihm aber die Adresse eines neuen Dog- Objekts zu. Da ein Dog-Objekt von Mammal abgeleitet ist, stellt dies eine zulässige Zuweisung dar. Über diesen Zeiger wird dann die Funktion Move() aufgerufen. Da der Compiler pDog nur als Mammal-Objekt kennt, sucht er beim Mammal-Objekt nach der Methode Move().

In Zeile 32 wird über den Zeiger die Methode Speak() aufgerufen. Speak() ist virtuell, so daß die überschriebene Speak()-Methode in Dog aufgerufen wird.

Das ist schon fast Zauberei. Der aufrufenden Funktion ist eigentlich nur bekannt, daß sie einen Mammal-Zeiger hat. Hier wird aber eine Methode von Dog aufgerufen. In der Tat ließe sich ein Array von Zeigern auf Mammal einrichten, in dem jeder Zeiger auf eine Unterklasse von Mammal verweist. Führt man nacheinander Aufrufe mit diesen Zeigern aus, aktiviert das Programm immer die korrekte Funktion. Listing 11.9 verdeutlicht dieses Konzept.

Listing 11.9: Mehrere virtuelle Elementfunktionen der Reihe nach aufrufen

1:      // Listing 11.9 Mehrere virtuelle Elementfunktionen der Reihe nach 
2: // aufrufen
3: #include <iostream.h>
4:
5: class Mammal
6: {
7: public:
8: Mammal():itsAge(1) { }
9: virtual ~Mammal() { }
10: virtual void Speak() const { cout << "Saeugetier, gib Laut!\n"; }
11: protected:
12: int itsAge;
13: };
14:
15: class Dog : public Mammal
16: {
17: public:
18: void Speak()const { cout << "Wuff!\n"; }
19: };
20:
21:
22: class Cat : public Mammal
23: {
24: public:
25: void Speak()const { cout << "Miau!\n"; }
26: };
27:
28:
29: class Horse : public Mammal
30: {
31: public:
32: void Speak()const { cout << "Wieher!\n"; }
33: };
34:
35: class Pig : public Mammal
36: {
37: public:
38: void Speak()const { cout << "Grunz!\n"; }
39: };
40:
41: int main()
42: {
43: Mammal* theArray[5];
44: Mammal* ptr;
45: int choice, i;
46: for ( i = 0; i<5; i++)
47: {
48: cout << "(1)Hund (2)Katze (3)Pferd (4)Schwein: ";
49: cin >> choice;
50: switch (choice)
51: {
52: case 1: ptr = new Dog;
53: break;
54: case 2: ptr = new Cat;
55: break;
56: case 3: ptr = new Horse;
57: break;
58: case 4: ptr = new Pig;
59: break;
60: default: ptr = new Mammal;
61: break;
62: }
63: theArray[i] = ptr;
64: }
65: for (i=0;i<5;i++)
66: theArray[i]->Speak();
67: return 0;
68: }

(1)Hund (2)Katze (3)Pferd (4)Schwein: 1
(1)Hund (2)Katze (3)Pferd (4)Schwein: 2
(1)Hund (2)Katze (3)Pferd (4)Schwein: 3
(1)Hund (2)Katze (3)Pferd (4)Schwein: 4
(1)Hund (2)Katze (3)Pferd (4)Schwein: 5
Wuff!
Miau!
Wieher!
Grunz!
Saeugetier, gib Laut!

Dieses abgespeckte Programm, das lediglich die grundlegende Funktionalität für jede Klasse bereitstellt, zeigt virtuelle Elementfunktionen in ihrer reinsten Form. Die vier deklarierten Klassen - Dog, Cat, Horse und Pig (Hund, Katze, Pferd und Schwein) - sind alle von Mammal abgeleitet.

Zeile 10 deklariert die Funktion Speak() von Mammal als virtuell. In den Zeilen 18, 25, 32 und 38 überschreiben die vier abgeleiteten Klassen die Implementierung von Speak().

Das Programm fordert den Anwender auf auszuwählen, welche Objekte zu erzeugen sind. Dementsprechend werden die Zeiger in den Zeilen 46 bis 64 in das Array aufgenommen.

Zur Kompilierzeit ist es nicht möglich, vorauszusagen, welches Objekt erzeugt und welche der Speak()-Methoden demzufolge aufzurufen ist. Der Zeiger ptr wird erst zur Laufzeit an sein Objekt gebunden. Man bezeichnet das als dynamisches Binden oder Binden zur Laufzeit im Gegensatz zum statischen Binden oder Binden zur Kompilierzeit.

Wenn ich eine Elementmethode in der Basisklasse als virtuell deklariere, muß ich sie dann auch in der abgeleiteten Klasse als virtuell markieren?

Antwort: Nein. Ist eine Methode erst einmal virtuell, bleibt sie auch nach dem Überschreiben in einer abgeleiteten Klasse virtuell. Es ist jedoch durchaus zu empfehlen (wenn auch nicht unbedingt nötig), sie auch in den abgeleiteten Klassen als virtuell zu markieren - damit wird der Code leichter zu lesen.

Arbeitsweise virtueller Elementfunktionen

Beim Erzeugen eines abgeleiteten Objekts, etwa eines Dog-Objekts, wird zuerst der Konstruktor für die Basisklasse und dann der Konstruktor für die abgeleitete Klasse aufgerufen. Abbildung 11.2 zeigt, wie sich das Dog-Objekt nach seiner Erzeugung darstellt. Beachten Sie, daß der Mammal-Teil des Objekts angrenzend an den Dog-Teil im Speicher abgelegt ist.

Abbildung 11.2:  Das Dog-Objekt nach seiner Erzeugung

Wenn in einem Objekt eine virtuelle Funktion erzeugt wird, muß das Objekt festhalten, wo diese Funktion zu finden ist. Viele Compiler bauen eine sogenannte V-Tabelle für virtuelle Funktionen auf. Jeder Typ erhält eine eigene Tabelle, und jedes Objekt dieses Typs verwaltet einen virtuellen Tabellenzeiger (einen sogenannten vptr oder V-Zeiger) auf diese Tabelle.

Trotz verschiedenartiger Implementierungen müssen alle Compiler die gleiche Aufgabe umsetzen, so daß diese Beschreibung zumindest im Kern zutrifft.

Jeder vptr eines Objekts zeigt auf die V-Tabelle, die wiederum Zeiger auf alle virtuellen Elementfunktionen enthält. (Hinweis: Zeiger auf Funktionen werden im Kapitel 14, »Spezielle Themen zu Klassen und Funktionen«, eingehend behandelt). Beim Erzeugen des Mammal-Teils von Dog wird vptr mit einem Zeiger auf den entsprechenden Teil der V-Tabelle initialisiert. Abbildung 11.3 verdeutlicht diesen Sachverhalt.

Abbildung 11.3:  Die V-Tabelle eines Mammal-Objekts

Wenn beim Aufruf des Dog-Konstruktors der Dog-Teil dieses Objekts hinzukommt, wird vptr angepaßt und zeigt nun auf die überschriebenen virtuellen Funktionen (falls vorhanden) im Dog-Objekt, siehe Abbildung 11.4.

Abbildung 11.4:  Die V-Tabelle eines Dog-Objekts

Wenn man einen Zeiger auf ein Mammal verwendet, weist der vptr weiterhin - gemäß dem tatsächlichen Typ des Objekts - auf die richtige Funktion. Wenn dann Speak() aufgerufen wird, wird die zu dem Objekt passende Funktion ausgeführt.

Verbotene Zugriffe

Verfügt das Dog-Objekt über eine Methode WagTail(), die im Mammal-Objekt nicht vorhanden ist, kann man mit dem Zeiger auf Mammal nicht auf diese Methode zugreifen (jedenfalls so lange nicht, wie man keine Typumwandlung in einen Zeiger auf Dog vornimmt). Da WagTail() keine virtuelle Funktion ist und sich diese Methode nicht in einem Mammal-Objekt befindet, läßt sich dieser Weg nur über ein Dog-Objekt oder einen Dog-Zeiger beschreiten.

Obwohl man den Mammal-Zeiger in einen Dog-Zeiger umwandeln kann, gibt es wesentlich bessere und sicherere Wege, um die Methode WagTail() aufzurufen. In C++ sollte man auf explizite Typumwandlungen verzichten, da sie fehleranfällig sind. Auf dieses Thema gehen die Kapitel zur Mehrfachvererbung (Kapitel 13) und zu den Templates (Kapitel 19) näher ein.

Slicing (Aufspaltung von Objekten)

Der Zauber der virtuellen Funktionen funktioniert nur bei Zeigern und Referenzen. Die Übergabe eines Objekts als Wert läßt den Aufruf virtueller Elementfunktionen nicht zu. Listing 11.10 verdeutlicht dieses Problem.

Listing 11.10: Slicing bei Übergabe als Wert

1:      // Listing 11.10 Datenteilung bei Übergabe als Wert
2:
3: #include <iostream.h>
4:
5: class Mammal
6: {
7: public:
8: Mammal():itsAge(1) { }
9: virtual ~Mammal() { }
10: virtual void Speak() const { cout << "Saeugetier, gib Laut!\n"; }
11: protected:
12: int itsAge;
13: };
14:
15: class Dog : public Mammal
16: {
17: public:
18: void Speak()const { cout << "Wuff!\n"; }
19: };
20:
21: class Cat : public Mammal
22: {
23: public:
24: void Speak()const { cout << "Miau!\n"; }
25: };
26:
27: void ValueFunction (Mammal);
28: void PtrFunction (Mammal*);
29: void RefFunction (Mammal&);
30: int main()
31: {
32: Mammal* ptr=0;
33: int choice;
34: while (1)
35: {
36: BOOL fQuit = FALSE;
37: cout << "(1)Hund (2)Katze (0)Beenden: ";
38: cin >> choice;
39: switch (choice)
40: {
41: case 0: fQuit = true;
42: break;
43: case 1: ptr = new Dog;
44: break;
45: case 2: ptr = new Cat;
46: break;
47: default: ptr = new Mammal;
48: break;
49: }
50: if (fQuit)
51: break;
52: PtrFunction(ptr);
53: RefFunction(*ptr);
54: ValueFunction(*ptr);
55: }
56: return 0;
57: }
58:
59: void ValueFunction (Mammal MammalValue)
60: {
61: MammalValue.Speak();
62: }
63:
64: void PtrFunction (Mammal * pMammal)
65: {
66: pMammal->Speak();
67: }
68:
69: void RefFunction (Mammal & rMammal)
70: {
71: rMammal.Speak();
72: }

(1)Hunde (2)Katze (0)Beenden: 1
Wuff!
Wuff!
Saeugetier, gib Laut!
(1)Hund (2)Katze (0)Beenden: 2
Miau!
Miau!
Saeugetier, gib Laut!
(1)Hund (2)Katze (0)Beenden: 0

Die Zeilen 5 bis 25 deklarieren abgespeckte Versionen der Klassen Mammal, Dog und Cat. Die drei deklarierten Funktionen PtrFunction(), RefFunction() und ValueFunction() übernehmen einen Zeiger auf ein Mammal, eine Mammal-Referenz bzw. ein Mammal-Objekt. Alle drei Funktionen machen dann das gleiche - sie rufen die Methode Speak() auf.

Der Anwender wird aufgefordert, einen Hund (Dog-Objekt) oder eine Katze (Cat-Objekt) zu wählen. Gemäß der Auswahl erzeugen die Zeilen 43 bis 46 einen Zeiger auf den entsprechenden Typ.

In der ersten Ausgabezeile hat sich der Anwender für einen Hund entschieden. Zeile 43 erzeugt das Dog-Objekt im Heap. Danach erfolgt die Übergabe des Dog-Objekts als Zeiger, als Referenz und als Wert an die drei Funktionen.

Der Zeiger und die Referenzen rufen virtuelle Elementfunktionen auf, und es wird beide Male Elementfunktion Dog->Speak() aktiviert. Das zeigt sich in den beiden ersten Ausgabezeilen nach der Benutzereingabe.

Zuletzt wird der dereferenzierte Zeiger als Wert übergeben. Die aufgerufene Funktion erwartet ein Mammal-Objekt, so daß der Compiler das Dog-Objekt genau bis zum Mammal -Teil auftrennt - das sogenannte Slicing. An diesem Punkt erfolgt der Aufruf der Mammal-Methode Speak(), wie es die dritte Ausgabezeile nach der Benutzerauswahl dokumentiert.

Dieses Experiment wiederholt sich mit entsprechenden Ergebnissen für das Cat-Objekt.

Virtuelle Destruktoren

Es ist zulässig und üblich, einen Zeiger auf ein abgeleitetes Objekt zu übergeben, wenn ein Zeiger auf ein Basisobjekt erwartet wird. Was passiert, wenn dieser Zeiger auf ein abgeleitetes Objekt gelöscht wird? Ist der Destruktor virtuell (wie er es sein sollte), geht alles in Ordnung - der Destruktor der abgeleiteten Klasse wird aufgerufen. Da der Destruktor der abgeleiteten Klasse automatisch den Destruktor der Basisklasse aufruft, wird das gesamte Objekt ordnungsgemäß zerstört.

Als Faustregel für diesen Fall gilt: Wenn irgendeine Funktion in der Klasse virtuell ist, sollte der Destruktor ebenfalls virtuell sein.

Virtuelle Kopierkonstruktoren

Wie bereits erwähnt, können Konstruktoren nicht virtuell sein und deshalb gibt es, technisch gesehen, keinen virtuellen Kopierkonstruktor. Trotzdem kann es vorkommen, daß ein Programm unbedingt einen Zeiger auf ein Basisobjekt übergeben muß und eine Kopie des erzeugten und korrekt abgeleiteten Objekts braucht. Eine übliche Lösung für dieses Problem besteht in der Erzeugung einer Clone()-Methode in der Basisklasse, wobei man diese Methode virtuell deklariert. Eine Clone()-Methode erzeugt eine neue Kopie des aktuellen Objekts und gibt dieses Objekt zurück.

Da jede abgeleitete Klasse die Clone()-Methode überschreibt, wird eine Kopie der abgeleiteten Klasse erzeugt. Listing 11.11 zeigt wie dies in der Praxis aussieht.

Listing 11.11: Virtueller Kopierkonstruktor

1:     // Listing 11.11 Virtueller Kopierkonstruktor
2:
3: #include <iostream.h>
4:
5: class Mammal
6: {
7: public:
8: Mammal():itsAge(1) { cout << "Mammal-Konstruktor...\n"; }
9: virtual ~Mammal() { cout << "Mammal-Destruktor...\n"; }
10: Mammal (const Mammal & rhs);
11: virtual void Speak() const { cout << "Saeugetier, gib Laut!\n"; }
12: virtual Mammal* Clone() { return new Mammal(*this); }
13: int GetAge()const { return itsAge; }
14: protected:
15: int itsAge;
16: };
17:
18: Mammal::Mammal (const Mammal & rhs):itsAge(rhs.GetAge())
19: {
20: cout << "Mammal-Kopierkonstruktor...\n";
21: }
22:
23: class Dog : public Mammal
24: {
25: public:
26: Dog() { cout << "Dog-Konstruktor...\n"; }
27: virtual ~Dog() { cout << "Dog-Destruktor...\n"; }
28: Dog (const Dog & rhs);
29: void Speak()const { cout << "Wuff!\n"; }
30: virtual Mammal* Clone() { return new Dog(*this); }
31: };
32:
33: Dog::Dog(const Dog & rhs):
34: Mammal(rhs)
35: {
36: cout << "Dog-Kopierkonstruktor...\n";
37: }
38:
39: class Cat : public Mammal
40: {
41: public:
42: Cat() { cout << "Cat-Konstruktor...\n"; }
43: ~Cat() { cout << "Cat-Destruktor...\n"; }
44: Cat (const Cat &);
45: void Speak()const { cout << "Miau!\n"; }
46: virtual Mammal* Clone() { return new Cat(*this); }
47: };
48:
49: Cat::Cat(const Cat & rhs):
50: Mammal(rhs)
51: {
52: cout << "Cat-Kopierkonstruktor...\n";
53: }
54:
55: enum ANIMALS { MAMMAL, DOG, CAT};
56: const int NumAnimalTypes = 3;
57: int main()
58: {
59: Mammal *theArray[NumAnimalTypes];
60: Mammal* ptr;
61: int choice, i;
62: for (i = 0; i<NumAnimalTypes; i++)
63: {
64: cout << "(1)Hund (2)Katze (3)Saeugetier: ";
65: cin >> choice;
66: switch (choice)
67: {
68: case DOG: ptr = new Dog;
69: break;
70: case CAT: ptr = new Cat;
71: break;
72: default: ptr = new Mammal;
73: break;
74: }
75: theArray[i] = ptr;
76: }
77: Mammal *OtherArray[NumAnimalTypes];
78: for (i=0;i<NumAnimalTypes;i++)
79: {
80: theArray[i]->Speak();
81: OtherArray[i] = theArray[i]->Clone();
82: }
83: for (i=0;i<NumAnimalTypes;i++)
84: OtherArray[i]->Speak();
85: return 0;
86: }

1:  (1)Hund (2)Katze (3)Saeugetier: 1
2: Mammal-Konstruktor...
3: Dog-Konstruktor...
4: (1)Hund (2)Katze (3)Saeugetier: 2
5: Mammal-Konstruktor...
6: Cat-Konstruktor...
7: (1)Hund (2)Katze (3)Saeugetier: 3
8: Mammal-Konstruktor...
9: Wuff!
10: Mammal-Kopierkonstruktor...
11: Dog-Kopierkonstruktor...
12: Miau!
13: Mammal-Kopierkonstruktor...
14: Cat-Kopierkonstruktor...
15: Saeugetier, gib Laut!
16: Mammal-Kopierkonstruktor...
17: Wuff!
18: Miau!
19: Saeugetier, gib Laut!

Listing 11.11 ist den beiden vorangehenden Listings sehr ähnlich. Lediglich die Mammal -Klasse hat eine neue virtuelle Methode erhalten: Clone)(). Diese Methode gibt einen Zeiger auf ein neues Mammal-Objekt zurück. Die Methode ruft dazu den Kopierkonstruktor auf und übergibt sich dabei selbst (*this) als konstante Referenz.

Dog und Cat überschreiben die Clone()-Methode, wobei sie ihre eigenen Kopierkonstruktoren mit sich selbst als Argument aufrufen. Da Clone() eine virtuelle Methode ist, erzeugt dies praktisch einen virtuellen Kopierkonstruktor, wie es aus Zeile 81 hervorgeht.

Der Anwender wird aufgefordert, zwischen Hunden, Katzen oder Säugetieren zu wählen. Diese werden in den Zeilen 62 bis 74 erzeugt. Zeile 75 speichert einen Zeiger auf jede Auswahl in einem Array.

Beim Durchlaufen des Arrays in den Zeilen 80 und 81 werden für jedes Objekt die Methoden Speak() und Clone() aufgerufen. Der Aufruf von Clone() liefert einen Zeiger auf eine Kopie des Objekts, das Zeile 81 in einem zweiten Array speichert.

In der ersten Ausgabezeile wird der Anwender aufgefordert, eine Auswahl zu treffen. Er entscheidet sich für einen Hund und gibt eine 1 ein. Die Mammal- und Dog-Konstruktoren werden aufgerufen. Das Ganze wiederholt sich für die Konstruktoren von Cat und Mammal in den Ausgabezeilen 4 bis 8.

Zeile 9 der Ausgabe zeigt das Ergebnis des Aufrufs von Speak() für das erste Objekt, das Dog-Objekt. Das Programm ruft die virtuelle Methode Speak() auf und aktiviert die korrekte Version. Es schließt sich der Aufruf der Methode Clone() an. Da sie ebenfalls virtuell ist, wird die Clone()-Methode von Dog aufgerufen, was wiederum zum Aufruf des Mammal-Konstruktors und des Dog-Kopierkonstruktors führt.

Das gleiche wiederholt sich in den Ausgabezeilen 12 bis 14 für Cat und in den Ausgabezeilen 15 und 16 für Mammal. Schließlich wird das neue Array durchlaufen, wobei für jedes neue Objekt die Methode Speak() aufgerufen wird.

Der Preis der virtuellen Methoden

Da Objekte mit virtuellen Methoden eine V-Tabelle einrichten müssen, fallen zusätzliche Verwaltungsaufgaben an. Für eine kleine Klasse, von denen man voraussichtlich keine anderen Klassen ableiten wird, gibt es vermutlich keinen Grund, virtuelle Methoden vorzusehen.

Hat man einmal irgendwelche Methoden als virtuell deklariert, sind die »Anschaffungskosten« für die V-Tabelle zum größten Teil bereits bezahlt (auch wenn jeder Eintrag ein wenig zusätzliche Speicherverwaltung erfordert). Jetzt werden Sie auch einen virtuellen Destruktor einrichten, und man sollte prüfen, ob die anderen Methoden nicht auch virtuell sein sollten. Sehen Sie sich alle nicht virtuellen Methoden eingehend an, und legen Sie sich Rechenschaft darüber ab, warum Sie diese nicht als virtuell deklariert haben.

Was Sie tun sollten

... und was nicht

Verwenden Sie virtuelle Methoden, wenn Sie davon ausgehen, daß von Ihrer Klasse andere Klassen abgeleitet werden.

Richten Sie einen virtuellen Destruktor ein, wenn irgendeine Methode virtuell ist.

Konstruktoren werden nicht als virtuell gekennzeichnet.

Zusammenfassung

In diesem Kapitel haben Sie gelernt, wie man abgeleitete Klassen von Basisklassen erbt. Klassen erben alle öffentlichen (public) und geschützten (protected) Daten und Funktionen ihrer Basisklassen.

Der geschützte Zugriff ist bezüglich der abgeleiteten Klassen öffentlich und zu allen anderen Objekten privat. Auf private Daten oder Funktionen der Basisklassen können selbst abgeleitete Klassen nicht zugreifen.

Konstruktoren lassen sich vor dem Rumpf des Konstruktors initialisieren. Genau zu diesem Zeitpunkt werden die Konstruktoren der Basisklasse aufgerufen, und man kann Parameter an die Basisklasse übergeben.

Funktionen der Basisklasse lassen sich in der abgeleiteten Klasse überschreiben. Wenn die Funktionen der Basisklasse virtuell sind und wenn man auf das Objekt über einen Zeiger oder eine Referenz zugreift, werden die Funktionen der abgeleiteten Klasse entsprechend dem Laufzeittyp des referenzierten Objekts aufgerufen.

Methoden der Basisklasse können aufgerufen werden, indem man vor den Namen der Funktion den Namen der Basisklasse und zwei Doppelpunkte schreibt. Erbt zum Beispiel Dog von Mammal, ruft man die Methode Walk() von Mammal mit Mammal::walk() auf.

In Klassen mit virtuellen Methoden sollte man den Destruktor eigentlich immer als virtuell deklarieren. Ein virtueller Destruktor stellt sicher, daß beim Löschen (delete) von Zeigern auf abgeleitete Objekte auch der abgeleitete Teil des Objekts freigegeben wird. Konstruktoren dürfen nicht virtuell sein. Virtuelle Kopierkonstruktoren lassen sich effektiv erzeugen, indem man eine virtuelle Funktion einrichtet, die den Kopierkonstruktor aufruft.

Fragen und Antworten

Frage:
Werden vererbte Elemente und Funktionen an nachfolgende Generationen weitergereicht? Wenn sich Dog von Mammal ableitet und Mammal von Animal, erbt dann Dog die Funktionen und Daten von Animal?

Antwort:
Ja. Setzt man die Ableitung fort, erben die abgeleiteten Klassen die Gesamtheit aller Funktionen und Daten aller darüberliegenden Basisklassen.

Frage:
Wenn in dem obigen Beispiel Mammal eine Funktion in Animal überschreibt, erhält dann Dog die originale oder die überschriebene Funktion.

Antwort:
Wenn Dog bei der Vererbung auf Mammal basiert, erhält es die Funktion im gleichen Status, in dem sie auch in Mammal vorhanden ist: als überschriebene Funktion.

Frage:
Kann man in einer abgeleiteten Klasse eine öffentliche Basisfunktion als privat deklarieren?

Antwort:
Ja, die abgeleitete Klasse kann die Methode als privat überschreiben. Die Funktion bleibt dann aber für alle nachfolgenden Ableitungen ebenfalls privat.

Frage:
Warum deklariert man nicht alle Funktionen einer Klasse als virtuell?

Antwort:
Für die erste virtuelle Funktion entsteht zusätzlicher Verwaltungsaufwand durch das Anlegen einer V-Tabelle. Danach läßt sich der Overhead vernachlässigen. Viele C++-Programmierer gehen einfach davon aus, daß alle Funktionen virtuell zu sein haben, wenn mindestens eine Funktion als virtuell deklariert ist. Andere Programmierer stimmen damit nicht überein, sondern fragen immer nach einem vernünftigen Grund für eine virtuelle Funktion.

Frage:
Angenommen, EineFunk() sei eine virtuelle Funktion in einer Basisklasse. Diese Funktion wird überladen, um einen oder zwei Integer-Wert(e) zu übernehmen. Die abgeleitete Klasse überschreibt nun die Version mit einem Integer-Wert. Welche Funktion wird dann aktiviert, wenn ein Zeiger auf ein abgeleitetes Objekt die Version mit zwei Integer-Argumenten aufruft?

Antwort:
Das Überschreiben der Version mit einem Integer-Argument verbirgt die gesamte Funktion der Basisklasse. Demzufolge erhält man einen Compiler-Fehler mit dem Hinweis, daß diese Funktion nur einen Integer-Wert erfordert.

Workshop

Der Workshop enthält Quizfragen, die Ihnen helfen sollen, Ihr Wissen zu festigen, und Übungen, die Sie anregen sollen, das eben Gelernte umzusetzen und eigene Erfahrungen zu sammeln. Versuchen Sie, das Quiz und die Übungen zu beantworten und zu verstehen, bevor Sie die Lösungen in Anhang D lesen und zur Lektion des nächsten Tages übergehen.

Quiz

  1. Was ist eine V-Tabelle?
  2. Was ist ein virtueller Destruktor?
  3. Wie deklariert man einen virtuellen Konstruktor?
  4. Wie erzeugt man einen virtuellen Kopierkonstruktor?
  5. Wie rufen Sie eine Elementfunktion einer Basisklasse aus einer abgeleiteten Klasse auf, wenn diese Funktion in der abgeleiteten Klasse überschrieben wurde?
  6. Wie rufen Sie eine Elementfunktion einer Basisklasse aus einer abgeleiteten Klasse auf, wenn diese Funktion in der abgeleiteten Klasse nicht überschrieben wurde?
  7. Wenn eine Basisklasse eine Funktion als virtuell deklariert hat und eine abgeleitete Klasse beim Überschreiben dieser Klasse den Begriff virtuell nicht verwendet, ist die Funktion immer noch virtuell, wenn sie an eine Klasse der dritten Generation vererbt wird?
  8. Wofür wird das Schlüsselwort protected verwendet?

Übungen

  1. Setzen Sie die Deklaration einer virtuellen Funktion auf, die einen Integer als Parameter übernimmt und void zurückliefert.
  2. Geben Sie die Deklaration einer Klasse Square an, die sich von Rectangle ableitet, die wiederum eine Ableitung von Shape ist.
  3. Angenommen, in Übung 2 übernimmt Shape keine Parameter, Rectangle übernimmt zwei (length und width), aber Square übernimmt nur einen (length). Wie sieht die Konstruktorinitialisierung für Square aus?
  4. Schreiben Sie einen virtuellen Kopierkonstruktor für die Klasse Square (aus Übung 3).
  5. FEHLERSUCHE: Was ist falsch in diesem Codefragment?
    void EineFunktion (Shape);
    Shape * pRect = new Rectangle;
    EineFunktion(*pRect);
  6. FEHLERSUCHE: Was ist falsch in diesem Codefragment?
    class Shape()
    {
    public:
    Shape();
    virtual ~Shape();
    virtual Shape(const Shape&);
    };



vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbacknächstes Kapitel


© Markt&Technik Verlag, ein Imprint der Pearson Education Deutschland GmbH