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

20 Polymorphismus

20 Polymorphismus

Der Polymorphismus ist mit einer der genialsten Vorteile, die sich aus einer glücklichen Vererbungshierarchie ergibt. Zu deutsch heißt es Vielgestaltigkeit. Ein Objekt kann also mehrere Formen annehmen. Bevor ich Ihnen aber zeigen kann was ich damit meine, muss ich mit Ihnen noch kurz ein anderes Thema behandeln, nämlich den Mechanismus, mit welchen man es erreicht, dass ein Objekt eine andere Gestalt annehmen kann.

Zum Seitenanfang
Zum Inhaltsverzeichnis

20.3 Wozu braucht man Polymorphismus

20.3 Wozu braucht man Polymorphismus

Stellen Sie sich vor, Sie bauen tatsächlich ein Rennspiel mit den von mir gezeigten Klassen. Nun wollen Sie die Logik für das eigentliche Rennen bauen, in welchem beliebige Fahrzeuge gegeneinander antreten sollen. Nun hat man zunächst ein Problem. Selbst wenn man die Anzahl der Fahrzeuge festsetzt, muss man immer noch verschiedene Typen in Variablen unterbringen, wobei der Typ aber erst zur Laufzeit feststeht. Genau hier kommt der Polymorphismus ins Spiel und das ursprüngliche Problem ist keines mehr. Sie erzeugen sich also das gewünscht Fahrzeug (egal ob Mercedes oder Harley Davidson) und speichern es in einem Array, welches Fahrzeuge aufnehmen kann.

In folgender Grafik habe ich dies mal Schematisch dargestellt. Wichtig dabei ist, dass die Objekte, trotz der Typumwandlung, intern erhalten bleiben, auch wenn man nicht mehr auf alle Attribute und Methoden zugreifen kann.

Schematische Darstellung für einen Polymorphismus

Das eigentlich geniale daran kommt aber noch. Wenn die Klasse "CFahrzeug" eine virtuelle Methode "Zeichnen" bereitstellt und alle anderen Klassen diese überschreiben, kann man sich den Downcast sparen, um jene Methode aufzurufen. Intern bleiben die Objekte wie sie ursprünglich waren und da sie eine virtuelle Tabelle besitzen, weiß der Computer, dass er nicht die Methode des Fahrzeuges, sondern des gewünschten Objektes aufrufen soll. In jener Methode stehen dann auch wieder die einzigartigen Attribute und Methoden der einzelnen Klasse zur Verfügung. Man braucht sich also nicht zu merken, an welcher Stelle im Array was steht. Nur wenn man auf Sachen zugreifen will, die nicht in der Basisklasse stehen, muss man wieder explizit Downcasten und man muss sich somit merken, was wo stand. Hier mal ein Beispiel.

 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
31
32
33
34
35
36
37
38
39
					
class CFahrzeug { 
	public:
		virtual void Aktualisieren() {
			printf("CFahrzeug::Aktualisieren\n");
		} // Aktualisieren
};

class CAuto : public CFahrzeug {
	public:
		virtual void Aktualisieren() {
			printf("CAuto::Aktualisieren\n");
		} // Aktualisieren
};

class CMotorrad : public CFahrzeug {
	public:
		virtual void Aktualisieren() { 
			printf("CMotorrad::Aktualisieren\n");
		} // Aktualisieren
};

// ...

CFahrzeug*	aFahrzeuge[3];
CFahrzeug*	pFahrzeug	= new CFahrzeug();
CAuto*		pAuto		= new CAuto();
CMotorrad*	pMotorrad	= new CMotorrad;

aFahrzeuge[0]			= pFahrzeug;	// Suchen der richtigen Methode
aFahrzeuge[1]			= pAuto;	// Suchen der richtigen Methode
aFahrzeuge[2]			= pMotorrad;	// Suchen der richtigen Methode

aFahrzeuge[0]->Aktualisieren();
aFahrzeuge[1]->Aktualisieren();
aFahrzeuge[2]->Aktualisieren();

delete aFahrzeuge[2];
delete aFahrzeuge[1];
delete aFahrzeuge[0];
					

Ausgabe:

CFahrzeug::Aktualisieren
CAuto::Aktualisieren
CMotorrad::Aktualisieren
		

