Vorgestern haben Sie gelernt, wie man virtuelle Funktionen in abgeleiteten Klassen implementiert. Diese bilden den Grundpfeiler der Polymorphie: der Fähigkeit, zur Laufzeit spezialisierte, abgeleitete Klassenobjekte an Zeiger der Basisklassen zu binden. Heute lernen Sie,
Angenommen, Sie arbeiten schon seit einiger Zeit mit Tierklassen und haben Ihre
Klassenhierarchie in Bird
s (Vögel) und Mammal
s (Säugetiere) aufgeteilt. Die Bird
-Klasse
verfügt über die Elementfunktion Fly()
. Die Mammal
-Klasse wurde unterteilt in eine
Reihe von Mammal
s, einschließlich Horse
(Pferd). Horse
verfügt über die Elementfunktionen
Whinny()
und Gallop()
.
Plötzlich stellen Sie fest, daß Sie ein Pegasus
-Objekt benötigen - eine Kreuzung zwischen
Horse
und Bird
. Ein Pegasus
kann fliegen (Fly()
), er kann wiehern (Whinny()
) und
er kann galoppieren (Gallop()
). Mit einfacher Vererbung kommen Sie da nicht weiter.
Sie können Pegasus
zu einer Bird
-Klasse machen, doch dann könnte er weder wiehern
noch galoppieren. Oder Sie deklarieren ihn als Horse
-Klasse, dann jedoch könnte
er nicht fliegen.
Eine Lösung wäre, die Methode Fly()
in die Pegasus
-Klasse zu kopieren und Pegasus
von Horse
abzuleiten. Das funktioniert auch problemlos, hat allerdings den Nachteil,
daß Fly()
jetzt in zwei Klassen vorhanden ist (Bird
und Pegasus
). Wenn Sie Änderungen
an der einen Methode vornehmen, müssen Sie auch daran denken, die andere
Methode zu ändern. Selbstverständlich muß der Programmierer, der Monate oder
Jahre später die Aufgabe hat, Ihren Code zu überholen, ebenfalls wissen, daß der
Code an zwei Stellen zu ändern ist.
Über kurz oder lang wird noch ein weiteres Problem auftauchen, dann nämlich, wenn
Sie eine Liste von Horse
-Objekten und eine Liste von Bird
-Objekten erstellen und in
beide Listen Pegasus
-Objekte einfügen möchten. Wenn aber Pegasus
von Horse
abgeleitet
wurde, können Sie es nicht in die Liste der Bird
-Objekte aufnehmen.
Es gibt für solche Probleme eine ganze Reihe von möglichen Lösungen. Sie könnten
die Horse
-Methode Gallop()
in Move()
umbenennen und dann Move()
in Ihrem Pegasus
überschreiben, so daß es die Arbeit von Fly()
ausführt. In Ihren anderen Horse
-
Objekten würden Sie Move()
dann ebenfalls überschreiben, so daß es die Arbeit von
Gallop()
ausführt. Vielleicht ist Pegasus
schlau genug, nur kurze Strecken zu galoppieren
und längere Strecken zu fliegen.
Pegasus::Move(long distance)
{
if (distance > veryFar)
fly(distance);
else
gallop(distance);
}
Ein solcher Ansatz ist allerdings sehr beschränkt, was später zu Problemen führen
kann. Was ist, wenn Pegasus eines Tages eine kurze Strecke fliegen und eine lange
Strecke galoppieren möchte? Ihr nächster Lösungsvorschlag könnte vorsehen, Move(),
wie in Listing 13.1 demonstriert, nach oben in die Klasse
Horse auszulagern.
Dann haben Sie allerdings das Problem, daß die meisten Pferde nicht fliegen können
und die Methode so implementiert werden muß, daß sie nur für Pegasus
-Objekte Code
ausführt.
Listing 13.1: Wenn Pferde fliegen könnten ...
1: // Listing 13.1. Wenn Pferde fliegen könnten...
2: // Fly() in Horse auslagern
3:
4: #include <iostream.h>
5:
6: class Horse
7: {
8: public:
9: void Gallop(){ cout << "Galoppiert...\n"; }
10: virtual void Fly() { cout << "Pferde koennen nicht fliegen.\n" ; }
11: private:
12: int itsAge;
13: };
14:
15: class Pegasus : public Horse
16: {
17: public:
18: virtual void Fly() {cout <<"Ich kann fliegen! Ich kann fliegen!\n";}
19: };
20:
21: const int NumberHorses = 5;
22: int main()
23: {
24: Horse* Ranch[NumberHorses];
25: Horse* pHorse;
26: int choice,i;
27: for (i=0; i<NumberHorses; i++)
28: {
29: cout << "(1)Pferd (2)Pegasus: ";
30: cin >> choice;
31: if (choice == 2)
32: pHorse = new Pegasus;
33: else
34: pHorse = new Horse;
35: Ranch[i] = pHorse;
36: }
37: cout << "\n";
38: for (i=0; i<NumberHorses; i++)
39: {
40: Ranch[i]->Fly();
41: delete Ranch[i];
42: }
43: return 0;
44: }
(1)Pferd (2)Pegasus: 1
(1)Pferd (2)Pegasus: 2
(1)Pferd (2)Pegasus: 1
(1)Pferd (2)Pegasus: 2
(1)Pferd (2)Pegasus: 1
Pferde koennen nicht fliegen.
Ich kann fliegen! Ich kann fliegen!
Pferde koennen nicht fliegen.
Ich kann fliegen! Ich kann fliegen!
Pferde koennen nicht fliegen.
Es läßt sich nicht leugnen: Das Programm funktioniert. Wenn auch zu Lasten der Horse
-Klasse, die jetzt eine Fly()
-Methode aufweist (Zeile 10). In einer echten Anwendung
würde man die Fly()
-Methode in Horse
so implementieren, daß sie eine Fehlermeldung
ausgibt oder zumindest, ohne weitere Probleme zu verursachen, abbricht. In
Zeile 18 überschreibt die Pegasus
-Klasse die Fly()
-Methode, um die Methode an ihre
eigenen Bedürfnisse anzupassen: in unserem Falle, um eine Freudenbotschaft auszugeben.
Das Array von Horse
-Zeigern in Zeile 24 wird benötigt, um zu zeigen, daß je nach
Laufzeitbindung an ein Horse
- oder ein Pegasus-Objekt stets die korrekte Fly()
-Methode
für die Zeiger aufgerufen wird.
Diese Beispiele wurden bis zum Kern abgespeckt, um die Betrachtung auf das Wesentliche zu konzentrieren. Auf Konstruktoren, virtuelle Destruktoren und so weiter wurde verzichtet, um den Code möglichst einfach zu halten.
Es ist absolut üblich, zur Lösung solcher Probleme die erforderlichen Funktionen in der Klassenhierarchie von unten nach oben durchzureichen. Die Basisklasse läuft dabei allerdings Gefahr, zum globalen Namensbereich für alle Funktionen zu mutieren, die eventuell von einer der abgeleiteten Klassen verwendet werden. Dies kann das Klassenprinzip von C++ ernsthaft unterminieren und zu großen und unhandlichen Basisklassen führen.
Generell liegt der Sinn der Auslagerung in Basisklassen darin, gemeinsame Funktionalität
in der Hierarchie hochzureichen. Wenn also zwei Klassen eine gemeinsame Basisklasse
haben (z.B. Horse
und Bird
beide von Animal
abgeleitet sind) und ihnen eine
Funktion gemeinsam ist (beispielsweise essen sowohl Pferde als auch Vögel), würde
man diese Funktionalität in die Basisklasse auslagern und eine virtuelle Funktion einrichten.
Sie sollten jedoch vermeiden, Schnittstellenelemente (wie Fly()
) an Basisklassen
durchzureichen, in denen sie nichts zu suchen haben, nur damit diese Funktion dann
für einige wenige abgeleitete Klassen zur Verfügung steht.
Eine Alternative zu obigem Ansatz, die ebenfalls noch mit den Mitteln der Einfachvererbung
auskommt, bestünde darin, Fly()
innerhalb von Pegasus
zu halten und sie nur
aufzurufen, wenn der Zeiger tatsächlich auf ein Pegasus
-Objekt zeigt. Damit das funktioniert,
müssen Sie befähigt sein, Ihren Zeiger zur Laufzeit zu befragen, auf welchen
Typ er nun eigentlich zeigt. Man bezeichnet das auch als Laufzeit-Typidentifizierung
(RTTI). RTTI ist erst seit kurzem offizieller Bestandteil von C++.
Wenn Ihr Compiler RTTI nicht unterstützt, können Sie RTTI simulieren, indem Sie
eine Methode in jede der Klassen aufnehmen, die einen Aufzählungstyp zurückliefert.
Sie können dann diesen Typen zur Laufzeit abfragen und Fly()
aufrufen, wenn Pegasus
zurückgeliefert wird.
Die Zufluchtnahme zu RTTI kann ein Indiz für ein schlecht konzipiertes Programm sein. Erwägen Sie statt dessen den Einsatz von virtuellen Funktionen, Templates oder Mehrfachvererbung.
Um Fly()
aufrufen zu können, müssen Sie den Typ des Zeigers umwandeln, damit er
weiß, daß das Objekt, auf das er zeigt, ein Pegasus
-Objekt und kein Horse
-Objekt ist.
Dies nennt man abwärts gerichtete Typumwandlung, da Sie den Typ des Horse
-Objekts
in einen abgeleiteten Typ umwandeln.
C++ unterstützt mittlerweile offiziell, wenn vielleicht auch etwas widerstrebend, die abwärts
gerichtete Typumwandlung und stellt dazu den neuen Operator dynamic_cast
zur Verfügung. Dies funktioniert so:
Wenn Sie einen Zeiger auf eine Basisklasse wie Horse
haben, und sie dem Zeiger einen
Zeiger auf eine abgeleitete Klasse wie Pegasus
zuweisen, können Sie den Horse
-
Zeiger polymorph verwenden. Um dabei über diesen Zeiger auf das Pegasus
-Objekt
zuzugreifen, müssen Sie den Zeiger mit Hilfe des dynamic_cast
-Operator in einen Pegasus
-Zeiger umwandeln.
Zuerst wird der Basiszeiger zur Laufzeit geprüft. Ist die Umwandlung zulässig, können
Sie mit Ihrem neuen Pegasus
-Zeiger wie gewünscht weiterarbeiten. Ist die Umwandlung
unzulässig, z.B. wenn Sie überhaupt kein Pegasus
-Objekt haben, erhalten Sie einen
Null-Zeiger. Listing 13.2 soll dies veranschaulichen.
Listing 13.2: Abwärts gerichtete Typumwandlung
1: // Listing 13.2 Der Operator dynamic_cast.
2: // Verwendung von rtti
3:
4: #include <iostream.h>
5: enum TYPE { HORSE, PEGASUS };
6:
7: class Horse
8: {
9: public:
10: virtual void Gallop(){ cout << "Galoppiert...\n"; }
11:
12: private:
13: int itsAge;
14: };
15:
16: class Pegasus : public Horse
17: {
18: public:
19:
20: virtual void Fly() {cout <<"Ich kann fliegen! Ich kann fliegen!\n";}
21: };
22:
23: const int NumberHorses = 5;
24: int main()
25: {
26: Horse* Ranch[NumberHorses];
27: Horse* pHorse;
28: int choice,i;
29: for (i=0; i<NumberHorses; i++)
30: {
31: cout << "(1)Pferd (2)Pegasus: ";
32: cin >> choice;
33: if (choice == 2)
34: pHorse = new Pegasus;
35: else
36: pHorse = new Horse;
37: Ranch[i] = pHorse;
38: }
39: cout << "\n";
40: for (i=0; i<NumberHorses; i++)
41: {
42: Pegasus *pPeg = dynamic_cast< Pegasus *> (Ranch[i]);
42: if (pPeg)
43: pPeg->Fly();
44: else
45: cout << "Nur ein Pferd\n";
46:
47: delete Ranch[i];
48: }
49: return 0;
50: }
(1)Pferd (2)Pegasus: 1
(1)Pferd (2)Pegasus: 2
(1)Pferd (2)Pegasus: 1
(1)Pferd (2)Pegasus: 2
(1)Pferd (2)Pegasus: 1
Nur ein Pferd
Ich kann fliegen! Ich kann fliegen!
Nur ein Pferd
Ich kann fliegen! Ich kann fliegen!
Nur ein Pferd
Beim Kompilieren habe ich von Microsoft Visual C++ eine Warnung erhalten: »warning C4541: 'dynamic_cast' fuer polymorphen Typ 'class Horse' mit /GR- verwendet; unvorhersehbares Verhalten moeglich«. Was soll ich machen?
Antwort: Dies ist eine der verwirrendsten Fehlermeldungen der MFC. Beheben Sie den Fehler folgendermaßen:
Diese Lösung funktioniert ebenfalls. Fly()
wird aus der Horse
-Klasse herausgehalten
und nicht für Horse
-Objekte aufgerufen. Wenn sie jedoch für Pegasus
-Objekte aufgerufen
werden soll, muß explizit eine Typumwandlung erfolgen, denn Horse
-Objekte verfügen
ja über keine Fly()
-Methode. Dem Zeiger muß daher mitgeteilt werden, daß er
auf ein Pegasus
-Objekt zeigt, bevor er verwendet werden kann.
Die Notwendigkeit, eine Typumwandlung für das Pegasus
-Objekt vorzunehmen, sollte
Ihnen eine Warnung sein, daß eventuell mit Ihrem Design etwas nicht stimmt. Ein solches
Programm untergräbt die Polymorphie virtueller Funktionen, da es darauf beruht,
daß der Typ des Objekts zur Laufzeit umgewandelt wird.
Ein anderes Manko dieser Lösungsansätze ist, daß sie Pegasus
als vom Typ Horse
deklariert
haben - mit der Konsequenz, daß Sie Pegasus
-Objekte nicht in Bird
-Listen aufnehmen
können. Obwohl Sie also den Preis bezahlt haben, entweder Fly()
nach
oben an Horse
durchzureichen oder eine Typumwandlung des Zeigers vorzunehmen,
erhalten Sie dennoch nicht die volle Funktionalität, die Sie benötigen.
Einen weiteren Ansatz zur Lösung des Problems mittels einfacher Vererbung gäbe es
noch. Sie könnten Fly()
, Whinny()
und Gallop()
alle in eine gemeinsame Basisklasse
von Bird
und Horse
, z.B. Animal
, verschieben. Anstatt jetzt eine Liste von Horse
und
eine von Bird
anzulegen, haben Sie eine gemeinsame Liste von Animal
. Das funktioniert
ohne Frage, verlagert aber noch mehr Funktionalität in die Basisklassen.
Alternativ könnten Sie die Methoden dort lassen, wo sie sind, und statt dessen für den
Zugriff auf die Horse
-, Bird
- und Pegasus
-Objekte zur Laufzeit die Typen der Basisklassenzeiger
umwandeln, doch das ist sogar noch schlimmer.
Es ist möglich, eine neue Klasse von mehr als einer Basisklasse abzuleiten. Dies wird
auch als Mehrfachvererbung bezeichnet. Um die Ableitung von mehr als einer Basisklasse
zu deklarieren, müssen Sie die einzelnen Basisklasse in der Klassendefinition
durch Kommata getrennt angeben. Listing 13.3 zeigt Ihnen, wie Sie Pegasus
deklarieren,
so daß es sowohl von der Horse
-Klasse als auch von der Bird
-Klasse erbt. Anschließend
nimmt das Programm die Pegasus
-Objekte in beide Listentypen auf.
Listing 13.3: Mehrfachvererbung
1: // Listing 13.3. Mehrfachvererbung.
2: // Mehrfachvererbung
3:
4: #include <iostream.h>
5:
6: class Horse
7: {
8: public:
9: Horse() { cout << "Horse-Konstruktor... "; }
10: virtual ~Horse() { cout << "Horse-Destruktor... "; }
11: virtual void Whinny() const { cout << "Wieher!... "; }
12: private:
13: int itsAge;
14: };
15:
16: class Bird
17: {
18: public:
19: Bird() { cout << "Bird-Konstruktor... "; }
20: virtual ~Bird() { cout << "Bird-Destruktor... "; }
21: virtual void Chirp() const { cout << "Chirp... "; }
22: virtual void Fly() const
23: {
24: cout <<"Ich kann fliegen! Ich kann fliegen! Ich kann fliegen!";
25: }
26: private:
27: int itsWeight;
28: };
29:
30: class Pegasus : public Horse, public Bird
31: {
32: public:
33: void Chirp() const { Whinny(); }
34: Pegasus() { cout << "Pegasus-Konstruktor... "; }
35: ~Pegasus() { cout << "Pegasus-Destruktor... "; }
36: };
37:
38: const int MagicNumber = 2;
39: int main()
40: {
41: Horse* Ranch[MagicNumber];
42: Bird* Aviary[MagicNumber];
43: Horse * pHorse;
44: Bird * pBird;
45: int choice,i;
46: for (i=0; i<MagicNumber; i++)
47: {
48: cout << "\n(1)Pferd (2)Pegasus: ";
49: cin >> choice;
50: if (choice == 2)
51: pHorse = new Pegasus;
52: else
53: pHorse = new Horse;
54: Ranch[i] = pHorse;
55: }
56: for (i=0; i<MagicNumber; i++)
57: {
58: cout << "\n(1)Vogel (2)Pegasus: ";
59: cin >> choice;
60: if (choice == 2)
61: pBird = new Pegasus;
62: else
63: pBird = new Bird;
64: Aviary[i] = pBird;
65: }
66:
67: cout << "\n";
68: for (i=0; i<MagicNumber; i++)
69: {
70: cout << "\nRanch[" << i << "]: " ;
71: Ranch[i]->Whinny();
72: delete Ranch[i];
73: }
74:
75: for (i=0; i<MagicNumber; i++)
76: {
77: cout << "\nVogelhaus[" << i << "]: " ;
78: Aviary[i]->Chirp();
79: Aviary[i]->Fly();
80: delete Aviary[i];
81: }
82: return 0;
83: }
(1)Pferd (2)Pegasus: 1
Horse-Konstruktor...
(1)Pferd (2)Pegasus: 2
Horse-Konstruktor... Bird-Konstruktor... Pegasus-Konstruktor...
(1)Vogel (2)Pegasus: 1
Bird-Konstruktor...
(1)Vogel (2)Pegasus: 2
Horse-Konstruktor... Bird-Konstruktor... Pegasus-Konstruktor...
Ranch[0]: Wieher!... Horse-Destruktor...
Ranch[1]: Wieher!... Pegasus-Destruktor... Bird-Destruktor...
Horse-Destruktor...
Vogelhaus[0]: Chirp... Ich kann fliegen! Ich kann fliegen! Ich kann fliegen! Bird-Destruktor...
Vogelhaus[1]: Wieher!... Ich kann fliegen! Ich kann fliegen! Ich kann fliegen!
Pegasus-Destruktor... Bird-Destruktor... Horse-Destruktor...
In den Zeilen 6 bis 14 wird die Horse
-Klasse deklariert. Der Konstruktor und der Destruktor
geben eine Nachricht und die Whinny()
-Methode das Wort Wieher!
aus.
Die Zeilen 16 bis 28 deklarieren die Bird
-Klasse. Zusätzlich zu dem Konstruktor und
dem Destruktor weist die Klasse zwei Methoden auf: Chirp()
und Fly()
, die beide eine
Nachricht ausgeben, anhand deren man erkennen kann, welche Methode aufgerufen
wurde. In einem richtigen Programm könnten sie zum Beispiel den Lautsprecher ansprechen
oder eine Animation ablaufen lassen.
Zuletzt wird in den Zeilen 30 bis 36 die Klasse Pegasus
deklariert. Sie wird sowohl von
Horse
als auch von Bird
abgeleitet. Die Pegasus
-Klasse überschreibt die Chirp()
-Methode
mit Whinny()
, die sie von Horse
geerbt hat.
In der main()
-Funktion werden zwei Listen erzeugt: Ranch
in Zeile 41 mit Zeigern auf
Horse,
und Aviary
(Vogelhaus) in Zeile 42 mit Zeigern auf Bird
. Die Zeilen 46 bis 55
hängen Horse
- und Pegasus
-Objekte an die Ranch
-Liste an und die Zeilen 56 bis 65
Bird
- und Pegasus
-Objekte an die Aviary
-Liste.
Wenn jetzt über Bird
- oder Horse
-Zeiger die virtuellen Methoden aufgerufen werden,
werden auch für Pegasus
-Objekte die korrekten Befehle ausgeführt. So werden zum
Beispiel in Zeile 78 die Zeiger im Aviary
-Array durchlaufen, um für die verschiedenen
Objekte, auf die die Zeiger weisen, die Chirp()
-Methode aufzurufen. Die Bird
-Klasse
deklariert Chirp()
als virtuelle Methode, so daß für jedes Objekt die richtige Funktion
aufgerufen wird.
Beachten Sie, daß jedes Mal, wenn ein Pegasus
-Objekt erzeugt wird, die Ausgabe widerspiegelt,
daß dabei sowohl ein Bird
- als auch ein Horse
-Teil erzeugt werden. Wird
ein Pegasus
-Objekt zerstört, werden die Bird
- und Horse
-Teile dank der virtuellen Destruktoren
korrekt aufgelöst.
Deklaration der Mehrfachvererbung
Für die Deklaration eines Objekts, das von mehr als einer Klasse erben soll, müssen Sie nach dem Doppelpunkt, der auf den Klassennamen folgt, die Basisklassen getrennt durch Kommata angeben.
class Pegasus : public Horse, public Birdclass Schnudel : public Schnauzer, public Pudel
Wenn das Pegasus
-Objekt im Speicher erzeugt wird, erzeugen beide Basisklassen im
Pegasus
-Objekt eigene Kompartimente (siehe Abbildung 13.1).
Abbildung 13.1: Mehrfach vererbte Objekte
Im Zusammenhang mit Objekten, die sich von mehreren Basisklassen ableiten, stellen sich etliche Fragen. Was geschieht zum Beispiel, wenn zwei Basisklassen, die den gleichen Namen tragen, virtuelle Funktionen oder Daten aufweisen? Wie werden Konstruktoren von mehreren Basisklassen initialisiert? Was passiert, wenn mehrere Basisklassen von der gleichen Klasse abgeleitet wurden? Im nächsten Abschnitt möchte ich auf diese Fragen eingehen und zeigen, wie Mehrfachvererbung in der Praxis genutzt werden kann.
Wenn sich Pegasus
von Bird
und Horse
ableitet und jede dieser Basisklassen über Konstruktoren
verfügt, die Parameter übernehmen, werden diese Konstruktoren von der
Pegasus
-Klasse aufgerufen. Listing 13.4 veranschaulicht dies.
Listing 13.4: Mehrere Konstruktoren aufrufen
1: // Listing 13.4
2: // Mehrere Konstruktoren aufrufen
3: #include <iostream.h>
4: typedef int HANDS;
5: enum COLOR { Red, Green, Blue, Yellow, White, Black, Brown } ;
6:
7: class Horse
8: {
9: public:
10: Horse(COLOR color, HANDS height);
11: virtual ~Horse() { cout << "Horse-Destruktor...\n"; }
12: virtual void Whinny()const { cout << "Wieher!... "; }
13: virtual HANDS GetHeight() const { return itsHeight; }
14: virtual COLOR GetColor() const { return itsColor; }
15: private:
16: HANDS itsHeight;
17: COLOR itsColor;
18: };
19:
20: Horse::Horse(COLOR color, HANDS height):
21: itsColor(color),itsHeight(height)
22: {
23: cout << "Horse-Konstruktor...\n";
24: }
25:
26: class Bird
27: {
28: public:
29: Bird(COLOR color, bool migrates);
30: virtual ~Bird() {cout << "Bird-Destruktor...\n"; }
31: virtual void Chirp()const { cout << "Chirp... "; }
32: virtual void Fly()const
33: {
34: cout <<"Ich kann fliegen! Ich kann fliegen! Ich kann fliegen!";
35: }
36: virtual COLOR GetColor()const { return itsColor; }
37: virtual bool GetMigration() const { return itsMigration; }
38:
39: private:
40: COLOR itsColor;
41: bool itsMigration;
42: };
43:
44: Bird::Bird(COLOR color, bool migrates):
45: itsColor(color), itsMigration(migrates)
46: {
47: cout << "Bird-Konstruktor...\n";
48: }
49:
50: class Pegasus : public Horse, public Bird
51: {
52: public:
53: void Chirp()const { Whinny(); }
54: Pegasus(COLOR, HANDS, bool,long);
55: ~Pegasus() {cout << "Pegasus-Destruktor...\n";}
56: virtual long GetNumberBelievers() const
57: {
58: return itsNumberBelievers;
59: }
60:
61: private:
62: long itsNumberBelievers;
63: };
64:
65: Pegasus::Pegasus(
66: COLOR aColor,
67: HANDS height,
68: bool migrates,
69: long NumBelieve):
70: Horse(aColor, height),
71: Bird(aColor, migrates),
72: itsNumberBelievers(NumBelieve)
73: {
74: cout << "Pegasus-Konstruktor...\n";
75: }
76:
77: int main()
78: {
79: Pegasus *pPeg = new Pegasus(Red, 5, true, 10);
80: pPeg->Fly();
81: pPeg->Whinny();
82: cout << "\nIhr Pegasus ist " << pPeg->GetHeight();
83: cout << " cm groß und ";
84: if (pPeg->GetMigration())
85: cout << "geht auf Wanderschaft.";
86: else
87: cout << "geht nicht auf Wanderschaft.";
88: cout << "\nInsgesamt " << pPeg->GetNumberBelievers();
89: cout << " Leute glauben, er existiert.\n";
90: delete pPeg;
91: return 0;
92: }
Horse-Konstruktor...
Bird-Konstruktor...
Pegasus-Konstruktor...
Ich kann fliegen! Ich kann fliegen! Ich kann fliegen! Wieher!...
Ihr Pegasus ist 50 cm groß und geht auf Wanderschaft.
Insgesamt 10 Leute glauben, er existiert.
Pegasus-Destruktor...
Bird-Destruktor...
Horse-Destruktor...
Die Zeilen 7 bis 18 deklarieren die Klasse Horse
. Der Konstruktor übernimmt zwei
Parameter: einen Aufzählungstypen, der in Zeile 5 deklariert ist, und einen typedef
-
Alias-Typen, deklariert in Zeile 4. Die Implementierung des Konstruktors in den Zeilen
20 bis 24 initialisiert lediglich die Elementvariablen und gibt eine Meldung aus.
Die Zeilen 26 bis 42 deklarieren die Bird
-Klasse, deren Konstruktor in den Zeilen 45
bis 49 implementiert wird. Die Bird
-Klasse übernimmt zwei Parameter. Interessant
hieran ist, daß sowohl der Horse
-Konstruktor als auch der Bird
-Konstruktor je einen
color
-Parameter übernehmen. Dies führt zu einem Problem, wie Sie im nächsten Beispiel
feststellen werden.
Die Deklaration der Pegasus
-Klasse erfolgt in den Zeilen 50 bis 63 und die des dazugehörigen
Konstruktors in den Zeilen 65 bis 75. Die Initialisierung des Pegasus
-Objekts
umfaßt drei Anweisungen. Zuerst wird der Horse
-Konstruktor mit color
und height
initialisiert.
Dann wird der Bird
-Konstruktor mit color
und einem Boole'schen Wert initialisiert.
Zum Schluß erfolgt die Initialisierung der Pegasus
-Elementvariablen itsNumberBelievers
. Nachdem dies alles erledigt ist, wird der Rumpf des Pegasus
-
Konstruktors aufgerufen.
In main()
wird ein Pegasus
-Zeiger erzeugt, der dazu dient, auf die Elementfunktionen
der Basisobjekte zuzugreifen.
In Listing 13.4 verfügen sowohl die Horse
-Klasse als auch die Bird
-Klasse über eine
Methode namens GetColor()
. Wenn Sie in die Verlegenheit kommen, das Pegasus
-
Objekt aufzufordern, seine Farbe (color
) zurückzugeben, haben Sie ein Problem: Die
Pegasus
-Klasse erbt von beiden, der Horse
- und der Bird
-Klasse. Beide haben eine Farbe
und beide verwenden Methoden gleichen Namens und gleicher Signatur, um die
Farbe abzufragen. Dies führt zu Mehrdeutigkeiten für den Compiler, die Sie erst auflösen
müssen.
COLOR currentColor = pPeg->GetColor();
erhalten Sie den Compiler-Fehler:
Member ist ambiguous: 'Horse::GetColor' and 'Bird::GetColor'
Diese Doppeldeutigkeit läßt sich durch einen direkten Aufruf der Funktion, die gewünscht wird, vermeiden:
COLOR currentColor = pPeg->Horse::GetColor();
Immer wenn Sie genau angeben müssen, von welcher Klasse eine Elementfunktion oder ein Datenelement erben, müssen Sie beim Aufruf den vollen Qualifizierer angeben, indem Sie den Klassennamen vor das Datenelement oder die Funktion der Basisklasse stellen.
Beachten Sie, daß für den Fall, daß Pegasus
diese Funktion überschreiben sollte, das
Problem in die Elementfunktion von Pegasus
verschoben wird:
virtual COLOR GetColor()const { return Horse::GetColor(); }
Das verbirgt das Problem vor den Klienten der Pegasus
-Klasse und kapselt die Information,
von welcher Basisklasse color
geerbt werden soll, in Pegasus
. Ein Klient hat
dann aber immer noch die Möglichkeit, einen bestimmten Aufruf zu erzwingen:
COLOR currentColor = pPeg->Bird::GetColor();
Was passiert, wenn Bird
und Horse
von einer gemeinsamen Basisklasse, wie zum Beispiel
Animal
erben? Abbildung 13.2 veranschaulicht die Abhängigkeiten.
Abbildung 13.2: Gemeinsame Basisklassen
Wie Sie in Abbildung 13.2 sehen können, gibt es zwei Basisklassenobjekte. Wird eine
Funktion oder ein Datenelement in der gemeinsamen Basisklasse aufgerufen, führt
das zu einer weiteren Mehrdeutigkeit. Angenommen Animal
deklariert die Elementvariable
itsAge
und die Elementfunktion GetAge()
. Wenn Sie jetzt pPeg->GetAge()
aufrufen,
stellt sich die Frage, ob Sie die Funktion GetAge()
aufrufen möchten, die Sie über
Horse
von Animal
geerbt haben oder ob Sie die GetAge()
-Funktion aufrufen möchten,
die Sie über Bird
von Animal
geerbt haben Auch diese Mehrdeutigkeit müssen Sie auflösen.
Einen Lösungsweg zeigt Ihnen Listing 13.5.
Listing 13.5: Gemeinsame Basisklassen
1: // Listing 13.5
2: // Gemeinsame Basisklassen
3: #include <iostream.h>
4:
5: typedef int HANDS;
6: enum COLOR { Red, Green, Blue, Yellow, White, Black, Brown } ;
7:
8: class Animal // gemeinsame Basis fuer horse und bird
9: {
10: public:
11: Animal(int);
12: virtual ~Animal() { cout << "Animal-Destruktor...\n"; }
13: virtual int GetAge() const { return itsAge; }
14: virtual void SetAge(int age) { itsAge = age; }
15: private:
16: int itsAge;
17: };
18:
19: Animal::Animal(int age):
20: itsAge(age)
21: {
22: cout << "Animal-Konstruktor...\n";
23: }
24:
25: class Horse : public Animal
26: {
27: public:
28: Horse(COLOR color, HANDS height, int age);
29: virtual ~Horse() { cout << "Horse-Destruktor...\n"; }
30: virtual void Whinny()const { cout << "Wieher!... "; }
31: virtual HANDS GetHeight() const { return itsHeight; }
32: virtual COLOR GetColor() const { return itsColor; }
33: protected:
34: HANDS itsHeight;
35: COLOR itsColor;
36: };
37:
38: Horse::Horse(COLOR color, HANDS height, int age):
39: Animal(age),
40: itsColor(color),itsHeight(height)
41: {
42: cout << "Horse-Konstruktor...\n";
43: }
44:
45: class Bird : public Animal
46: {
47: public:
48: Bird(COLOR color, bool migrates, int age);
49: virtual ~Bird() {cout << "Bird-Destruktor...\n"; }
50: virtual void Chirp()const { cout << "Chirp... "; }
51: virtual void Fly()const
52: { cout << "Ich kann fliegen! Ich kann fliegen! "; }
53: virtual COLOR GetColor()const { return itsColor; }
54: virtual bool GetMigration() const { return itsMigration; }
55: protected:
56: COLOR itsColor;
57: bool itsMigration;
58: };
59:
60: Bird::Bird(COLOR color, bool migrates, int age):
61: Animal(age),
62: itsColor(color), itsMigration(migrates)
63: {
64: cout << "Bird-Konstruktor...\n";
65: }
66:
67: class Pegasus : public Horse, public Bird
68: {
69: public:
70: void Chirp()const { Whinny(); }
71: Pegasus(COLOR, HANDS, bool, long, int);
72: virtual ~Pegasus() {cout << "Pegasus-Destruktor...\n";}
73: virtual long GetNumberBelievers() const
74: { return itsNumberBelievers; }
75: virtual COLOR GetColor()const { return Horse::itsColor; }
76: virtual int GetAge() const { return Horse::GetAge(); }
77: private:
78: long itsNumberBelievers;
79: };
80:
81: Pegasus::Pegasus(
82: COLOR aColor,
83: HANDS height,
84: bool migrates,
85: long NumBelieve,
86: int age):
87: Horse(aColor, height,age),
88: Bird(aColor, migrates,age),
89: itsNumberBelievers(NumBelieve)
90: {
91: cout << "Pegasus-Konstruktor...\n";
92: }
93:
94: int main()
95: {
96: Pegasus *pPeg = new Pegasus(Red, 5, true, 10, 2);
97: int age = pPeg->GetAge();
98: cout << "Dieser Pegasus ist " << age << " Jahre alt.\n";
99: delete pPeg;
100: return 0;
101: }
Animal-Konstruktor...
Horse-Konstruktor...
Animal-Konstruktor...
Bird-Konstruktor...
Pegasus-Konstruktor...
Dieser Pegasus ist 2 Jahre alt.
Pegasus-Destruktor...
Bird-Destruktor...
Animal-Destruktor...
Horse-Destruktor...
Animal-Destruktor...
Dieses Listing weist einige interessante Besonderheiten auf. Die Zeilen 8 bis 17 deklarieren
die Animal
-Klasse. Animal
enthält eine Elementvariable namens itsAge
und zwei
Zugriffsfunktionen, GetAge()
und SetAge()
.
Zeile 25 deklariert die Klasse Horse
als Ableitung von Animal
. Der Horse
-Konstruktor
übernimmt in diesem Fall einen dritten Parameter, age
, den er seiner Basisklasse Animal
übergibt. Beachten Sie, daß die Horse
-Klasse GetAge()
nicht überschreibt, sondern
einfach nur erbt.
Zeile 45 deklariert die von Animal
abgeleitete Bird
-Klasse. Deren Konstruktor übernimmt
ebenfalls den Parameter age
und verwendet ihn, um die Basisklasse Animal
zu
initialisieren. Auch Bird
erbt GetAge()
, ohne die Methode zu überschreiben.
Pegasus
erbt sowohl von Bird
als auch von Animal
und weist damit zwei Animal
-Klassen
in seiner Vererbungskette auf. Wenn Sie GetAge()
für ein Pegasus
-Objekt aufzurufen
hätten, müßten Sie die gewünschte Methode, falls sie nicht von Pegasus
überschrieben
wurde, durch entsprechende Qualifizierung disambiguieren.
Die Lösung dazu finden Sie in Zeile 76. Dort überschreibt das Pegasus
-Objekt die Methode
GetAge()
, damit sie lediglich als Kettenglied fungiert - das heißt, die gleichnamige
Methode in der Basisklasse aufruft.
Das geschieht aus zwei Gründen: Zum einen soll, wie in diesem Falle, geklärt werden, welche Basisklasse aufzurufen ist; zum anderen, um eine Arbeit auszuführen und dann die Arbeit von der Funktion in der Basisklasse weiterführen zu lassen. In manchen Fällen ist es besser, erst die Arbeit zu machen und dann zu verketten; es ist aber auch möglich, erst zu verketten und die Arbeit zu machen, wenn die Funktion der Basisklasse zurückkehrt.
Der Pegasus
-Konstruktor übernimmt fünf Parameter: die Farbe der Kreatur, seine
Größe (in HANDS), seine Zugbereitschaft, wie viele daran glauben und das Alter. Der
Konstruktor initialisiert den Horse
-Teil von Pegasus
mit der Farbe, der Größe und dem
Alter (Zeile 87). Er initialisiert den Bird
-Teil mit Farbe, Zugbereitschaft und Alter in
Zeile 88. Zeile 89 initialisiert zum Schluß itsNumberBelievers
.
Der Aufruf des Horse
-Konstruktors in Zeile 87 führt den Code aus Zeile 38 aus. Der
Horse
-Konstruktor initialisiert mit dem age
-Parameter den Animal
-Teil vom Horse
-Teil
des Pegasus
. Daraufhin initialisiert er die beiden Elementvariablen von Horse
- itsColor
und itsHeight
.
Der Aufruf des Bird
-Konstruktors in Zeile 88 führt den Code aus Zeile 60 aus. Auch
hier wird mit dem age
-Parameter der Animal
-Teil von Bird
initialisiert.
Beachten Sie, daß der color
-Parameter von Pegasus
dazu dient, die Elementvariablen
in Horse
und Bird
zu initialisieren. Beachten Sie ebenfalls, daß mit age
die Elementvariable
itsAge
in der Animal
-Basisklasse von Horse
und der Animal
-Basisklasse von Bird
initialisiert wird.
Im Listing 13.5 betrieb die Pegasus
-Klasse einigen Aufwand, um die Mehrdeutigkeiten
in Hinblick auf die aufzurufenden Animal
-Basisklassen aufzulösen. Meistens jedoch ist
die Entscheidung, welche verwendet werden sollte, ziemlich egal - denn schließlich
haben Horse
und Bird
die gleiche Basisklasse.
Es ist möglich, in C++ anzugeben, daß Sie keine zwei Kopien einer gemeinsamen Basisklasse (wie in Abbildung 13.2) wünschen, sondern lieber eine einzige gemeinsame Basisklasse hätten (Abbildung 13.3).
Um dies zu erreichen, müssen Sie Animal
zu einer virtuellen Basisklasse von Horse
und
Bird
machen. Die Animal
-Klasse ändert sich dadurch nicht. Für die Klassen Horse
und
Bird
ändert sich nur die Deklaration, die um das Schlüsselwort virtual
erweitert wird.
Pegasus
hingegen, ist wesentlich von dieser Änderung betroffen.
Normalerweise initialisiert der Konstruktor einer Klasse nur seine eigenen Variablen
und seine direkte Basisklasse. Virtuell geerbte Basisklassen stellen jedoch eine Ausnahme
dar. Sie werden von der Klasse initialisiert, die in der Vererbungskette ganz
hinten steht. Demnach wird Animal
nicht von Horse
oder Bird
, sondern von Pegasus
initialisiert. Horse
und Bird
müssen Animal
zwar ebenfalls in ihren Konstruktoren initialisieren,
doch werden diese Initialisierungen ignoriert, wenn ein Pegasus
-Objekt erzeugt
wird.
Abbildung 13.3: Diamantförmige Vererbung
Listing 13.6 ist eine Neufassung von Listing 13.5, die die Vorteile der virtuellen Ableitung nutzt.
Listing 13.6: Einsatz der virtuellen Vererbung
1: // Listing 13.6
2: // Virtuelle Vererbung
3: #include <iostream.h>
4:
5: typedef int HANDS;
6: enum COLOR { Red, Green, Blue, Yellow, White, Black, Brown } ;
7:
8: class Animal // gemeinsame Basis fuer horse und bird
9: {
10: public:
11: Animal(int);
12: virtual ~Animal() { cout << "Animal-Destruktor...\n"; }
13: virtual int GetAge() const { return itsAge; }
14: virtual void SetAge(int age) { itsAge = age; }
15: private:
16: int itsAge;
17: };
18:
19: Animal::Animal(int age):
20: itsAge(age)
21: {
22: cout << "Animal-Konstruktor...\n";
23: }
24:
25: class Horse : virtual public Animal
26: {
27: public:
28: Horse(COLOR color, HANDS height, int age);
29: virtual ~Horse() { cout << "Horse-Destruktor...\n"; }
30: virtual void Whinny()const { cout << "Wieher!... "; }
31: virtual HANDS GetHeight() const { return itsHeight; }
32: virtual COLOR GetColor() const { return itsColor; }
33: protected:
34: HANDS itsHeight;
35: COLOR itsColor;
36: };
37:
38: Horse::Horse(COLOR color, HANDS height, int age):
39: Animal(age),
40: itsColor(color),itsHeight(height)
41: {
42: cout << "Horse-Konstruktor...\n";
43: }
44:
45: class Bird : virtual public Animal
46: {
47: public:
48: Bird(COLOR color, bool migrates, int age);
49: virtual ~Bird() {cout << "Bird-Destruktor...\n"; }
50: virtual void Chirp()const { cout << "Chirp... "; }
51: virtual void Fly()const
52: { cout << "Ich kann fliegen! Ich kann fliegen! "; }
53: virtual COLOR GetColor()const { return itsColor; }
54: virtual bool GetMigration() const { return itsMigration; }
55: protected:
56: COLOR itsColor;
57: bool itsMigration;
58: };
59:
60: Bird::Bird(COLOR color, bool migrates, int age):
61: Animal(age),
62: itsColor(color), itsMigration(migrates)
63: {
64: cout << "Bird-Konstruktor...\n";
65: }
66:
67: class Pegasus : public Horse, public Bird
68: {
69: public:
70: void Chirp()const { Whinny(); }
71: Pegasus(COLOR, HANDS, bool, long, int);
72: virtual ~Pegasus() {cout << "Pegasus-Destruktor...\n";}
73: virtual long GetNumberBelievers() const
74: { return itsNumberBelievers; }
75: virtual COLOR GetColor()const { return Horse::itsColor; }
76: private:
77: long itsNumberBelievers;
78: };
79:
80: Pegasus::Pegasus(
81: COLOR aColor,
82: HANDS height,
83: bool migrates,
84: long NumBelieve,
85: int age):
86: Horse(aColor, height,age),
87: Bird(aColor, migrates,age),
88: Animal(age*2),
89: itsNumberBelievers(NumBelieve)
90: {
91: cout << "Pegasus-Konstruktor...\n";
92: }
93:
94: int main()
95: {
96: Pegasus *pPeg = new Pegasus(Red, 5, true, 10, 2);
97: int age = pPeg->GetAge();
98: cout << "Dieser Pegasus ist " << age << " Jahre alt.\n";
99: delete pPeg;
100: return 0;
101: }
Animal-Konstruktor...
Horse-Konstruktor...
Bird-Konstruktor...
Pegasus-Konstruktor...
Dieser Pegasus ist 4 Jahre alt.
Pegasus-Destruktor...
Bird-Destruktor...
Horse-Destruktor...
Animal-Destruktor...
In Zeile 25 deklariert die Horse
-Klasse, daß sie nur virtuell von Animal
erben will, und
in Zeile 45 macht Bird
die gleiche Deklaration. Beachten Sie, daß die Konstruktoren
von Bird
und Animal
weiterhin das Animal
-Objekt initialisieren.
Pegasus
erbt von Bird
, von Horse
und - als letztem Objekt in der Ableitungshierarchie
- von Animal
. Zur Initialisierung des Animal
-Objekts wird die Initialisierung von Pegasus
verwendet und die Aufrufe des Animal
-Konstruktors in Bird
und Horse
werden ignoriert.
Sie erkennen dies daran, daß für das Alter der Wert 2
übergeben wird, den Horse
und Bird
unverändert an Animal
weitergeben, während Pegasus
den Wert verdoppelt.
Das Ergebnis 4
zeigt sich in der Ausgabe von Zeile 98.
Pegasus
muß den Aufruf von GetAge()
nicht länger eindeutig auflösen und kann diese
Funktion einfach von Animal
zu erben. Beachten Sie, daß Pegasus
immer noch die
Mehrdeutigkeiten beim Aufruf von GetColor()
lösen muß, da diese Funktion in beiden
Basisklassen, jedoch nicht in Animal
vorkommt.
Klassen mit virtueller Vererbung deklarieren
Um sicherzustellen, daß abgeleitete Klassen nur eine Instanz gemeinsamer Basisklassen erhalten, deklarieren Sie für die dazwischenliegenden Klassen die Vererbung der Basisklasse als virtuell.
class Horse : virtual public Animal
class Bird : virtual public Animal
class Pegasus : public Horse, public Birdclass Schnauzer : virtual public Dog
class Pudel : virtual public Dog
class Schnudel : public Schnauzer, public Pudel
Trotzdem die Mehrfachvererbung im Vergleich zur Einfachvererbung etliche Vorteile bietet, zögern viele C++-Programmierer, sie einzusetzen. Als Gründe führen Sie an, daß viele Compiler die Mehrfachvererbung noch nicht unterstützen, daß das Debuggen schwieriger wird und daß fast alles, was mit Mehrfachvererbung gelöst werden kann, auch ohne geht.
Diese Bedenken sind begründet, und Sie sollten sich davor hüten, Ihre Programme unnötig komplex zu machen. Einige Debugger haben große Schwierigkeiten mit Mehrfachvererbung und einige Designs werden dadurch unnötig kompliziert.
Wer einen Mittelweg zwischen Mehrfach- und Einfachvererbung einschlagen möchte,
kann die sogenannten Mixins verwenden. Angenommen Sie hätten eine Horse
-Klasse,
die sich von Animal
und von Displayable
ableitet. Displayable
würde nur wenige
Methoden zur Verfügung stellen, mit deren Hilfe man beliebige Objekte auf dem Bildschirm
ausgeben kann.
Eine Mixin-Klasse ist eine Klasse, die zwar Funktionalität, aber möglichst wenig oder sogar gar keine Daten bereitstellt.
Der Begriff Mixin hat seinen Ursprung in einer Eisdiele in Sommersville, Massachusetts, wo er für eine neue Eissorte verwendet wurde, die Süßigkeiten und Kekse unter die Haupteissorten mischte. Einige Programmierern, die beruflich mit der objektorientierten Programmiersprache SCOOPS arbeiteten und gerade dort ihre Sommerferien verbrachten, erschien dieser Begriff als eine geeignete Metapher.
Mixin-Klassen werden wie andere Klassen auch durch öffentliche Vererbung in die abgeleitete Klasse »aufgenommen«. Der einzige Unterschied zu normalen Klassen besteht darin, daß Mixin-Klassen kaum oder keine Daten enthalten. Zugegeben, dies ist eine willkürliche Unterscheidung, die letztlich nur der Tatsache Ausdruck verleiht, daß man manchmal eben nur bestimmte Fähigkeiten in die Klasse aufnehmen möchte, ohne die Arbeit mit der abgeleiteten Klasse dadurch unnötig zu komplizieren.
Einige Debugger haben weniger Schwierigkeiten damit, Mixins zu debuggen als komplizierte Objekte der Mehrfachvererbung. Außerdem besteht eine geringere Gefahr der Mehrdeutigkeit beim Zugriff auf die Daten in der anderen Haupt-Basisklasse.
Wenn zum Beispiel Horse
von Animal
und von Displayable
abgeleitet ist, wäre Displayable
die Basisklasse ohne Daten und Animal
wäre so wie immer. Demnach wären
alle Daten in Horse
direkt von Animal
abgeleitet, die Funktionen jedoch von beiden.
Häufig bildet man gleich in sich abgeschlossene Hierarchien von Klassen. Beispielsweise
erzeugt man eine Klasse Shape
(Form) und leitet davon Rectangle
(Rechteck)
und Circle
(Kreis) ab. Als Spezialfall des Rechtecks läßt sich von Rectangle
die Klasse
Square
(Quadrat) ableiten.
Alle abgeleiteten Klassen überschreiben die Methoden Draw()
(Zeichnen), GetArea()
(Fläche ermitteln) und andere. Listing 13.7 zeigt das Grundgerüst der Implementierung
einer Shape
-Klasse und die davon abgeleiteten Klassen Circle
und Rectangle
.
1: // Listing 13.7. Shape-Klassen
2:
3: #include <iostream.h>
4:
5:
6: class Shape
7: {
8: public:
9: Shape(){}
10: virtual ~Shape(){}
11: virtual long GetArea() { return -1; }
12: virtual long GetPerim() { return -1; }
13: virtual void Draw() {}
14: private:
15: };
16:
17: class Circle : public Shape
18: {
19: public:
20: Circle(int radius):itsRadius(radius){}
21: ~Circle(){}
22: long GetArea() { return 3 * itsRadius * itsRadius; }
23: long GetPerim() { return 6 * itsRadius; }
24: void Draw();
25: private:
26: int itsRadius;
27: int itsCircumference;
28: };
29:
30: void Circle::Draw()
31: {
32: cout << "Routine zum Zeichnen eines Kreises!\n";
33: }
34:
35:
36: class Rectangle : public Shape
37: {
38: public:
39: Rectangle(int len, int width):
40: itsLength(len), itsWidth(width){}
41: virtual ~Rectangle(){}
42: virtual long GetArea() { return itsLength * itsWidth; }
43: virtual long GetPerim() {return 2*itsLength + 2*itsWidth; }
44: virtual int GetLength() { return itsLength; }
45: virtual int GetWidth() { return itsWidth; }
46: virtual void Draw();
47: private:
48: int itsWidth;
49: int itsLength;
50: };
51:
52: void Rectangle::Draw()
53: {
54: for (int i = 0; i<itsLength; i++)
55: {
56: for (int j = 0; j<itsWidth; j++)
57: cout << "x ";
58:
59: cout << "\n";
60: }
61: }
62:
63: class Square : public Rectangle
64: {
65: public:
66: Square(int len);
67: Square(int len, int width);
68: ~Square(){}
69: long GetPerim() {return 4 * GetLength();}
70: };
71:
72: Square::Square(int len):
73: Rectangle(len,len)
74: {}
75:
76: Square::Square(int len, int width):
77: Rectangle(len,width)
78:
79: {
80: if (GetLength() != GetWidth())
81: cout << "Fehler, kein Quadrat... ein Rechteck?\n";
82: }
83:
84: int main()
85: {
86: int choice;
87: bool fQuit = false;
88: Shape * sp;
89:
90: while ( ! fQuit )
91: {
92: cout << "(1)Kreis (2)Rechteck (3)Quadrat (0)Beenden: ";
93: cin >> choice;
94:
95: switch (choice)
96: {
97: case 0: fQuit = true;
98: break;
99: case 1: sp = new Circle(5);
100: break;
101: case 2: sp = new Rectangle(4,6);
102: break;
103: case 3: sp = new Square(5);
104: break;
105: default: cout << "Zahl zwischen 0 und 3 eingeben" << endl;
106: continue;
107: break;
108: }
109: if(! fQuit)
110: sp->Draw();
111: delete sp;
112: cout << "\n";
113: }
114: return 0;
115: }
(1)Kreis (2)Rechteck (3)Quadrat (0)Beenden: 2
x x x x x x
x x x x x x
x x x x x x
x x x x x x
(1)Kreis (2)Rechteck (3)Quadrat (0)Beenden: 3
x x x x x
x x x x x
x x x x x
x x x x x
x x x x x
(1)Kreis (2)Rechteck (3)Quadrat (0)Beenden: 0
Die Zeilen 6 bis 15 deklarieren die Klasse Shape
. Die Methoden GetArea()
und GetPerim()
liefern einen Fehlerwert zurück, Draw()
führt keine Aktionen aus. Eine unbestimmte
Form kann man schließlich auch schlecht zeichnen? Nur bestimmte Formen
(Kreise, Rechtecke usw.) lassen sich zeichnen, Formen als Abstraktion sind nicht darstellbar.
Circle
leitet sich von Shape
ab und überschreibt die drei virtuellen Methoden. Beachten
Sie, daß es keinen Grund gibt, das Schlüsselwort virtual
hinzuzufügen, da es Bestandteil
der Vererbung virtueller Methoden ist. Allerdings schadet es auch nichts,
wenn man es trotzdem angibt, wie es in der Klasse Rectangle
auf den Zeilen 42, 43
und 46 zu sehen ist. Es empfiehlt sich, den Begriff virtual
als Erinnerung oder eine
Form der Dokumentation aufzunehmen.
Square
leitet sich von Rectangle
ab, überschreibt ebenfalls die Methode GetPerim()
und erbt die restlichen Methoden, die in Rectangle
definiert sind.
Da es Probleme gibt, wenn ein Klient ein Shape
-Objekt instantiiert, wäre es wünschenswert,
dieses zu verhindern. Die Klasse Shape
existiert nur, um eine Schnittstelle
für die davon abgeleiteten Klassen bereitzustellen. Als solches handelt es sich um einen
abstrakten Datentyp (ADT).
Ein abstrakter Datentyp repräsentiert ein Konzept (wie Shape
) und nicht ein Objekt
(wie Circle
). In C++ ist ein ADT immer die Basisklasse für andere Klassen, und es ist
nicht erlaubt, Instanzen eines ADT zu erzeugen.
Abstrakte oder »rein virtuelle« Funktionen lassen sich in C++ erzeugen, indem man
die Funktion als virtual
deklariert und mit 0
initialisiert:
virtual void Draw() = 0;
Jede Klasse mit einer oder mehreren abstrakten Funktionen ist ein abstrakter Datentyp. Es ist verboten, ein Objekt von einer als ADT fungierenden Klasse zu instantiieren. Der Versuch führt bereits zu einem Compiler-Fehler. Bringt man eine abstrakte Funktion in einer Klasse unter, signalisiert man den Klienten der Klasse zwei Dinge:
Von einem ADT abgeleitete Klassen erben die abstrakten Funktionen in ihrer »reinen«
Form. Aus diesem Grund muß man die geerbten abstrakten Funktionen überschreiben,
wenn man Objekte der abgeleiteten Klasse instantiieren möchte. Wenn also Rectangle
von Shape
erbt und Shape
über drei abstrakte Funktionen verfügt, muß Rectangle
alle drei abstrakte Funktionen überschreiben - oder Rectangle
ist ebenfalls ein
ADT. In Listing 13.8 liegt die Klasse Shape
in einer neuen Fassung als abstrakter Datentyp
vor. Um Platz zu sparen, sei auf die Wiederholung des restlichen Teils von Listing
13.7 verzichtet. Ersetzen Sie bitte die Deklaration von Shape
in den Zeilen 6 bis
16 aus Listing 13.7 durch die Deklaration von Shape
in Listing 13.8, und starten Sie
das Programm erneut.
Listing 13.8: Abstrakte Datentypen
1: class Shape
2: {
3: public:
4: Shape(){}
5: ~Shape(){}
6: virtual long GetArea() = 0;
7: virtual long GetPerim()= 0;
8: virtual void Draw() = 0;
9: private:
10: };
(1)Kreis (2)Rechteck (3)Quadrat (0)Beenden: 2
x x x x x x
x x x x x x
x x x x x x
x x x x x x
(1)Kreis (2)Rechteck (3)Quadrat (0)Beenden: 3
x x x x x
x x x x x
x x x x x
x x x x x
x x x x x
(1)Kreis (2)Rechteck (3)Quadrat (0)Beenden: 0
Wie Sie feststellen, hat sich die Arbeitsweise des Programms überhaupt nicht geändert.
Der einzige Unterschied ist, daß man nun keine Objekte der Klasse Shape
erzeugen
kann.
Eine Klasse deklariert man als abstrakten Datentyp, indem man eine oder mehrere abstrakte Funktionen in die Klassendeklaration aufnimmt. Deklarieren Sie eine abstrakte Funktion, indem Sie
= 0
hinter die Funktionsdeklaration schreiben.class Shape
{
virtual void Draw() = 0; // abstrakt
};
Normalerweise werden abstrakte Funktionen in einer abstrakten Basisklasse überhaupt nicht implementiert. Da niemals Objekte dieses Typs erzeugt werden, gibt es keinen Anlaß, Implementierungen bereitzustellen. Der ADT arbeitet damit ausschließlich als Definition einer Schnittstelle für Objekte, die sich von ihm ableiten.
Es ist allerdings möglich, eine Implementierung für eine abstrakte Funktion anzugeben.
Die Funktion läßt sich dann von Objekten aufrufen, die vom ADT abgeleitet sind,
etwa um allen überschriebenen Funktionen eine gemeinsame Funktionalität zu verleihen.
Listing 13.9 bringt eine Neuauflage von Listing 13.7 in der Shape
als ADT realisiert
und mit einer Implementierung für die abstrakte Funktion Draw()
ausgestattet ist.
Die Klasse Circle
überschreibt Draw()
wie erforderlich, greift dann aber auch auf die
Funktionalität aus der Basisklasse zurück.
In diesem Beispiel besteht die zusätzliche Funktionalität einfach in einer weiteren Ausgabe auf den Bildschirm. Die Basisklasse könnte aber auch einen gemeinsam genutzten Zeichenmechanismus bereitstellen, der zum Beispiel ein Fenster einrichtet, mit dem alle abgeleiteten Klassen arbeiten.
Listing 13.9: Abstrakte Funktionen implementieren
1: // Abstrakte Funktionen implementieren
2:
3: #include <iostream.h>
4:
5: class Shape
6: {
7: public:
8: Shape(){}
9: virtual ~Shape(){}
10: virtual long GetArea() = 0;
11: virtual long GetPerim()= 0;
12: virtual void Draw() = 0;
13: private:
14: };
15:
16: void Shape::Draw()
17: {
18: cout << "Abstrakter Zeichenmechanismus!\n";
19: }
20:
21: class Circle : public Shape
22: {
23: public:
24: Circle(int radius):itsRadius(radius){}
25: virtual ~Circle(){}
26: long GetArea() { return 3 * itsRadius * itsRadius; }
27: long GetPerim() { return 9 * itsRadius; }
28: void Draw();
29: private:
30: int itsRadius;
31: int itsCircumference;
32: };
33:
34: void Circle::Draw()
35: {
36: cout << "Zeichenroutine fuer Kreis!\n";
37: Shape::Draw();
38: }
39:
40:
41: class Rectangle : public Shape
42: {
43: public:
44: Rectangle(int len, int width):
45: itsLength(len), itsWidth(width){}
46: virtual ~Rectangle(){}
47: long GetArea() { return itsLength * itsWidth; }
48: long GetPerim() {return 2*itsLength + 2*itsWidth; }
49: virtual int GetLength() { return itsLength; }
50: virtual int GetWidth() { return itsWidth; }
51: void Draw();
52: private:
53: int itsWidth;
54: int itsLength;
55: };
56:
57: void Rectangle::Draw()
58: {
59: for (int i = 0; i<itsLength; i++)
60: {
61: for (int j = 0; j<itsWidth; j++)
62: cout << "x ";
63:
64: cout << "\n";
65: }
66: Shape::Draw();
67: }
68:
69:
70: class Square : public Rectangle
71: {
72: public:
73: Square(int len);
74: Square(int len, int width);
75: virtual ~Square(){}
76: long GetPerim() {return 4 * GetLength();}
77: };
78:
79: Square::Square(int len):
80: Rectangle(len,len)
81: {}
82:
83: Square::Square(int len, int width):
84: Rectangle(len,width)
85:
86: {
87: if (GetLength() != GetWidth())
88: cout << "Fehler, kein Quadrat... ein Rechteck?\n";
89: }
90:
91: int main()
92: {
93: int choice;
94: bool fQuit = false;
95: Shape * sp;
96:
97: while (1)
98: {
99: cout << "(1)Kreis (2)Rechteck (3)Quadrat (0)Beenden: ";
100: cin >> choice;
101:
102: switch (choice)
103: {
104: case 1: sp = new Circle(5);
105: break;
106: case 2: sp = new Rectangle(4,6);
107: break;
108: case 3: sp = new Square (5);
109: break;
110: default: fQuit = true;
111: break;
112: }
113: if (fQuit)
114: break;
115:
116: sp->Draw();
117: delete sp;
118: cout << "\n";
119: }
120: return 0;
121: }
(1)Kreis (2)Rechteck (3)Quadrat (0)Beenden: 2
x x x x x x
x x x x x x
x x x x x x
x x x x x x
Abstrakter Zeichenmechanismus!
(1)Kreis (2)Rechteck (3)Quadrat (0)Beenden: 3
x x x x x
x x x x x
x x x x x
x x x x x
x x x x x
Abstrakter Zeichenmechanismus!
(1)Kreis (2)Rechteck (3)Quadrat (0)Beenden: 0
Die Zeilen 5 bis 14 deklarieren den abstrakten Datentyp Shape
. Hier sind alle drei Zugriffsmethoden
als abstrakt deklariert - ich möchte noch einmal darauf hinweisen, daß
es auch genügt hätte, nur eine Methode als abstrakt zu deklarieren. Wenn irgendeine
Methode als abstrakt deklariert ist, wird die gesamte Klasse zu einem ADT.
Die Methoden GetArea()
und GetPerim()
sind nicht implementiert, Draw()
hat dagegen
eine Implementierung. Circle
und Rectangle
überschreiben Draw()
und rufen die
Basismethode auf, um die - beiden zur Verfügung stehende - Funktionalität der Basisklasse
zu nutzen.
Manchmal leitet man ADTs von anderen ADTs ab, um zum Beispiel einige der abgeleiteten abstrakten Funktionen zu implementieren und andere abstrakt zu belassen.
So könnte man eine Klasse Animal
erzeugen, die Eat()
(Essen), Sleep()
(Schlafen),
Move()
(Bewegen) und Reproduce()
(Fortpflanzen) als abstrakte Funktionen deklarieren.
Von Animal
leitete man vielleicht Mammal
(Säugetier) und Fish
(Fisch) ab.
Da sich die Säugetiere in der gleichen Weise fortpflanzen, implementieren Sie Mammal::Reproduce()
, belassen aber Eat()
, Sleep()
und Move()
als abstrakte Funktionen.
Von Mammal
leiten Sie Dog
ab, und Dog
muß die drei restlichen abstrakten Funktionen
überschreiben und implementieren, damit sich Objekte vom Typ Dog
erzeugen lassen.
Als Klassendesigner haben Sie damit gesagt, daß sich keine Animal
s oder Mammal
s instantiieren
lassen, aber daß alle Mammal
s die bereitgestellte Methode Reproduce()
erben
können, ohne sie überschreiben zu müssen.
Listing 13.10 verdeutlicht diese Technik anhand einer skizzenhaften Implementierung der oben aufgeführten Klassen.
Listing 13.10: ADTs von anderen ADTs ableiten
1: // Listing 13.10
2: // ADTs von anderen ADTs ableiten
3: #include <iostream.h>
4:
5: enum COLOR { Red, Green, Blue, Yellow, White, Black, Brown } ;
6:
7: class Animal // Gemeinsame Basisklasse sowohl für Horse
// als auch Fish
8: {
9: public:
10: Animal(int);
11: virtual ~Animal() { cout << "Animal-Destruktor...\n"; }
12: virtual int GetAge() const { return itsAge; }
13: virtual void SetAge(int age) { itsAge = age; }
14: virtual void Sleep() const = 0;
15: virtual void Eat() const = 0;
16: virtual void Reproduce() const = 0;
17: virtual void Move() const = 0;
18: virtual void Speak() const = 0;
19: private:
20: int itsAge;
21: };
22:
23: Animal::Animal(int age):
24: itsAge(age)
25: {
26: cout << "Animal-Konstruktor...\n";
27: }
28:
29: class Mammal : public Animal
30: {
31: public:
32: Mammal(int age):Animal(age)
33: { cout << "Mammal-Konstruktor...\n";}
34: virtual ~Mammal() { cout << "Mammal-Destruktor...\n";}
35: virtual void Reproduce() const
36: { cout << "Mammal-Fortpflanzung...\n"; }
37: };
38:
39: class Fish : public Animal
40: {
41: public:
42: Fish(int age):Animal(age)
43: { cout << "Fish-Konstruktor...\n";}
44: virtual ~Fish() {cout << "Fish-Destruktor...\n"; }
45: virtual void Sleep() const { cout << "Fisch schlummert...\n"; }
46: virtual void Eat() const { cout << "Fisch fuettern...\n"; }
47: virtual void Reproduce() const
48: { cout << "Fisch legt Eier...\n"; }
49: virtual void Move() const
50: { cout << "Fisch schwimmt...\n"; }
51: virtual void Speak() const { }
52: };
53:
54: class Horse : public Mammal
55: {
56: public:
57: Horse(int age, COLOR color ):
58: Mammal(age), itsColor(color)
59: { cout << "Horse-Konstruktor...\n"; }
60: virtual ~Horse() { cout << "Horse-Destruktor...\n"; }
61: virtual void Speak()const { cout << "Wieher!... \n"; }
62: virtual COLOR GetItsColor() const { return itsColor; }
63: virtual void Sleep() const
64: { cout << "Pferd schlaeft...\n"; }
65: virtual void Eat() const { cout << "Pferd fuettern...\n"; }
66: virtual void Move() const { cout << "Pferd laeuft...\n";}
67:
68: protected:
69: COLOR itsColor;
70: };
71:
72: class Dog : public Mammal
73: {
74: public:
75: Dog(int age, COLOR color ):
76: Mammal(age), itsColor(color)
77: { cout << "Dog-Konstruktor...\n"; }
78: virtual ~Dog() { cout << "Dog-Destruktor...\n"; }
79: virtual void Speak()const { cout << "Wuff!... \n"; }
80: virtual void Sleep() const { cout << "Hund schlaeft...\n"; }
81: virtual void Eat() const { cout << "Hund frisst...\n"; }
82: virtual void Move() const { cout << "Hund laeuft...\n"; }
83: virtual void Reproduce() const
84: { cout << "Hunde pflanzen sich fort...\n"; }
85:
86: protected:
87: COLOR itsColor;
88: };
89:
90: int main()
91: {
92: Animal *pAnimal=0;
93: int choice;
94: bool fQuit = false;
95:
96: while (1)
97: {
98: cout << "(1)Hund (2)Pferd (3)Fisch (0)Beenden: ";
99: cin >> choice;
100:
101: switch (choice)
102: {
103: case 1: pAnimal = new Dog(5,Brown);
104: break;
105: case 2: pAnimal = new Horse(4,Black);
106: break;
107: case 3: pAnimal = new Fish (5);
108: break;
109: default: fQuit = true;
110: break;
111: }
112: if (fQuit)
113: break;
114:
115: pAnimal->Speak();
116: pAnimal->Eat();
117: pAnimal->Reproduce();
118: pAnimal->Move();
119: pAnimal->Sleep();
120: delete pAnimal;
121: cout << "\n";
122: }
123: return 0;
124: }
(1)Hund (2)Pferd (3)Fisch (0)Beenden: 1
Animal-Konstruktor...
Mammal-Konstruktor...
Dog-Konstruktor...
Wuff!...
Hund frisst...
Hunde pflanzen sich fort....
Hund laeuft...
Hund schlaeft...
Dog-Destruktor...
Mammal-Destruktor...
Animal-Destruktor...
(1)Hund (2)Pferd (3)Fisch (0)Beenden: 0
Die Zeilen 7 bis 21 deklarieren den abstrakten Datentyp Animal
. Die Zugriffsfunktionen
für itsAge
sind nicht abstrakt. Alle Animal
-Objekte nutzen diese Funktionen. Weiterhin
gibt es die fünf abstrakten Funktionen Sleep()
, Eat()
, Reproduce()
, Move()
und
Speak()
.
Die Deklaration der von Animal
abgeleiteten Klasse Mammal
folgt in den Zeilen 29 bis
37. Die Klasse fügt keine neuen Daten hinzu. Allerdings überschreibt Mammal
die Funktion
Reproduce()
, um eine allgemeine Form der Fortpflanzung für alle Säugetiere bereitzustellen.
Fish
muß Reproduce()
überschreiben, da sich Fish
direkt von Animal
ableitet
und nicht die Fortpflanzung von Säugetieren nutzen kann. (Logisch, oder?)
Mammal
-Klassen brauchen nun nicht mehr die Funktion Reproduce()
zu überschreiben,
können es aber bei Bedarf tun, wie zum Beispiel Dog
in Zeile 83. Fish
, Horse
und Dog
überschreiben alle geerbten abstrakten Funktionen, so daß sich Objekte dieser Typen
instantiieren lassen.
Im Rumpf des Programms wird ein Animal
-Zeiger verwendet, um auf die verschiedenen
abgeleiteten Objekte der Reihe nach zuzugreifen. Beim Aufruf der virtuellen Methoden
wird gemäß der Laufzeitbindung des Zeigers stets die korrekte Methode der
abgeleiteten Klasse aufgerufen.
Versucht man, ein Animal
oder ein Mammal
zu instantiieren, erhält man einen Compiler-Fehler,
da beides abstrakte Datentypen sind.
In einem Programm ist die Klasse Animal
abstrakt, in einem anderen nicht. Wann soll
man eine Klasse zu einer abstrakten Klasse machen?
Diese Frage läßt sich nicht global beantworten. Es hängt immer davon ab, was für das
jeweilige Programm sinnvoll ist. In einem Programm für einen Bauernhof oder einen
Zoo deklariert man Animal
zum Beispiel als abstrakten Datentyp und Dog
als Klasse,
von der man Objekte instantiieren kann.
Wenn man andererseits einen Hundezwinger erstellt, kann man Dog
als abstrakten Datentyp
deklarieren und nur Hunderassen instantiieren: Retriever, Terrier usw. Die Abstraktionsebene
ist davon abhängig, wie fein man die Typen unterscheiden muß.
Ein besonders heißer Trend in C++ ist derzeit die Erstellung und Verbreitung von Entwurfsmustern. Dabei handelt es sich um gut dokumentierte Lösungen zu allgemeinen Problemen von C++-Programmierern. So löst zum Beispiel das Überwachungsmuster ein allgemeines Problem bei der Vererbung.
Angenommen Sie entwickeln eine Zeitgeber
-Klasse, die die verstrichenen Sekunden
zählen kann. So eine Klasse könnte über ein Integer-Klassenelement Sekunden
und
Methoden zum Setzen, Auslesen und Inkrementieren von Sekunden
verfügen.
Lassen Sie uns weiterhin annehmen, Ihr Programm möchte jedes Mal darüber informiert
werden, wenn das Element Sekunden
des Zeitgebers inkrementiert wurde. Eine
naheliegende Lösung wäre, eine Benachrichtigungsmethode in die Zeitgeber
-Klasse
aufzunehmen. Benachrichtigung ist jedoch kein essentieller Bestandteil der Zeitmessung
und der komplexe Code zur Registrierung der Klassen, die informiert werden
müssen, wenn die Uhr inkrementiert wird, gehört eigentlich nicht in Ihre Zeitgeber
-
Klasse.
Noch wichtiger: Wenn Sie einmal die Logik für die Registrierung und Benachrichtigung all der Klassen, die an diesen Änderungen interessiert sind, ausgearbeitet haben, möchten Sie sicherlich diese Funktionalität in einer eigenen abstrakten Klasse zusammenfassen. Diese könnten Sie dann bei anderen Klassen, die auf die gleiche Weise »observiert« werden sollen, wiederverwenden.
Deshalb ist es die bessere Lösung, eine eigene Beobachter
-Klasse zu erzeugen. Machen
Sie diese Klasse zu einem abstrakten Datentyp mit der abstrakten Funktion Aktualisieren()
.
Erzeugen Sie dann einen zweiten abstrakten Datentyp namens Subjekt
. Subjekt
hält
einen Array von Beobachter
-Objekten und stellt zwei Methoden bereit: Registrieren()
(mit dem Beobachter
-Objekte der Liste hinzugefügt werden) und Benachrichtigen()
, die aufgerufen wird, wenn es etwas zu berichten gibt.
Die Klassen, die von den Änderungen Ihres Zeitgebers informiert werden wollen, werden
von Beobachter
abgeleitet. Zeitgeber
selbst wird von Subjekt
abgeleitet. Die Beobachter
-Klasse registriert sich in der Subjekt
-Klasse. Die Subjekt
-Klasse ruft Benachrichtigen()
auf, wenn sie sich ändert (in diesem Falle, wenn der Zeitgeber
aktualisiert wird).
Abschließend stellen wir fest, daß nicht jeder Klient von Zeitgeber
observiert werden
möchte. Deshalb erzeugen wir eine neue Klasse namens BeobachteterZeitgeber
, die
von Zeitgeber
und von Subjekt
abgeleitet ist. Damit erhält BeobachteterZeitgeber
die
Merkmale von Zeitgeber
und die Fähigkeit, beobachtet zu werden.
Viele C++-Programmierer wissen, daß Java zu einem großen Teil auf C++ basiert, und trotzdem haben die Java-Entwickler bewußt auf Mehrfachvererbung verzichtet. Sie waren der Meinung, daß Mehrfachvererbung Java unnötig verkompliziert und damit der leichten Anwendbarkeit dieser Programmiersprache entgegenwirkt. Sie glauben, daß 90 % der Funktionalität der Mehrfachvererbung mit der Verwendung von sogenannten Schnittstellen abgedeckt werden kann.
Eine Schnittstelle ähnelt sehr stark einem abstrakten Datentyp. Sie definiert einen Satz an Funktionen, die nur in einer abgeleiteten Klasse implementiert werden können. Bei Schnittstellen leitet man jedoch nicht direkt von der Schnittstelle, sondern von einer anderen Klasse ab und implementiert die Schnittstelle, fast vergleichbar der Mehrfachvererbung. Diese Kombination aus abstraktem Datentyp und Mehrfachvererbung erfüllt in etwa die Aufgabe einer Mixin-Klasse, ohne die Komplexität oder den Overhead der Mehrfachvererbung. Außerdem besteht kein Bedarf mehr an virtueller Vererbung, da Schnittstellen weder Implementierungen noch Elementdaten aufweisen.
Ob dies ein Manko oder ein Merkmal ist, liegt am Betrachter. Wenn Sie jedoch Mehrfachvererbung und abstrakte Datentypen in C++ verstehen, haben Sie eine gute Ausgangsbasis, um die etwas fortschrittlicheren Merkmale von Java einzusetzen, sollten Sie eines Tages den Entschluß fassen, diese Sprache auch zu lernen.
Heute haben Sie gelernt, wie man einige der Beschränkungen der einfachen Vererbung umgeht. Sie wissen jetzt, wo die Gefahren liegen, wenn Sie Schnittstellen in der Vererbungshierarchie hochreichen, und welche Risiken es birgt, abwärts gerichtete Typumwandlungen vorzunehmen. Sie haben gelernt, wie man Mehrfachvererbung einsetzt, welche Probleme sich in diesem Zusammenhang stellen und wie man sie mit virtueller Vererbung löst.
Sie haben gesehen, wie man abstrakte Datentypen mit Hilfe von abstrakten Funktionen erzeugt, und wie, wann und warum man abstrakte Funktionen implementiert. Abschließend wurde Ihnen gezeigt, wie Sie das Überwachungsmuster mit Hilfe der Mehrfachvererbung und abstrakten Datentypen implementieren.
Frage:
Was bedeutet es, Funktionalität hochzureichen?
Antwort:
Gemeint ist die Verlagerung von Funktionalität in eine darüberliegende, gemeinsame
Basisklasse. Wenn mehrere Klassen eine Funktion gemeinsam nutzen,
ist es sinnvoll, eine gemeinsame Basisklasse zu suchen, in der sich diese
Funktion unterbringen läßt.
Frage:
Ist das Hochreichen von Funktionalität immer empfehlenswert?
Antwort:
Ja, wenn man gemeinsam genutzte Funktionalität nach oben verschiebt.
Nein, wenn man lediglich die Schnittstelle nach oben bringt. Das heißt, wenn
nicht alle abgeleiteten Klassen die Methode verwenden können, ist es ein Mißgriff,
diese Funktion in eine gemeinsam genutzte Basisklasse zu verlagern.
Wenn man es doch tut, muß man den Laufzeittyp des Objekts aktivieren, bevor
man entscheidet, ob man die Funktion aufrufen kann.
Frage:
Warum ist die Aufschlüsselung des Objekttyps zur Laufzeit nicht zu
empfehlen?
Antwort:
In großen Programmen werden die switch
-Anweisungen groß und schwer zu
warten. Virtuelle Funktionen werden eingesetzt, damit die virtuelle Tabelle
und nicht der Programmierer den Laufzeittyp des Objekts bestimmt.
Frage:
Warum ist Typenumwandlung nicht zu empfehlen?
Antwort:
Gegen Typumwandlung ist nichts einzuwenden, solange sie typensicher
durchgeführt werden. Wird eine Funktion aufgerufen, die weiß, daß das Objekt
von einem bestimmten Typ sein muß, ist die Umwandlung in diesen Typ
in Ordnung. Typumwandlung kann aber zur Untergrabung der strengen Typenprüfung
in C++ führen, und dies gilt es zu vermeiden. Wenn Sie den Code
nach dem Laufzeittyp des Objekts auftrennen und dann mit Typumwandlungen
von Zeigern arbeiten, kann dies ein Zeichen dafür sein, daß etwas mit Ihrem
Code nicht stimmt.
Frage:
Warum deklariert man nicht alle Funktionen als virtuell?
Antwort:
Virtuelle Funktionen werden durch virtuelle Tabellen realisiert, die die Größe
und die Ausführungsgeschwindigkeit des Programms beeinträchtigen. Die Methoden
kleinerer Klassen, die vermutlich nie als Basisklassen eingesetzt werden,
wird man kaum als virtual
deklarieren wollen.
Frage:
Wann sollte ein virtueller Destruktor definiert werden?
Antwort:
Immer dann, wenn Sie eine Klasse aufsetzen, die als Basisklasse fungieren
könnte, und Sie annehmen, daß Zeiger auf die Basisklasse für den Zugriff auf
Objekte der abgeleiteten Klasse verwendet werden. Als allgemeine Regel kann
man sich merken: »Wenn Sie irgendeine Funktion in Ihrer Klasse als virtuell
deklariert haben, deklarieren Sie auch den Destruktor als virtuell«.
Frage:
Warum soll man sich mit abstrakten Datentypen herumschlagen? Man
könnte doch die nicht abstrakte Form beibehalten und einfach das Erzeugen
von Objekten dieses Typs vermeiden.
Antwort:
Der Zweck vieler Konventionen in C++ besteht darin, den Compiler bei der
Fehlersuche heranzuziehen, um Laufzeitfehler aus dem Code (den man
schließlich verkaufen möchte) zu verbannen. Wenn man eine Klasse mit abstrakten
Funktionen ausstattet und damit abstrakt macht, weist der Compiler
mit einer entsprechenden Fehlermeldung auf alle Objekte hin, die man aus
diesem abstrakten Typ erzeugen möchte.
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.
RoundRect
für Rechtecke mit abgerundeten Ecken, die sowohl von Rectangle
als auch von Circle
abgeleitet ist. Rectangle
und Circle
sind ihrerseits von Shape
abgeleitet. Wie viele Shape
s werden dann bei der Instantiierung eines RoundRects
erzeugt?
Horse
und Bird
von der Klasse Animal
als public virtual
Basisklasse abgeleitet sind, rufen dann ihre Konstruktoren den Animal
-Konstruktor auf? Wenn Pegasus
sowohl von Horse
als auch von Bird
abgeleitet ist, wie kann Pegasus
den Animal
-Konstruktor aufrufen?
Vehicle,
und machen Sie die Klasse zu einem abstrakten Datentyp.
JetPlane
auf, die von Rocket
und Airplane
abgeleitet ist.
747
auf, die von der Klasse JetPlane
aus Übung 1 abgeleitet ist.
Car
und Bus
von der Klasse Vehicle
ableitet. Deklarieren Sie Vehicle
als ADT mit zwei abstrakten Funktionen. Car
und Bus
sollen keine abstrakten Datentypen sein.
Car
ein abstrakter Datentyp ist, von dem die Klassen SportsCar
und Coupe
abgeleitet werden. Die Klasse Car
soll für eine der abstrakten Funktionen von Vehicle
einen Anweisungsteil vorsehen, so daß die Funktion nicht mehr abstrakt ist.
© Markt&Technik Verlag, ein Imprint der Pearson Education Deutschland GmbH