Twitter und Facebook-Anbindung
X
Tweet Follow @twitterapi
!!! Anbindung an twitter und facebook öffnen !!!

Wenn Ihnen mein Online-Buch gefällt,
dann bedanken Sie sich doch mit einer kleinen Spende...

22.2 Diamantförmige Vererbung

22.2 Diamantförmige Vererbung

Bisher habe ich Ihnen zwar prinzipiell gezeigt, wie eine Mehrfachvererbung vom Prinzip her funktioniert, aber ich konnte Ihnen noch nicht zeigen, welche negativen Konsequenzen sich daraus ergeben und warum dieses Konzept in vielen anderen Sprachen nicht implementiert wird.

Mit einer diamantförmigen Vererbung ist gemeint, dass es irgendwo eine Basisklasse gibt, wovon zwei oder mehr andere Klassen erben. Anschließend gibt es eine weitere Klasse, welche genau von diesen abgeleiteten Klassen erbt. Im Klassendiagramm ergibt sich das eine Art Raute oder Diamant. Dies soll folgendes Beispiel demonstrieren.

Diamantförmige Vererbung

Wie Sie sehen, habe ich mir ein Beispiel ausgedacht, welches zum einen komplexer ist und zum anderen vom bisherigen Beispiel abweicht. Dies war notwendig, damit ich Ihnen alle wichtigen Aspekte n6AUML,her bringen kann. Schauen Sie sich zunächst die Klassendefinitionen an.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
					
class CTransportmittel {
	public:
		int m_iPositionX;
		int m_iPositionY;
		int m_iPositionZ;
};



class CFahrzeug : public CTransportmittel {
	public:
		int m_iReader;
};

class CFlugzeug : public CTransportmittel {
	public:
		int m_iTriebwerke;
};

class CSchiff : public CTransportmittel {
	public:
		int m_iSegel;
};



class CWasserFlugzeug : public CFahrzeug, public CFlugzeug, public CSchiff {
	public:
		int m_iPontons;
};
					

Man würde jetzt ganz spontan von folgendem Speicherlayout ausgehen.

Gewünschtes Speicherlayout bei einer diamantförmigen Vererbung

Schauen wir uns doch mit einem kleinen Testprogramm an, ob dies tatsächlich der Wahrheit entspricht.

 1
 2
 3
 4
 5
					
printf("CTransportmittel: %i\n", sizeof(CTransportmittel));
printf("CFahrzeug:        %i\n", sizeof(CFahrzeug));
printf("CFlugzeug:        %i\n", sizeof(CFlugzeug));
printf("CSchiff:          %i\n", sizeof(CSchiff));
printf("CWasserFlugzeug:  %i\n", sizeof(CWasserFlugzeug));
					

Ausgabe:

CTransportmittel: 12
CFahrzeug:        16
CFlugzeug:        16
CSchiff:          16
CWasserFlugzeug:  52
		

Ich denke, die ersten vier Werte sind nachvollziehbar. Fahrzeug, Flugzeug und Schiff erben die zwölf Byte der Basisklasse und bringen selbst noch jeweils vier Byte hinzu. Rechnet man jetzt aber vier Byte mal drei erbende Klassen, plus zwölf Byte der obersten Basisklasse, plus vier Byte für die Pontons, erhält man 28. Scheinbar sieht das Layout des Speichers doch anders aus.

Speicherlayout bei einer diamantförmigen Vererbung

Diese Abbildung gleicht schon eher der Realität. Rechnen wir mal nach. Sechzehn mal drei erbende Klassen plus vier Byte für die Pontons, ergibt 52. Ihnen sollte jetzt auffallen, dass es drei mal die Attribute für die Positionsangabe gibt. Wenn man also von außen auf ein Objekt der Klasse zugreift und man die Position ändern will, fragt sich jetzt, auf welche Positionsangabe man Zugriff hat, bzw. wie man diese unterscheiden kann. Genau diese Frage stellt sich der Compiler auch und wird einen normalen Zugriff bemängeln. Deshalb muss man immer den Namensraum mit angeben. Dies könnte dann wie folgt aussehen.

 1
 2
					
CWasserFlugzeug oWasserFlugzeug;
oWasserFlugzeug.CSchiff::m_iPositionX = 10;
					

Diese Schreibweise sieht nicht nur komisch aus, sie ist auch unpraktisch. Stellen Sie sich vor, Sie wollten tatsächlich mit dieser Klasse arbeiten und das Objekt würde sich bewegen. Um alles konsistent zu halten, müssten Sie also jeweils drei mal die X, Y und Z Position ändern. Im Zweifelsfall ist die Vererbungsstruktur noch größer und Sie wissen gar nicht, wie oft es diese Positionsangabe noch gibt.