Wie Sie sehen konnten, habe ich eine ganz ganz spartanische Implementierung der drei Klassen "CFahrzeug", "CAuto" und "CMotorrad" vorgenommen und lediglich eine virtuelle Methode namens "Zeichnen" implementiert, wobei jene nur den eigenen Klassennamen auf der Konsole ausgibt. Anschließend erzeuge ich ein Array, welches Fahrzeuge aller Art aufnehmen kann, sowie drei Objekte (je eines pro Klasse), welche ich in das Array einhänge. Anschließend rufe ich die Methode auf und man sollte meinen, da es sich jetzt allgemein um Fahrzeuge handelt, dass immer "CFahrzeug::Aktualisieren" ausgegeben wird. Dank der virtuellen Tabelle eines jeden Objektes, wird aber immer die eigene Methode aufgerufen und die Ausgabe entspricht den jeweiligen Klassennamen. Falls eine Klasse die gewünschte Methode nicht implementiert, wird die Methode der Basisklasse aufgerufen.

Hier sollte aber eines schon auffallen. Genau weil man nur Sachen aufrufen kann, die in der Basisklasse vorkommen, neigt man dazu, immer mehr Code so variabel zu gestalten, dass man ihn in eine Basisklasse verschieben kann. Das hat zur Folge, dass die Kindklassen irgendwann nur noch zu Datencontainern werden. Dann stellt sich wieder die Frage, ob man wirklich Vererbung braucht. Wie angedeutet, braucht man virtuelle Methoden, welche die Bearbeitung von Objekten langsamer macht. Sie sollten also nicht auf Teufel komm raus eine Basisklasse bauen, nur um all Ihre Objekte in einem Array halten zu können. Schaffen Sie nur da eine Basisklasse, wo es vom reinen Sprachgebrauch auch sinnvoll wäre. Auto und Fahrzeug ist noch sinnvoll, aber wenn man jetzt einen Mensch hat und möchte Mensch und Fahrzeug gleichzeitig verwalten, macht es nicht viel Sinn, eine Klasse "CDingsda" zu erzeugen. Später weiß keiner mehr, was damit gemeint ist. Sicher könnte man diese Klasse auch "C3DModel" nennen und das wäre zu mindestens sinnvoller bzw. nachvollziehbarer. Trotzdem sollten Sie solche Scherze vermeiden, da im allgemeinen Verständnis ein Fahrzeug nicht wirklich etwas mit einem Menschen zu tun hat (abgesehen davon, das er es fährt, aber dann würde man eher eine Aggregation als eine Vererbung annehmen).

Zum Seitenanfang
Zum Inhaltsverzeichnis

20.2 Was ist Polymorphismus

20.2 Was ist Polymorphismus

Wie bereits erwähnt, ist der Polymorphismus einer der genialsten Vorteile, welcher sich aus einer Vererbung ergibt. Gemeint ist, dass ein Objekt die Form seiner Basisklasse und oder deren Basisklasse annehmen kann. Diese Verwandlung ist auch umkehrbar. Man kann zum Beispiel sagen, dass ein Ferrari ein Auto ist und das ein Auto ein Fahrzeug ist. Genauso kann man sagen, dass ein Ferrari ein Fahrzeug ist. Somit kann ich aus einem Ferrari ein Auto machen. In Folge stehen mir dann auch nur die Attribute und Methoden zur Verfügung, welche ein Auto hat. Später kann ich dann aus diesem Auto wieder ein Ferrari machen. Letzter Schritt funktioniert aber nur, wenn es wirklich mal ein Mercedes war.

Die Umwandlung von Ferrari nach Auto nennt man einen "impliziten Upcast". Implizit, weil man bei der Umwandlung keine Castfunktion aufrufen braucht. Upcast meint, dass ich mich in der Vererbungshierarchie noch oben bewege. Hier mal ein Beispiel.

 1
 2
 3
					
CFerrari	oF40(...);
CAuto		oAllgemeinesAuto	= oF40;	// keine händische Typumwandlung
CFahrzeug	oAllgemeinesFehrzeug	= oF40;	// keine händische Typumwandlung
					

