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

19 Vererbung

19 Vererbung

Mit Vererbung ist gemeint, dass Klassen von s.g. Basisklassen Attribute und Methoden erben können. Das heißt, die Klasse hat dann eigene Attribute und Methoden, sowie jene der Basisklasse und ggf. auch die der Basisklasse der Basisklasse.

Ziel der Vererbung ist folgende. Angenommen man soll mehrere Klassen entwerfen und man stellt fest, dass es Gemeinsamkeiten gibt. Bisher musste man diese für jede Klasse einzeln implementieren, was redundanten Quelltext bedeutet. Die Grundidee der OOP ist nun, dass man diese Gemeinsamkeiten in einer Basisklasse (die oftmals rein abstrakt ist) zusammen fasst und die eigentlichen Klassen von dieser Erben lassen.

Immer dann, wenn man Gemeinsamkeiten in einer Basisklasse kapselt, spricht man von einer Generalisierung. Hat man einen allgemeinen Fall (Basisklasse) und leitet davon ausgehend andere Klassen ab, welche besondere Zusatzfunktionen haben, spricht man von einer Spezialisierung. Eine Basisklasse und seine Kindklasse stehen in einer s.g. "part of" Beziehung (Die Basisklasse ist Teil der Kindklasse).

Part of Beziehung zwischen zwei Klassen

Dieses Konzept ist für Neulinge etwas verwirrend, bietet aber sehr viele Vorteile. Um diese aufzuzeigen, werde ich alle Mittel und Wege an einem großen Beispiel erläutern.

Zum Seitenanfang
Zum Inhaltsverzeichnis

19.3 Geschützte Attribute und Methoden

19.3 Geschützte Attribute und Methoden

Immer dann, wenn es Sachen gibt, die man in abgeleiteten Klassen haben möchte, aber nicht nach außen zugreifbar machen will, dann deklariert man diese als "protected".

Ich habe z.B. in der Basisklasse "CFahrzeug" die Methode "Zeichnen" geschützt, da ich nicht möchte, dass sie von außen aufgerufen werden soll. Nun könnten Sie sich fragen, was dies für ein Sinn macht. Die Antwort ist, ich möchte vermeiden, dass man das Fahrzeug zeichnen kann, wenn zum einen noch nicht alle Komponenten erzeugt wurden und zum anderen auch nur, wenn es notwendig ist. Hat sich das Fahrzeug z.B. nicht von der Stelle bewegt, ist es unnötig es neu zu Zeichnen. Dies würde im schlimmsten Fall nur zu einem Ruckeln führen. Stattdessen soll dies nur durch die Methode "Aktualisieren" aufgerufen werden, welche ihrerseits erst einmal die Rahmenbedingen prüft und dann nur ggf. das Fahrzeug neu zeichnet.

Oftmals sieht man aber, dass gerade Zeichen-Methoden öffentlich sind. Mit meinem Beispiel wollte ich nur darauf hindeuten, dass man von Zeit zu Zeit anders denken muss.

Zum Seitenanfang
Zum Inhaltsverzeichnis

19.4 Virtuell und Abstrakt

19.4 Virtuell und Abstrakt

Was hat es nun mit diesem virtuell und abstrakt auf sich? Zunächst einmal ist zu sagen, dass diese Begriffe in den unterschiedlichsten Zusammenhängen verwendet werden und oftmals das Gleiche meinen. Dies ist allerdings nicht korrekt. Spricht man von einer virtuellen Methode, so ist damit gemeint, dass es sich um eine Methode handelt, die in der Basisklasse implementiert ist, aber durch eine abgeleitete Klasse überschrieben werden kann, aber nicht muss. Man nutzt dies hauptsächlich, um bestehende Funktionalitäten zu erweitern. In meinem Beispiel sehen Sie so etwas an der Methode "Zeichnen". Auch wenn sie in der Klasse "CMotorrad" bzw. in der Klasse "CAuto" im Klassendiagramm nicht aufgeführt ist, so könnte sie in den Basisklassen die Farbe des "Zeichenstiftes" festlegen und andere vorbereitende Dinge erledigen. Die abgeleiteten Klassen könnten diese dann einfach nur aufrufen und sich allein auf das eigentliche Zeichnen beschränken. Somit spart man wieder redundanten Quelltext für jede der abgeleiteten Klassen. Dies sähe z.B. folgendermaßen aus.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
					
			
void CMotorrad::Zeichnen() {
	// Prüfen ob alle Komponenten erzeugt wurden
	// ...

	// Stiftfarben setzen und Hintergrund neu malen
	// ...
}

