C++ OOP
Polymorphie (griechisch "Vielgestaltigkeit") ist einer der wichtigen Bestandteile der OOP.
Die Polymorphie macht es möglich, Objekte verschiedener Klassen, die von einer Basisklasse abgeleitet sind,
über den gleichen Funktionsaufruf anzusprechen. Das Programm erkennt während der Laufzeit mit Hilfe sog.
virtual funcion table (oder virtual method table) selbständig,
welche Methode für das jeweilige Objekt aufzurufen ist.
Dieser Artikel zeigt, wie mit virtuellen Funktionen (und rein virtuellen Funktionen),
sowie virtuellen Destruktoren umgegangen wird und man diese nutzen kann.
Als Beispiel wird ein Zoo modelliert, der verschiedene Tiere aufnehmen kann.
Dabei können die Tiere wiederum selbst Tiere in Form von Flöhen in beliebiger Anzahl aufnehmen.
Neben lästigen Flöhen besitzen die Tiere noch die Fähigkeit, verschiedene Laute von sich zu geben.
Als erstes definieren wir die Basisklasse tier für alle Tiere:
// tier.h #include <iostream> using namespace std; class tier { public: tier() {} virtual ~tier() {} // virtueller Destruktor virtual void GibLaut() = 0; // rein virtuelle Funktion };
Der Destruktor wird virtuell deklariert. Wieso dieser virtuell sein sollte, wird später erläutert.
Die Methode GibLaut() der Klasse tier ist als rein virtuell deklariert.
Diese Form der Deklaration bewirkt, dass für die Funktion keine Definition angegeben werden muss.
Sie dient in der Klasse tier nur als Platzhalter und gibt den von tier erbenden Klassen vor,
eine Funktion void GibLaut() zu definieren. Klassen, die rein virtuelle Methoden haben,
bezeichnet man als abstrakte Klassen. Da sie Funktionen enthalten, die nicht definiert sind,
kann von ihnen kein Objekt erzeugt werden.
Im Beispiel ist GibLaut() rein virtuell, da für Tiere kein allgemein gültiges Geräusch definiert werden kann.
Da aber jedes Tier in unserem Zoo etwas von sich geben soll, deklarieren wir den Platzhalter in der Basisklasse.
Das Schlüsselwort virtual bewirkt weiterhin, dass für alle Tiere immer die passende GibLaut()
Methode aufgerufen wird.
Als erstes richtiges Tier definieren wir einen Floh. Flöhe sollen in unserem Beispiel eigentlich nichts können,
sie sollen nur existieren, und zwar auf anderen Tieren. Da mir die genauen Laute von Flöhen derzeit unbekannt sind,
wird die GibLaut() Methode mit nichts definiert.
class floh : public tier { public: floh() {cout << "Ein neuer Floh!" << endl;} ~floh() {cout << "Floh stirbt!" << endl;} void GibLaut() {} };
Als nächstes definieren wir noch einen Hund und eine Katze. Die beiden Tiere unterscheiden sich nur in dem Geräusch, das sie machen.
class hund : public tier { public: hund(long floehe) { cout << "Ein neuer Hund!" << endl; m_floehe = new floh[floehe]; } ~hund() { cout << "Hund stirbt!" << endl; delete [] m_floehe; } void GibLaut() { cout << "Wuff Wuff!" << endl;} private: floh * m_floehe; }; class katze : public tier { public: katze(long floehe) { cout << "Eine neue Katze!" << endl; m_floehe = new floh[floehe]; } ~katze() { cout << "Katze stirbt!" << endl; delete [] m_floehe; } void GibLaut() {cout << "Miau Miau!" << endl ;} private: floh * m_floehe; };
Den Konstruktoren von Hund & Katz wird ein long als beliebige Anzahl von Flöhen übergeben. Mittels new Operator wird Speicher für diese reserviert. Der Rückgabewert wird in einem privaten Floh Zeiger gespeichert. Was einmal mit new erzeugt wurde, muss irgendwann durch delete wieder gelöscht werden. Dies geschieht im Destruktor von Hunden und Katzen. An dieser Stelle wird deutlich, wieso der Destruktor in tier virtuell sein muss. Das Schlüsselwort virtuell bewirkt bei Destruktoren genau wie bei den anderen Methoden, dass nach Typumwandlungen die passende Funktion bzw. der passende Destruktor aufgerufen wird. Da nicht jedes Tier Flöhe hat, hat es keinen Sinn in der Klassen tier Flöhe zu erzeugen und zu löschen. So wäre es zum Beispiel sinnlos, im Konstruktor von Flöhen, Flöhe zu erzeugen bzw. im Destruktor zu löschen. Da Hunde und Katzen aber Flöhe besitzen können, ist es wichtig, dass bei ihrem Ableben ihr eigener Destruktor aufgerufen wird, um in ihm die erzeugten Flöhe wieder zu löschen. Wäre der Destruktor in tier nicht virtuell, würde in unserer Anwendung beim Löschen von Hunden oder Katzen nur der Destruktor von tier aufgerufen. Die Zeile
delete [] pFloehe;
würde niemals aufgerufen werden,
die Folge wäre ein Speicherleck.
Zuletzt werden noch die GibLaut() Methoden mit artspezifischen Geräuschen überschrieben.
Wer sich in der Tierwelt gut auskennt, kann hier noch beliebig viele weitere Tierarten von tier ableiten.
Wir belassen es an dieser Stelle jedoch bei Hunden und Katzen und kommen nun zur eigentlichen Anwendung:
int main(int argc, char **argv) { typedef std::vector<tier*> zoo; cout << "Hallo Tierwelt!" << endl; zoo myzoo; myzoo.push_back(new hund(3)); myzoo.push_back(new katze(2)); for(zoo::iterator it = myzoo.begin(); it != myzoo.end(); ++it){ (*it)->GibLaut(); delete (*it); } return (0); }
Wir starten ins Programm mit einem fröhlichen Gruß an die Tierwelt. Unseren Zoo modellieren wir als vector, der Zeiger auf Tiere speichert. Mittels new werden Hunde und Katzen erzeugt und der zurückgegebene Zeiger im vector abgelegt. Man beachte, dass Zeiger auf Hunde und Katzen hier in einem tier * gespeichert werden! Die for-Schleife iteriert durch den gesamten Zoo und lässt jedes Tier einmal Laut geben, bevor selbiges durch delete ins Jenseits geschickt wird. Die Ausgabe sieht folgendermaßen aus:
Hallo Tierwelt! Ein neuer Hund! Ein neuer Floh! Ein neuer Floh! Ein neuer Floh! Eine neue Katze! Ein neuer Floh! Ein neuer Floh! Wuff Wuff! Hund stirbt! Floh stirbt! Floh stirbt! Floh stirbt! Miau Miau! Floh stirbt! Floh stirbt!
Wie man sieht, wird die korrekte Anzahl von Flöhen erzeugt und gelöscht. Zudem hört man verschiedene Tiergeräusche,
obwohl alle Funktionen mit dem gleichen Zeigertyp aufgerufen wurden.
Die Polymorphie gibt uns die Möglichkeit, verschiedene Objekt gleich zu behandeln und trotzdem das richtige Ergebnis zu erhalten.
Wie eingangs erwähnt wurde, wählt das Programm zur Laufzeit die passenden Funktionen aus einer Tabelle aus.
Wer sich für die dadurch entstehenden Kosten interessiert, sei auf diese Untersuchung [1] verwiesen:
Sende ein Kommentar, Frage, Korrekturen, Beschimpfungen...
24.06.2010 :
Ergänzung zum virtuellen Destruktor der Basisklasse: Die Destruktor der abgeleitete Klassen wird
nur dann nicht aufgerufen, wenn 1) der Basisklassen Destruktor nicht als virtuell deklariert wurde
UND 2) die abgeleitete Klasse über die Basisklasse gelöscht wird:
tier = new katze(5); delete tier; // der Destruktor von katze wird nicht aufgerufen
katze = new katze(5) delete katze; // jetzt auch wenn die Basisklasse nicht
// virtuellen Destruktor hat.
Gruß Thomas
doxapp c++ Zur Übersicht home