Die Rückverwandlung von einem Auto in ein Ferrari nennt man "expliziten Downcast". Mit explizit ist gemeint, dass man hier sehr wohl eine Typumwandlung vornehmen muss. Dem Compiler ist zwar bekannt, dass ein Ferrari ein Auto ist, aber umgekehrt kann man nicht zwangsläufig sagen, dass ein Auto ein Ferrari ist. Es könnte ja auch ein Opel sein. Downcast mein, dass man sich in der Vererbungshierarchie nach unten bewegt. Ich erweitere eben gezeigtes Beispiel, um dies zu verdeutlichen.

 4
 5
					
oF40	= static_cast<CFerrari>oAllgemeinesAuto;	// Typumwandlung nötig
oF40	= static_cast<CFerrari>oAllgemeinesFehrzeug;	// Typumwandlung nötig
					

Sie fragen sich jetzt bestimmt, was man mit diesem Mechanismus anstellen kann. Die Antwort auf diese Frage folgt im nächsten Unterkapitel.

Zum Seitenanfang
Zum Inhaltsverzeichnis

20.1 Typumwandlungen II

20.1 Typumwandlungen II

Wie ich bereits in Kapitel 1.2 erwähnt habe, spielt die Typumwandlung bei Klassen eine größere Rolle und deswegen werde ich an dieser Stelle diese Thematik erneut aufgreifen und etwas ausführlicher besprechen.

Man unterscheidet zwischen füf verschiedenen Grundarten der Typumwandlung.

Des weiteren findet man im Internet noch diverse andere Typumwandlungen (union_cast, number_cast, truncate_cast, ...), welche aber an dieser Stelle viel zu weit führen würden, nicht Standard sind und auch nur für die absoluten Hardcoreprogrammierer interessant sein könnten.

Zum Seitenanfang
Zum Inhaltsverzeichnis

20.1.1 C Casting

20.1.1 C Casting

Das klassische C Casting ist genau diese Art von Typumwandlung, welche ich in Kapitel 1.2 angesprochen habe. In modernen Quelltexten findet man so etwas allerdings eher selten, da diese Art des Castings noch aus den Uhrzeiten stammt. Erstaunlicherweise funktioniert es zwar in Visual Studio auch bei Klassen, aber allein die Tatsache, dass es eigentlich nicht funktionieren dürfte, sollte Sie abschrecken.

Der Vollständigkeit halber, sehen Sie im folgendem Quelltext noch einmal ein klassisches C Casting. Sie sollten aber ab jetzt die Finger davon lassen!

 1
 2
 3
 4
 5
					
float	fWert		= 1234.5678;
int	iGerundet1	= (int)fWert;	// Variante 1
int	iGerundet2	= int(fWert);	// Variante 2

printf("Abgerundet: %i und %i", iGerundet1, iGerundet2);
					

Ausgabe:

Abgerundet 1234 und 1234
		
Zum Seitenanfang
Zum Inhaltsverzeichnis

20.1.5 reinterpret Casting

20.1.5 reinterpret Casting

Diese Typumwandlung ist etwas besonderes und auch die gefährlichste Variante. Mit ihr kann man Speicherbereiche neu interpretieren. Was meine ich damit? Angenommen, Sie haben einen Float und machen ein statisches Casten in einen Int. Dann wird der Wert des Floats so im Speicher mgeschrieben, dass ein Integer daraus wird. Das Ergebnis im Arbeitsspeicher sieht also nach der Konvertierung total anders aus. Beim Reinterpretieren, bleibt jede 1 und 0 an der Stelle wo sie war, was zur folge hat, dass man nicht genau voraussagen kann, was am Ende dabei herauskommt.

Ein denkbarer Anwendungsfall ist, dass man wirklich sehen möchte, wie gewisse Datentypen im Speicher abgelegt werden. In diesem Fall würde man eine reinterpret Typumwandlung auf beispielsweise einen unsigned int machen. Dies will ich jetzt einmal Demonstrieren.

 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
31
32
33
34
35
36
37
38
39
40
41
					