void CHarleyDavidson::Zeichnen() {
	// Aufruf der virtuellen Methode der Basisklasse
	CMotorrad::Zeichnen();

	// Motorrad Zeichnen
	// ...
}
					

In der Definition der Klassen, also in den Header-Dateien, signalisiert man dies, indem man vor die entsprechende Methode das Schlüsselwort "virtual" schreibt. Dies genügt in der Basisklasse, da man aber nie weiß, welche Klassen von der Aktuellen später noch abgeleitet werden sollen, welche wiederum das Zeichnen anders implementieren und die Funktionalität der Basisklasse nutzen möchten, schreibt man das "virtual" auch vor die Methodendefinition der aktuellen Klasse. Wenn eine Methode einmal als virtuell gekennzeichnet ist, wird sie in der gesamten nachfolgenden Vererbungshierarchie virtuell bleiben (man kann sie also immer überschreiben). Dessen sollten Sie sich immer bewusst sein.

Des weiteren ist es durchaus möglich, die virtuelle Methode, einer in der Hierarchie weiter oben liegenden Klasse aufzurufen, falls man die Funktionalität der dazwischenliegenden Klassen nicht braucht bzw. sie sogar störend sind. Man ruft aber nie die virtuellen Methoden aller abgeleiteten Klassen auf, da es logischerweise zu Dopplungen kommt. In diesem Beispiel kann die Klasse "CHarleyDavidson" aber nicht auf die Methode "Zeichnen" der Klasse "CFahrzeug" zugreifen, da jene nur abstrakt ist, was mich zum nächsten Thema führt.

Mit abstrakt ist gemeint, dass es in der Basisklasse eine formale Definition einer Methode gibt, welche jedoch nicht implementiert ist. Man möchte damit eine formale Regel definieren, welche besagt, dass es prinzipiell in einer abgeleiteten Klasse diese Methode geben kann und wenn dem so ist, dann sieht sie entsprechend aus und auf keinen Fall anders. Jede Methode, welche abstrakt definiert ist, ist somit auch virtuell. Manchmal verwendet man dann auch die Bezeichnung "virtuell abstrakt". Wie Sie eine solche abstrakte Methode definieren können, werde ich jetzt zeigen.

 1
 2
 3
 4
 5
					
class CFahrzeug {
	protected:
		// Formale Definition einer abstrakten Zeichnen-Methode
		void Zeichnen(void) = 0;
};
					

Die Syntax für abstrakte Methoden sieht etwas merkwürdig aus, da man fast glauben könnte, dass es eine Inlinedeklaration ist. Tatsächlich ist es auch im Prinzip so etwas. Man bringt dem Compiler so bei, dass er in der entsprechenden CPP Datei nicht nach einer solchen Methode Ausschau halten muss.

An dieser Stelle sei noch erwähnt, dass nur Methoden virtuell und oder abstrakt sind.

Wenn man von abstrakten Basisklassen spricht, dann meint man Klassen, welche virtuell abstrakte Methoden besitzen. Eine sehr wichtige Eigenschaft dieser Klassen ist, dass man von ihnen keine Objektinstanz erzeugen kann (eben weil die Implementierung bestimmter Methoden fehlt). Aber wenn man nur von C++ ausgeht, kann eine Klasse auch als abstrakt definiert werden, obwohl sie es theoretisch nicht ist (keine abstrakten Methoden). Durch einen speziellen Modifikator kann dies erzwungen werden. In meinem großen Beispiel habe ich diesen Fall nicht bedacht, aber dennoch möchte ich Ihnen kurz zeigen, wie man eine solche Klasse definiert.

 1
 2
 3
					
class CBasisKlasse abstract {
	// Definition von Attributen und Methoden
};
					

Dann gibt es da noch rein abstrakte Klassen (pure abstract), welche ausschließlich aus virtuell abstrakten Methoden bestehen und keinerlei Attribute definieren. Man spricht auch von s.g. Interfaces. Dem Klassennamen wird hier gerne ein "I" statt ein "C" vorangestellt. Gerade in Java kommen Interfaces sehr oft zum Einsatz. Hier sehen Sie schon, dass abstrakt mehrere Bedeutungen haben kann. Alle haben sie aber eines gemeinsam, man kann von ihnen keine Objektinstanz erzeugen.

Es gibt zu guter Letzt auch noch virtuelle Klassen. Was das ist, werde ich spä erklären, wenn es um Mehrfachvererbung geht.

