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 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.
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.
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.
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.
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()
).
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.
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.
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.
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.
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.
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.
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.
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 Methodezeichnen()
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 richtigezeichnen()
-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 Zeigerptr
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.
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.
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.
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.
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.
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.
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.
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. |
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.
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.
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.
protected
verwendet?
void
zurückliefert.
Square
an, die sich von Rectangle
ableitet, die wiederum eine Ableitung von Shape
ist.
Shape
keine Parameter, Rectangle
übernimmt zwei (length
und width
), aber Square
übernimmt nur einen (length
). Wie sieht die Konstruktorinitialisierung für Square
aus?
Square
(aus Übung 3).
void EineFunktion (Shape);
Shape * pRect = new Rectangle;
EineFunktion(*pRect);
class Shape()
{
public:
Shape();
virtual ~Shape();
virtual Shape(const Shape&);
};
© Markt&Technik Verlag, ein Imprint der Pearson Education Deutschland GmbH