Warum Vererbung allein nicht ausreicht
Auf der vorherigen Seite hast du eine Klassenhierarchie aufgebaut: Eine abgeleitete Klasse erbt Member von ihrer Basis. Aber es gibt einen Haken. Wenn du eine Methode über einen Basisklassen-Zeiger aufrufst, entscheidet C++ anhand des Zeigertyps, welche Funktion ausgeführt wird, nicht anhand des tatsächlichen Objekttyps. Ein Animal*, der eigentlich auf einen Dog zeigt, ruft also trotzdem die Methodenversion von Animal auf.
Das ist fast nie das, was du willst. Meistens hast du eine Sammlung von Basisklassen-Zeigern, von denen jeder auf ein anderes abgeleitetes Objekt zeigt, und du möchtest, dass sich jeder wie er selbst verhält. Virtuelle Funktionen machen genau das möglich.
Das Objekt ist wirklich ein Dog, und trotzdem hat a->speak() Animal::speak() ausgeführt. Da speak nicht virtuell ist, hat der Compiler die Funktion zur Kompilierzeit anhand des statischen Typs Animal* ausgewählt. Genau dieser Fehler ist der Grund, warum es virtuelle Funktionen gibt.
Eine Funktion virtuell machen
Füge das Schlüsselwort virtual zur Methode der Basisklasse hinzu. Jetzt wird der Aufruf zur Laufzeit anhand des tatsächlichen Objekttyps aufgelöst - das ist die dynamische Bindung.
Eine einzige Schleife über Animal*, drei unterschiedliche Verhaltensweisen. Der Basiszeiger „kennt" zur Laufzeit den echten Typ und führt die Bindung entsprechend durch. Dieser eine Mechanismus - eine Schnittstelle, viele Implementierungen - ist genau das, was Polymorphie in C++ bedeutet.
Beachte, dass virtual nur in der Basisdeklaration stehen muss; sobald eine Funktion virtuell ist, bleibt sie in jeder abgeleiteten Klasse automatisch virtuell. Es in der abgeleiteten Klasse erneut zu schreiben, ist optional und redundant.
Verwende immer das Schlüsselwort override
Im obigen Beispiel ist jede abgeleitete Methode mit override markiert. Für die Funktionsfähigkeit des Codes ist das optional, du solltest es aber als Pflicht behandeln. override (C++11) bittet den Compiler zu überprüfen, dass du tatsächlich eine virtuelle Basisfunktion mit passender Signatur überschreibst. Wenn du die Signatur subtil falsch machst, bekommst du einen klaren Fehler statt eines stillen Bugs.
struct Animal {
virtual void speak() const { } // Hinweis: const
};
struct Dog : Animal {
void speak() { } // NICHT const - das ist eine NEUE Funktion, keine Überschreibung!
void speak() override { } // Fehler: 'speak' überschreibt nichts - sagt es dir sofort
};
Ohne override kompiliert das erste speak() problemlos, wird aber nie über einen Animal* aufgerufen, weil seine Signatur von der Basis abweicht (das const fehlt). Du würdest einen ganzen Nachmittag damit verbringen, dich zu fragen, warum deine Überschreibung nichts tut. Mit override erkennt der Compiler die Abweichung auf der Stelle. Füge es jeder überschreibenden Funktion hinzu.
Rein virtuelle Funktionen und abstrakte Klassen
Manchmal hat die Basisklasse keinen sinnvollen Standard - welches Geräusch macht ein allgemeines „Animal"? In diesem Fall deklarierst du die Funktion als rein virtuell, indem du ihr = 0 zuweist. Das lässt sie ohne Rumpf und macht die Klasse zu einer abstrakten Klasse, die nicht für sich allein instanziiert werden kann. Sie existiert nur, um eine Schnittstelle zu definieren, die abgeleitete Klassen erfüllen müssen.
Jede konkrete Unterklasse muss area() implementieren, sonst bleibt auch sie abstrakt. So drückt C++ „Schnittstellen" aus: Eine abstrakte Klasse mit ausschließlich rein virtuellen Funktionen ist das C++-Äquivalent zu einer Schnittstelle in Sprachen wie Java.
Die Regel des virtuellen Destruktors
Das ist der Fallstrick, in den jeder mindestens einmal tappt. Wenn du ein Objekt über einen Basisklassen-Zeiger mit delete löschst, ruft C++ den Destruktor auf, den es findet - und wenn dieser Destruktor nicht virtuell ist, führt es nur den Basis-Destruktor aus. Der abgeleitete Teil wird nie zerstört und leakt alles, was er besaß. Der Standard nennt das undefiniertes Verhalten.
Die Lösung ist ein einziges Wort: Mache den Basis-Destruktor virtual. Dann führt delete p zuerst ~Derived und dann ~Base aus, genau wie es sein soll.
struct Base {
virtual ~Base() { cout << "~Base\n"; } // korrekt
};
// jetzt: ~Derived dann ~Base
Faustregel: Sobald eine Klasse irgendeine virtuelle Funktion hat, gib ihr auch einen virtuellen Destruktor. Wenn eine Klasse als Basisklasse gedacht ist, die über Zeiger verwendet wird, muss ihr Destruktor virtuell sein.
Häufige Fehler und Fallstricke
Noch ein paar Fallen, auf die du achten solltest, sobald du dich mit virtuellen Funktionen wohlfühlst:
Object Slicing (Objektabschneidung). Wenn du ein abgeleitetes Objekt als Wert an eine Basisvariable übergibst oder darin speicherst, wird der abgeleitete Teil „abgeschnitten" und es bleibt ein reines Basisobjekt übrig - die virtuelle Bindung erreicht die Überschreibung nicht mehr. Verwende für Polymorphie immer Zeiger oder Referenzen:
Dog d;
Animal a = d; // ABGESCHNITTEN: a ist jetzt nur noch ein Animal, der Dog-Teil ist weg
a.speak(); // führt Animal::speak aus, obwohl virtuell
Animal& ref = d; // OK: die Referenz behält den echten Typ
ref.speak(); // führt Dog::speak aus
Rufe keine virtuellen Funktionen aus Konstruktoren oder Destruktoren auf. Während der Konstruktion existiert der abgeleitete Teil noch nicht, daher wird ein virtueller Aufruf zur Version der aktuellen Klasse aufgelöst, nicht zur abgeleiteten Überschreibung - selten das, was du beabsichtigst.
Die virtuelle Bindung hat geringe Kosten. Jeder virtuelle Aufruf läuft über eine versteckte Tabelle von Funktionszeigern (die „vtable"), eine Indirektion pro Aufruf. Das ist billig, aber nicht kostenlos, mache eine Funktion also nicht virtuell, wenn du das Überschreiben nicht wirklich brauchst.
Die Basisversion absichtlich aufrufen. Innerhalb einer Überschreibung kannst du die Basisimplementierung weiterhin explizit mit Base::method() aufrufen - nützlich, wenn das abgeleitete Verhalten das der Basis erweitert statt ersetzt.
Als Nächstes: Operatorüberladung
Virtuelle Funktionen erlauben deinen Objekten, ihr Verhalten über eine gemeinsame Schnittstelle anzupassen. Die nächste Seite zeigt, wie du die Operatoren anpasst, die auf deine Objekte wirken: Mit Operatorüberladung kannst du deinen eigenen Typen beibringen, auf +, ==, << und mehr zu reagieren, sodass sich Vector + Vector oder cout << myObject so natürlich liest wie bei eingebauten Typen.
Häufig gestellte Fragen
Was ist eine virtuelle Funktion in C++?
Eine virtuelle Funktion ist eine Member-Funktion, die in einer Basisklasse mit dem Schlüsselwort virtual deklariert wird, sodass C++ beim Aufruf über einen Basisklassen-Zeiger oder eine Basisklassen-Referenz die überschriebene Version der abgeleiteten Klasse statt der Basisversion ausführt. Diese Auswahl zur Laufzeit nennt man dynamische Bindung (dynamic dispatch) und ist die Grundlage der Polymorphie.
Was ist der Unterschied zwischen einer virtuellen und einer rein virtuellen Funktion?
Eine virtuelle Funktion hat einen Rumpf und kann überschrieben werden. Eine rein virtuelle Funktion wird mit = 0 deklariert und hat in der Basisklasse keinen Rumpf - sie zwingt jede konkrete abgeleitete Klasse, eine Implementierung bereitzustellen. Jede Klasse mit mindestens einer rein virtuellen Funktion ist eine abstrakte Klasse und kann nicht instanziiert werden.
Warum braucht eine Basisklasse in C++ einen virtuellen Destruktor?
Wenn du ein abgeleitetes Objekt über einen Basisklassen-Zeiger mit delete löschst und der Basis-Destruktor nicht virtuell ist, läuft nur der Basis-Destruktor - der abgeleitete Teil wird nie aufgeräumt, was Ressourcen leakt und undefiniertes Verhalten ist. Mache den Destruktor jeder Klasse, die polymorph verwendet werden soll, virtual.