An dieser Stelle möchte ich Sie gleich auf einen Nachteil der Vererbung hinweisen. Wenn man tatsächlich Methoden in einer Basisklasse als virtuell deklariert, hat dies zur Folge, dass Objektinstanzen größer werden, als man meint. Dies soll folgendes Beispiel demonstrieren.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
					
class CBasis1 {
	public:
		int m_iBasis1;
		virtual void TestMethode1() {}
};



class CBasis2 {
	public:
		int m_iBasis2;
		void TestMethode2() {};
};



class CKindVonBasis1 : public CBasis1 {};

class CKindVonBasis2 : public CBasis2 {};

// ...

printf("%i %i\n", sizeof(CKindVonBasis1), sizeof(CKindVonBasis2));
					

Ausgabe:

8 4
		

Wie Sie sehen können, sind die Basisklassen und die abgeleiteten Klassen vom Aufbau identisch, aber trotzdem haben die zwei Klassen "CKindVonBasis1" und "CKindVonBasis2" eine unterschiedliche Größe. Dies liegt daran, dass bei der Klasse "CKindVonBasis1" ein Zeiger auf eine s.g. virtuelle Tabelle hinzugefügt wird. Die ist zwingend notwendig, da man ja virtuelle Methoden überschreiben kann und sie somit mehrfach vorhanden sein können (eben in unterschiedlichen Kontexten). Damit der Computer also weiß, welche Methode er aufzurufen hat, wird eine Liste geführt (die genaue Funktionsweise erspare ich mir an dieser Stelle, aber falls Sie interessiert sind, suchen Sie doch mal im Internet nach "virtual table"). Der Verweis auf eine solche Tabelle kostet 4 Byte. Nun könnte man sich denken, dass das erst einmal nicht so schlimm ist, aber wenn man irgendwann ein Spiel entwickelt mit einer Million Polygonen, macht es schon einen Unterschied, ob man 4 MB mehr Speicher benötigt oder nicht. Abgesehen davon können Methoden auch nicht mehr direkt aufgerufen werden. Im Hintergrund muss immer erst die richtige Methode gesucht werden (abgesehen davon muss auch erst die virtuelle Tabelle geladen werden). Dies macht einen Methodenaufruf noch langsamer.

Zum Seitenanfang
Zum Inhaltsverzeichnis

19.2 Privates, geschütztes und öffentliches Erben

19.2 Privates, geschütztes und öffentliches Erben

Als ich vorhin gesagt habe, dass man alle Attribute und Methoden der Basisklasse erbt, war dies nicht ganz die Wahrheit bzw. etwas unscharf formuliert, da es davon abhängt, wie man erbt. Am wenigsten Einschränkungen gibt es, wenn man "public" erbt. Dann stehen all die Attribute und Methoden zur Verfügung, welche in der Basisklasse "public" und "protected" sind. Alle anderen Sachen, welche "private" sind, werden verborgen, existieren aber noch im Hintergrund, denn wenn man eine Methode der Basisklasse aufruft, kann diese wiederum auf ihre privaten Attribute zugreifen. Man bekommt davon nur nichts mit, weil die privaten Attribute und Methoden in einer Art Blackbox versteckt sind.

Erbt man nun "privat", so ändert sich für die Klasse die erbt erst einmal nichts, aber von außen betrachtet wird schon einiges anders, da alle Attribute und Methoden der Basisklasse, auf einmal nicht mehr zur Verfügung stehen. Dies würde auch Klassen betreffen, die von der abgeleiteten Klasse abgeleitet sind. Ich habe diesen Fall nicht mit in das komplexe Beispiel eingebunden, aber ein möglicher Anwendungsfall wäre, dass man sich einen speziellen Container bauen möchte, welcher irgendwelche Elemente aufnehmen soll. Dazu erbt man am Besten von der Klasse "CList" ab, da man dann schon alle Funktionalitäten besitzt, welche zur Verwaltung von Listen notwendig sind. Nun möchte man aber nicht alle diese geerbten Methoden nach außen tragen, weil man kontrollieren will, was z.B. hinzugefügt oder entfernt wird. Um dies zu erreichen, würde man "privat" erben und entsprechende öffentliche Methoden bauen, die die gewünschten Überprüfungen durchführen und ggf. die Aktionen abblocken.