Sie sehen also schon an diesem trivialen Beispiel, dass es zu großen Unannehmlichkeiten kommen kann. Dies ist der Grund, warum viele Programmiersprachen auf dieses Konzept verzichten. In Java ist z.B. eine Mehrfachvererbung nur in dem Sinne möglich, dass man von einer normalen und dann nur noch von zusätzlichen vollständig abstrakten Klassen (Interfaces) erbt. In diesem Fall kommt es zu keinen Dopplungen, da vollständig abstrakte Klassen weder Attribute noch Methoden bereitstellen. Es gibt, wie bereits erwähnt, nur Muster für Methoden, die in abgeleiteten Klassen zu implementieren sind.

Ein weiterer großer Nachteil ist, das man mit solchen Strukturen arge Probleme mit dem Polymorphismus bekommt. Der Upcast funktioniert in der Regel noch, aber spätestens der Downcast scheitert. Schauen wir uns folgenden Quelltext an.

 1
 2
 3
 4
 5
 6
 7
					
CWasserFlugzeug		oWasserFlugzeug;
CFlugzeug		oFlugzeug		= oWasserFlugzeug;
CTransportmittel	oTransportmittel	= oFlugzeug;

// Folgende Zeilen bringen einen Compilerfehler
oWasserFlugzeug					= static_cast<CWasserFlugzeug>(oFlugzeug);
oFlugzeug					= static_cast<CFlugzeug>(oTransportmittel);
					

Warum sind die Downcasts nicht mehr möglich? Zeile 6 funktioniert nicht, weil es keinen geeigneten benutzerdefinierten Konstruktor gibt. Sicher könnte man für die Klasse "CWasserFlugzeug" einen Konstruktor bauen, der sich aus einem Flugzeug erzeugen kann und in ihm kopiert man die Werte in die Positionsangaben für Fahrzeug, Flugzeug und Schiff, aber mit was initialisiert man dann "m_iReader", "m_iSegel" und "m_iPontons"? Man kommt ja an diese Informationen nicht mehr heran. Zeile 7 funktioniert aus den selben Gründen nicht. Auch hier würden wieder geeignete Konstruktoren fehlen, bei welchen noch mehr Informationen verloren gehen würden. Geeignete Konstruktoren lösen die Sache also nicht wirklich.

Wenn man jetzt Zeiger benutzt, kommt man um das Problem der Konstruktoren herum, da nur die Zeiger neu interpretiert werden müssen. Schauen wir uns ein Beispiel an.

 1
 2
 3
 4
 5
 6
 7
 8
					
CWasserFlugzeug*	pWasserFlugzeug		= new CWasserFlugzeug();
CFlugzeug*		pFlugzeug		= pWasserFlugzeug;
CTransportmittel*	pTransportmittel	= pFlugzeug;

pWasserFlugzeug					= static_cast<CWasserFlugzeug*>(pFlugzeug);
pFlugzeug					= static_cast<CFlugzeug*>(pTransportmittel);

delete pWasserFlugzeug;
					

Dieses Beispiel akzeptiert der Compiler. Zeile 5 und 6 funktionieren, welch die Klassen, auf welche die Zeiger verweisen, in einer direkten und eineindeutigen Vererbungshierarchie stehen. Eine Umwandlung von Transportmittel zurück zu einem Wasserflugzeug ist nicht möglich, da bei einem Zeiger keinerlei Typinformationen vorhanden sind und somit muss der Compiler davon ausgehen, dass es vorher kein Wasserfahrzeug war. Könnte ja ebenso gut ein Fahrzeug, Flugzeug oder Schiff gewesen sein.

Wenn man in den letzten Kapiteln aufgepasst hat, könnte man für genau letzten Fall ein reinterpretierenden Cast benutzen und der Compiler würde sich nicht beschweren, aber spätestens beim Freigeben des Wasserflugzeuges würde es böse krachen. Der Speicherbereich wird neu interpretiert und das hat zur folge, dass das Speicherlayout total durcheinander geworfen wird (ich verkneife mir an dieser Stelle ein Beispiel).

Sie sehen also, dass eine Mehrfachvererbung mehr Nachteile als Vorteile bringt. Man benötigt mehr Speicher, Variablen können doppelt auftreten und ein Polymorphismus ist nicht wirklich anwendbar. Wenn Sie also eine Mehrfachvererbung nutzen, dann bitte nur mit vollständig abstrakten Klassen.

Zum Seitenanfang
Zum Inhaltsverzeichnis

© Copyright by Thomas Weiß, 2009 - 2012