C++-Programmierung: Polymorphie
Eine aktuelle Seite zum gleichen Thema ist unter C++-Programmierung/ Objektorientierte Programmierung verfügbar.
Polymorphie (polymorphism, von griechisch πολυμορφία, „Vielgestaltigkeit“) ist neben Kapselung und Vererbung die dritte Säule der OOP. Alle objektorientierten Sprachen unterstützen dieses Konzept: scheinbar gleichartige Objekte können sich im Verhalten unterscheiden.
Virtuelle Funktionen
[Bearbeiten]Denken Sie an ein Programm, das mit einfachen geometrischen Figuren, z. B. Kreisen und Dreiecken, umgehen soll. Nutzen wir die Möglichkeiten der Vererbung, bieten sich folgende Klassendeklarationen an:
class Figur {
protected:
Punkt mittelpunkt;
Farbe farbe;
public:
void verschiebenUm(Punkt v) { mittelpunkt += v; }
// ...
};
class Dreieck : public Figur {
protected:
Punkt ecke[3];
public:
void verschiebenUm(Punkt v);
// ...
};
class Kreis : public Figur {
protected:
float radius;
public:
// ...
};
Dabei nehmen wir an, dass Klassen Punkt
und Farbe
mit der jeweils erforderlichen Funktionalität zur Verfügung stehen.
Die Klasse Dreieck
kann die geerbte Methode verschiebenUm
überschreiben:
void Dreieck::verschiebenUm(Punkt v) {
Figur::verschiebenUm(v); // Mittelpunkt verschieben
for (int i = 0; i < 3; i++) // Ecken verschieben
ecke[i] += v;
}
Bei der Klasse Kreis
ist nichts weiter zu tun.
So weit so gut. Das kennen Sie ja alles schon... Stellen Sie sich jetzt aber vor, der Benutzer hätte (z. B. im Rahmen einer grafischen Bedienoberfläche: mit der Maus) einige solcher Objekte erzeugt, die unser Programm in einer Liste verwaltet, und wollte nun alle gemeinsam verschieben. Es handelt sich durchwegs um Figur
en, aber zum Verschieben muss je nach exakter Klasse eine unterschiedliche Methode aufgerufen werden. C++ stellt hierfür das Konzept der virtuellen Funktion (virtual function) zur Verfügung. Kennzeichnen Sie die Methode in der Basisklasse mit dem Schlüsselwort virtual
:
class Figur {
// ...
virtual void verschiebenUm(Punkt v) { /* ... */ }
};
so wird die „passende“ Methode nicht von der statischen Typüberprüfung des Compilers festgelegt, sondern erst zur Laufzeit entschieden.
Figur *figur[10]; // Array von 10 Zeigern auf Figur
// ... Figuren (sowohl Kreise als auch Dreiecke) erzeugen, alle haben die Basisklasse "Figur"
Punkt v(5, 3); // Verschiebungsvektor
for (int i = 0; i < 10; i++)
figur[i]->verschiebenUm(v);
Wenn figur[i]
auf einen Kreis zeigt, wird Kreis::verschiebenUm
ausgeführt; zeigt er auf ein Dreieck
, statt dessen Dreieck::verschiebenUm
. Ohne dass Sie irgendeine Bedingung abprüfen oder einen dynamic_cast
probieren müssten! Ebenso einfach wie durchschlagskräftig.
Ein weiterer großer Vorteil: wenn Sie später einmal eine neue Figurenklasse ableiten wollen, etwa Quadrat
, müssen Sie tatsächlich nur die Implementation dieser Klasse neu schreiben. Ihr vorhandener Code, der mit der polymorphen Figur
-Klasse arbeitet, kann bleiben, wie er ist, und wird auch mit der zusätzlichen Klasse funktionieren.
Sie werden jetzt vielleicht fragen, weshalb dieses mächtige Werkzeug – man spricht auch von später Bindung (late binding), weil die Zuordnung von Methodenname zu Implementation erst zur Laufzeit vorgenommen wird – mit einem Schlüsselwort extra „eingeschaltet“ werden muss. Polymorphie verursacht mehr Verwaltungsaufwand für das Laufzeitsystem: Objekte einer polymorphen Klasse müssen einen „Rucksack“ mit Funktionszeigern (die so genannte virtual function table) mit sich herumtragen, und jeder Methodenaufruf erfordert eine zusätzliche Zeigerdereferenzierung.
Rein virtuelle Funktionen
[Bearbeiten]Polymorphie funktioniert nur, wenn die beteiligten Klassen über eine gemeinsame Basisklasse „verwandt“ sind. Was aber, wenn es in dieser Basisklasse keine „natürliche“ Definition der fraglichen Methode gibt? Wir wollen z. B. unser Figurenprogramm um eine Methode zeichnen()
erweitern. Wie ein Kreis oder ein Dreieck auf dem Ausgabegerät gezeichnet werden soll, können wir implementieren. Wie steht es aber mit der allgemeinen abstrakten Klasse Figur
? Ein naiver Ansatz wäre, die Methode überhaupt nichts tun zu lassen:
class Figur {
// ...
public:
virtual void zeichnen() const {}
};
Das funktioniert leidlich, aber es gibt in C++ eine bessere Lösung:
class Figur {
// ...
public:
virtual void zeichnen() const = 0;
};
Das = 0
kennzeichnet die Methode als rein virtuell. Das heißt, Polymorphie funktioniert wie bekannt, aber der Versuch, eine Instanz der Klasse Figur
zu erzeugen, wird vom Compiler abgewiesen. Es können nur Instanzen solcher Klassen erzeugt werden, welche diese „leere“ Definition mit Leben erfüllen. Eine zusätzliche Sicherheit für den Programmierer.
Virtuelle Destruktoren
[Bearbeiten]Wie Sie wissen, bewirkt delete
für eine Instanz auf dem Heap, dass nach Aufruf der jeweiligen Destruktoren der Speicherbereich freigegeben wird. Der Compiler erkennt die Größe dieses Speicherbereichs am Typ der Zeigervariablen. Das Beispiel
kompiliert und läuft zwar ohne Fehler, aber das delete
gibt tatsächlich nur denjenigen Teil des Speicherbereichs frei, der dem Figur
-Teilobjekt entspricht! Ein „Loch“ im Heap ist die Folge, Speicherplatz, den Ihr Programm nicht mehr nutzen kann. Abhilfe: Deklarieren Sie den Destruktor als virtuell
dann wird zur Laufzeit korrekt ermittelt, wieviel Speicher tatsächlich freigegeben werden kann und muss. Das sollten Sie immer machen, wenn eine Klasse gelöscht werden soll (via delete) und auf eine abgeleitete Klasse zeigen kann (und nicht "nur" oder "immer", wenn eine Klasse mindestens eine virtuelle Funktion deklariert) [1].
typeinfo
[Bearbeiten]Mit typeinfo bzw. der Funktion typeid, welche eine Variable vom Typ typeinfo zurückliefert, kann man den Typ eines Objekts ermitteln. Folgendes Beispiel zeigt die Funktionsweise:
#include <iostream>
#include <cstdlib>
#include <vector>
#include <typeinfo>
using namespace std;
class A
{
private:
public:
virtual int getStatus() = 0;
};
class B : public A
{
private:
int status;
public:
B(int status)
{
this->status = status;
}
int getStatus()
{
return this->status;
}
// ... weitere Methoden
};
class C : public A
{
private:
int status;
public:
C(int status)
{
this->status = status;
}
int getStatus()
{
return this->status;
}
// ... weitere Methoden
};
int main(void)
{
vector<A*> vec;
int i;
// A *a = new A(1); // geht nicht da A abstrakt ist
B *b = new B(2);
A *ab = new B(3);
C *c = new C(4);
vec.push_back(b);
vec.push_back(ab);
vec.push_back(c);
for(i=0;i<vec.size();i++)
{
A *tmp = vec.at(i);
cout << "Value: " << tmp->getStatus() << " Klasse: " << typeid(*tmp).name() << endl;
}
delete b;
delete ab;
delete c;
cin.get();
return EXIT_SUCCESS;
}
Die Ausgabe sieht folgendermaßen aus:
Value: 2 Klasse: B Value: 3 Klasse: B Value: 4 Klasse: C