Ähnlich ist es, wenn man "protected" erbt, nur dass dann die öffentlichen Attribute und Methoden "protected" werden. Das hat zur Folge, dass sie zwar auch von außen nicht mehr zur Verfügung stehen, jedoch haben abgeleitete Klassen noch Zugriff. In folgender Grafik habe ich dies versucht zu visualisieren.

Öffentliches, geschütztes und privates Erben

Wie man sehen kann, verändert sich die Sichtbarkeit der Methoden in den abgeleiteten Klassen und in den vavon abgeleiteten Klassen, findet wiederum eine Veränderung statt. In meinem komplexen Beispiel bin ich aber auch auf diese Art der Vererbung nicht eingegangen, da man diese vielen Varianten nur sehr selten braucht und es immer schwer ist, entsprechende sinnvolle Fälle zu konstruieren. Ich werde also immer nur öffentlich erben.

Kommen wir aber nun wieder zurück zum eigentlichen Thema. Dort sehen Sie nun, dass z.B. die Klasse "CHarleyDavidson" von der Klasse "CMotorrad" abgeleitet ist, welche wiederum von der Klasse "CFahrzeug" erbt. Die Ableitungen sind öffentlich (was man dem UML-Klassendiagramm leider nicht entnehmen kann). Im folgenden Quelltextausschnitt sehen Sie, wie dies aussehen könnte.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
					
class CFahrzeug {
	//...
};

class CMotorrad : public CFahrzeug {
	//...
};
						
class CHarleyDavidson : public CMotorrad {
	// ...
};
					

Vererbungspfad für die Klasse CHarleyDavidson

Die Klasse "CHarleyDavidson" besitzt jetzt also nicht nur einen Kickstarter, sondern auch einen Lenker, eine Kette sowie eine Farbe und eine Geschwindigkeit, nur mit dem feinen Unterschied, dass sie selbst an Kette, Lenker, Farbe und Geschwindigkeit nicht herankommt, da sie in den Basisklassen "CMotorrad" und "CFahrzeug" als privat deklariert sind. Dies wird auch an dem Minus im UML-Klassendiagramm kenntlich gemacht. Die Basisklassen müsste also noch entsprechende Getter bzw. Setter zur Verfügung stellen, damit der Zugriff möglich ist.

Auffällig ist auch, dass "CHarleyDavidson" nur von "CMotorrad" erben muss. Das zusätzliche Enthalten der Attribute und Methoden der Klasse "CFahrzeug" ist automatisch dadurch gegeben, dass "CMotorrad" von "CFahrzeug" erbt. Man hat also eine Vererbungshierarchie und die geerbten Sachen werden an alle abgeleiteten Klassen weiter vererbt, welche jene an weiter vererbte Klassen weiterreichen. Für dieses Beispiel bedeutet dies, dass ich in den entsprechenden Auto - und Motorradklassen Implementierungsaufwand spare. Gerade wenn ich zehn weitere Motorräder und zwanzig weitere Autotypen benötigen würde, spare ich enorm viel redundanten Quelltext.

Zum Seitenanfang
Zum Inhaltsverzeichnis

19.1 Das Vererbungsbeispiel

19.1 Das Vererbungsbeispiel

Um alle Aspekte der Vererbung, Aggregation und Kompisition, sowie des Polymorphismus zu zeigen, habe ich mir ein komplexeres Beispiel einfallen lassen. Ich gebe gleich vorweg, dass mein Lösungsansatz nicht perfekt ist und dass es hier und dort Sachen gibt, die man anders machen sollte. An entsprechenden Stellen werde ich aber darauf eingehen und das Für und Wider diskutieren.

Nun, stellen Sie sich vor, Sie wollten ein Computerspiel entwerfen, welches ein Rennsimulator sein soll. Dabei sollen folgende Kriterien erfüllt sein.

Für das Modellieren von Klassenhierarchien ist es üblich, dies mit UML zu machen, da dies weltweit verstanden wird und man so gewisse Teilaufgaben auch an andere Firmen weitergeben kann. Ich habe für das Beispiel ein solches UML-Klassendiagramm entworfen.

UML-Klassendiagramm für die Klassenhierarchien des Beispieles

Nun sieht das ganze auf den ersten Blick sehr umfangreich aus, aber keine Angst, ich werde in den folgenden Kapitel immer nur kleine Teile raus greifen und so das komplette Beispiel, Stück für Stück erläutern. Die obige Grafik dieht also zunächst nur für den Überblick und der Orientierung.

Zum Seitenanfang
Zum Inhaltsverzeichnis

© Copyright by Thomas Weiß, 2009 - 2012