// Ausgabe eines Integers als Binärzahl //////////////////////////////////
void Ausgabe(unsigned int iWert) {
	unsigned int	iErgebnis			= iWert;
	unsigned int	aSpeicher[32];
	int		iPosition			= 31;

	do { // Umwandlung Dezimal -> Binär
		aSpeicher[iPosition--]			= iErgebnis % 2;
		iErgebnis				/= 2;
	} while (iErgebnis > 0);

	// Mit Nullen auffüllen
	while (iPosition >= 0) aSpeicher[iPosition--]	= 0;

	printf("Dezimal: %11i - Binaer: ", iWert);
	
	for (iPosition = 0; iPosition < 32; iPosition++) | 
		printf("%u", aSpeicher[iPosition]);
	} // end of for
	
	printf("\n");
} // Ausgabe //////////////////////////////////////////////////////////////////



// Hauptfunktion der Anwendung ////////////////////////////////////////////////
int main(int argc, char** argtv) {
	float* pWert1			= new float(-20.25f);
	float* pWert2			= new float(0.125f);

	unsigned int* pKonvertiert1	= reinterpret_cast<unsigned int*>(pWert1);
	unsigned int* pKonvertiert2	= reinterpret_cast<unsigned int*>(pWert2);

	Ausgabe(*pKonvertiert1);
	Ausgabe(*pKonvertiert2);

	delete pWert1;
	delete pWert2;

	return 0;
} // main /////////////////////////////////////////////////////////////////////
					

Ausgabe:

Dezimal: -1046347776 - Binaer: 11000001101000100000000000000000
Dezimal:  1040187392 - Binaer: 00111110000000000000000000000000
		

Sie sehen also deutlich, dass der Speicherbereich komplett neu interpretiert wird und dass es ohne weiteres nicht möglich ist, den neuen Wert abzuschätzen. Wenn man sich nur den Wert des Integers anschaut, fragt man sich, wie jener zustande kommt. In dem Moment, wo man diese Dezimalzahl in eine binäre umwandelt und ausgibt, sieht man dann, dass der Binärwert, nach IEEE 754, tatsächlich dem Wert des Floats entspricht.

Ein weiterer Anwendungsfall wäre, eine Adresse (also den Wert eines Zeigers) in einem Array aus Integern zwischenzuspeichern. Genauso kann man auch Zeiger auf ganz andere Sachen umbiegen. Diese Art der Typumwandlung ist also eine Art Baseballschläger, mit dem man fast alles gnadenlos umwandeln kann. Also, auch hier sollten Sie sehr vorsichtig sein!

Zum Seitenanfang
Zum Inhaltsverzeichnis

20.1.4 dynamic Casting

20.1.4 dynamic Casting

Ich hatte bereits erwähnt, dass es bisher keinerlei Prüfmechanismen gab, welche eine Konvertierung in nicht voneinander abhängige Typen, verhindert hat und genau hier setzt die dynamische Typumwandlung an. Wenn man jetzt versucht aus einem Zeiger auf ein Fahrzeug, ein Zeiger auf ein Motorrad zu wandeln, dann wird zu mindestens geprüft, ob ein Motorrad in irgend einer Form von Fahrzeug abgeleitet wurde und es somit möglich sein kann, dass es sich bei dem Speicherbereich, auf welchen verwiesen wird, um ein Motorrad handeln kann. Dynamisches Casting funktioniert also nur bei polymorphen Klassen.

 1
 2
 3
 4
					
CMotorrad* pMotorrad1	= new CMotorrad();
CFahrzeug* pFahrzeug	= pMotorrad1;			// hoch casten

CMotorrad* pMotorrad2	= dynamic_cast<CMotorrad*>(pFahrzeug);	// runter casten
					

Diese Art der Typumwandlung wird eher selten gemacht, da sie recht teuer ist. Abgesehen von den zusätzlichen Prüfungen zur Laufzeit, muss jedes Objekt zusätzliche Typinformationen mit sich herumtragen, damit die Überprüfung weiß, was sich ursprünglich hinter einem Zeiger verborgen hat (Objekte benötigen mehr Speicherplatz). Sie sollten sich also von diesem Mechanismus distanzieren und ihn nur im Notfall verwenden.

Zum Seitenanfang
Zum Inhaltsverzeichnis

20.1.3 static Casting

20.1.3 static Casting

Das statische Casten ist die Mutter der Castings in C++ und funktioniert im Grunde genommen wie das klassische C Casting, nur mit dem Unterschied, dass man wieder ein Funktionstemplate benutzt. Bei der Umwandlung selbst werden keine Überprüfungen vorgenommen, aber falls man etwas casten will, was offensichtlich nicht funktioniert, wird man schon durch den Compiler darauf hingewiesen. In den nächsten Kapiteln wird Ihnen genau diese Art der Typumwandlung begegnen.

