vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbacknächstes Kapitel


Woche 2

Tag 13



Polymorphie

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,

Probleme bei der einfachen Vererbung

Angenommen, Sie arbeiten schon seit einiger Zeit mit Tierklassen und haben Ihre Klassenhierarchie in Birds (Vögel) und Mammals (Säugetiere) aufgeteilt. Die Bird-Klasse verfügt über die Elementfunktion Fly(). Die Mammal-Klasse wurde unterteilt in eine Reihe von Mammals, 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.

Elemente in Basisklassen auslagern

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.

Abwärts gerichtete Typumwandlung

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:

  1. Rufen Sie für Ihr Projekt den Befehl Projekt/Einstellungen auf.
  2. Gehen Sie zu der Registerkarte C++.
  3. Wählen Sie die Kategorie: Programmiersprache C++ aus.
  4. Setzen Sie die Option Run-Time-Type-Informationen (RTTI) aktivieren.
  5. Kompilieren Sie das gesamt Projekt neu.

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.

In zwei Listen aufnehmen

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.

Was Sie tun sollten

... und was nicht

Verschieben Sie Funktionalität in der Vererbungshierarchie nach oben.

Vermeiden Sie, den Laufzeittyp des Objekts zu wechseln - verwenden Sie statt dessen lieber virtuelle Methoden, Templates und Mehrfachvererbung.

Verschieben Sie Schnittstellenelemente nicht in der Vererbungshierarchie nach oben.

Wandeln Sie den Typ von Zeigern auf Basisobjekte nicht in Typen von abgeleiteten Objekten um.

Mehrfachvererbung

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.

Beispiel 1:

     class Pegasus : public Horse, public Bird

Beispiel 2:

     class Schnudel : public Schnauzer, public Pudel

Die Teile eines mehrfach vererbten Objekts

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.

Konstruktoren in mehrfach vererbten Objekten

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.

Auflösung von Mehrdeutigkeiten

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.

Wenn Sie nur schreiben

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();

Von einer gemeinsamen Basisklasse erben

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.

Virtuelle Vererbung

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.

Beispiel 1:

     class Horse : virtual public Animal
class Bird : virtual public Animal
class Pegasus : public Horse, public Bird

Beispiel 2:

     class Schnauzer : virtual public Dog
class Pudel : virtual public Dog
class Schnudel : public Schnauzer, public Pudel

Probleme bei der Mehrfachvererbung

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.

Was Sie tun sollten

... und was nicht

Verwenden Sie Mehrfachvererbung, wenn eine neue Klasse Funktionen und Merkmale von mehr als einer Basisklasse benötigt.

Verwenden Sie virtuelle Vererbung, wenn die Klassen am Ende der Ableitungshierarchie nur eine Instanz der gemeinsamen Basisklasse haben dürfen.

Lassen Sie die gemeinsame Basisklasse von der Klasse am Ende der Ableitungshierarchie aus initialisieren, wenn Sie virtuelle Basisklassen verwenden.

Verzichten Sie auf Mehrfachvererbung, wenn sich das Problem auch mit einfacher Vererbung lösen läßt.

Mixin-Klassen

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.

Abstrakte Datentypen (ADTs)

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.

Listing 13.7: Shape-Klassen

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 Funktionen

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.

Abstrakte Datentypen

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.

Dazu ein Beispiel:

     class Shape
{
virtual void Draw() = 0; // abstrakt
};

Abstrakte Funktionen implementieren

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.

Komplexe Abstraktionshierarchien

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 Animals oder Mammals instantiieren lassen, aber daß alle Mammals 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.

Welche Typen sind abstrakt?

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ß.

Was Sie tun sollten

... und was nicht

Verwenden Sie abstrakte Typen, um gemeinsame Funktionalität für mehrere Klassen bereitzustellen.

Überschreiben Sie alle abstrakten Funktionen.

Deklarieren Sie alle Funktionen, die in den abgeleiteten Klassen überschrieben werden sollen, als abstrakt.

Versuchen Sie nicht, ein Objekt eines abstrakten Datentyps zu instantiieren.

Das Überwachungsmuster

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.

Ein Wort zu Mehrfachvererbung, abstrakten Datentypen und Java

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.

Zusammenfassung

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.

Fragen und Antworten

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.

Workshop

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

Quiz

  1. Was ist eine abwärts gerichtete Typenumwandlung?
  2. Was ist der »v-ptr«?
  3. Stellen Sie sich vor, Sie haben eine Klasse 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 Shapes werden dann bei der Instantiierung eines RoundRects erzeugt?
  4. Wenn 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?
  5. Deklarieren Sie eine Klasse Vehicle, und machen Sie die Klasse zu einem abstrakten Datentyp.
  6. Wenn eine Basisklasse einen ADT darstellt und drei abstrakte Funktionen beinhaltet, wie viele dieser Funktionen müssen dann in den abgeleiteten Klassen überschrieben werden?

Übungen

  1. Setzen Sie die Deklaration für eine Klasse JetPlane auf, die von Rocket und Airplane abgeleitet ist.
  2. Setzen Sie die Deklaration für eine Klasse 747 auf, die von der Klasse JetPlane aus Übung 1 abgeleitet ist.
  3. Schreiben Sie ein Programm, das die Klassen 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.
  4. Ändern Sie das Programm aus Übung 3 dahingehend, daß die Klasse 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.



vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbacknächstes Kapitel


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