Im folgenden Beispiel zeige ich, wie man einen spezifischen Zeiger in einen Allgemeinen und wieder zurück wandelt.

 1
 2
 3
 4
 5
 6
 7
					
int*	pWert1	= new int(10);
void*	pWert2	= static_cast<void*>(pWert1);
long*	pWert3	= static_cast<long*>(pWert2);

printf("%li", *pWert3);

delete pWert1;
					

Ausgabe:

10
		

In Zeile 1 erzeuge ich mir einen Pointer auf einen Integer und initialisiere ihn gleich mit dem Wert 10. Anschließend, in Zeile 2, erzeuge ich mir einen "void" Zeiger, welchen ich auf den gleichen Wert zeigen lasse, allerdings jetzt ohne Angabe auf die Struktur, auf welche der Zeiger verweist. In Zeile 3 erzeuge ich jetzt einen Zeiger auf einen "long" Wert und lasse ihn, mittels statischem Casten, auf den selben Wert zeigen. Jetzt wird der Speicher, auf welchen der Zeiger verweist, als "long" interpretiert und wie Sie das an der Ausgabe sehen, hat alles wie gewünscht, funktioniert.

Zum Seitenanfang
Zum Inhaltsverzeichnis

20.1.2 const Casting

20.1.2 const Casting

Hin und wieder steht man vor dem Problem, dass man einen konstanten Wert bzw. eine Variable oder Funktion, welche mit "const" qualifiziert wurde, in eine dynamische Variable überführen möchte oder muss. Hierfür gab es bisher keine Möglichkeit. Das s.g. const Casting bietet hier einen Ausweg. Aber Vorsicht! Diese Umwandlung ist nur vorübergehend! Mann darf anschließend den Wert nicht ändern, da man sonst den konstanten Speicherbereich beschädigt und das Programm somit abstürzen wird. Seien Sie sich dessen immer bewusst und verwenden Sie folgenden Mechanismus, nur im Notfall.

 1
 2
 3
 4
 5
					
const char*	pcstrWert1	= "Hallo Welt";
char*		pstrWert2;
pstrWert2			= const_cast<char*>(pcstrWert1);

printf("%s", pstrWert2);
					

Ausgabe:

Hallo Welt
		

In Zeile 1 erzeuge ich mir also einen konstanten String mit einem konstanten Inhalt. Jene Variable weiße ich dann in Zeile 3, mit Hilfe der "const_case" Funktion, einem Zeiger auf ein dynamischen String zu. Die Syntax sieht ein wenig merkwürdig aus, aber das liegt daran, dass es sich hier um ein Funktionstemplate handelt. Was das genau ist und warum man das so schreiben muss, werde ich im entsprechenden Kapitel erklären. Wichtig im Moment ist nur, dass man den gewünschten Ergebnistyp, in spitze Klammern vor den runden Klammern, angibt. Alle weiteren Typumwandlungen werden auf ähnlichen Funktionstemplates basieren und stehen auch erst ab C++ zur Verfügung.

Der "const cast" kann und wird für ganz üble Hacks benutzt. Es geht nämlich eigentlich weniger darum eine klassische Konstante so zu manipulieren, dass sie nicht mehr konstant ist. Vielmehr hebelt man damit den Mechanismus aus, welcher mit einem konstanten Funktionsparameter erzwungen werden soll. Angenommen Sie übergeben einer Funktion eine Variable als konstante Referenz. Mit dem Modifikator "const", will der Ersteller der Funktion Ihnen zeigen, dass er den Wert nicht manipuliert und normalerweise könnte er es auch nicht. Da es sich aber nicht um eine richtige Konstante handelt und somit eine Änderung theoretisch nicht den Speicher kaputt machen würde, kann man mit dieser Art der Typumwandlung, die Variable wieder beschreibbar machen. Sie merken also, dass man hier eigentlich nur hässliche Sachen machen kann und deswegen Finger weg!

Zum Seitenanfang
Zum Inhaltsverzeichnis

© Copyright by Thomas Weiß, 2009 - 2012