Advanced Services | virtual |
Stehen Klassen in Vererbung so wird ein Objekt von innen nach außen angelegt, d.h. zuserst wird der Konstruktor der Basisklasse gerufen und dann der Reihe nach die anderen Konstruktoren der erbenden Klassen. Das Zerstören der Objekte geschieht dann in der umgekehrten Reihenfolge. Um das zu zeigen betrachtet man die folgende kleine Hierarchie.
In allen Konstruktoren und Destrukturen gibt es eine Konsolmeldung, damit man den Aufbau und die Zerstörung des Objekts verfolgen kann.
class Base { public: Base() { cout << "Constructor Base" << endl; } ~Base() {cout << "Destructor ~Base" << endl; } }; class Child : public Base { public: Child() : Base() { cout << "Constructor Child" << endl; } ~Child() {cout << "Destructor ~Child" << endl;} }; class GrandChild : public Child { public: GrandChild() : Child() { cout << "Constructor GrandChild" << endl; } ~GrandChild() { cout << "Destructor ~GrandChild" << endl; } };
Legt man jetzt ein Objekt vom Typ GrandChild an
void nonvirtualCase1() { GrandChild gc; }
und ruft die Methode nonvirtualCase1() so erhält man folgende Ausgabe.
Constructor Base Constructor Child Constructor GrandChild Destructor ~GrandChild Destructor ~Child Destructor ~Base
Legt man Objekte an, so geschehen diese Vorgänge automatisch. Anders sieht es aus, wenn man ein Objekt über Pointer anspricht.
void nonvirtualCase2() { //cout << "Objekt über Basispointer anlegen" << endl; Base *b = new GrandChild(); delete b; }
Ruft man nun die Methode nonvirtualCase2() so erhält man folgende Ausgabe.
Constructor Base Constructor Child Constructor GrandChild Destructor ~Base
Vom Objekt vom Typ GrandChild wird also nur der innerste Teil abgebaut, der Rest bleibt im Speicher hängen. Hier hilft uns das Schlüsselwort virtual.
Man ergänzt in der Basisklasse das Schlüsselwort virtual in der Signatur des Destruktors.
class VBase
{
public:
VBase() { cout << "Constructor VBase" << endl; }
virtual ~VBase() {cout << "Destructor ~VBase" << endl; }
};
class VChild : public VBase
{
public:
VChild() : VBase() { cout << "Constructor VChild" << endl; }
~VChild() {cout << "Destructor ~VChild" << endl;}
};
class VGrandChild : public VChild
{
public:
VGrandChild() : VChild() { cout << "Constructor VGrandChild" << endl; }
~VGrandChild() { cout << "Destructor ~VGrandChild" << endl; }
};
void virtualCase() { cout << "Objekt über Basispointer anlegen" << endl; VBase *b = new VGrandChild(); delete b; }
Ruft man nun die Methode virtualCase() so erhält man folgende Ausgabe.
Constructor VBase Constructor VChild Constructor VGrandChild Destructor ~VGrandChild Destructor ~VChild Destructor ~VBase
Perfekt, das Objekt wurde vollständig abgebaut und der Speicher ist komplett freigegeben.
Wie funktioniert das? Offensichtlich hat der Basiszeiger jetzt mehr Information als im nichtvirtuellen Fall. Sobald ein Destruktor als virtual deklariert, wird beim Compilieren eine Tabelle angelegt, die Virtual Method Table. In der VMT steht u.a. die Vererbungsreihenfolge der Klassen ab der Klasse, deren Destruktor erstmalig als virtual deklariert wurde. Dadurch kann bei der Zerstörung eines Objektes der Abbau mit dem Aufruf des letzten Destruktors in der Vererbungshierarchie begonnen werden und alle Destruktoren können in der richtigen Reihenfolge abgehandelt werden.
Es reicht virtual einmal zu verwenden. Alle Destruktoren aller Kindklassen sind damit automatisch virtell, d.h. sie werden in die VMT eingetragen.
Nicht nur Destruktoren können virtual sein, auch Methoden. Um die Wirkung besser schätzen zu können hier zunächst wieder ein Beispiel mit nichtvirtuellen Methoden.
Im nichtvirtuellen Fall schaut unsere Klassenhierarchie wie folgt aus.
class Base { public: Base() {} void method() { cout << "method Base" << endl; } }; class Child : public Base { public: Child() : Base() {} void method() { cout << "method Child" << endl; } }; class GrandChild : public Child { public: GrandChild() : Child() {} void method() { cout << "method GrandChild" << endl; } };
Wir untersuchen die folgende Situation.
void nonvirtualCase1() { cout << "Anlegen eines Objekt" << endl; GrandChild gc; Child ch; Base b = gc; b.method(); }
Der Aufruf erzeugt folgende Ausgabe.
method Base
Nun verwenden wir einen Zeiger.
void nonvirtualCase2() { cout << "Objekt über Basispointer anlegen" << endl; Base *b = new GrandChild(); b->method(); delete b; }
Der Aufruf erzeugt folgende Ausgabe.
method Base
In beiden Fällen wird die Methode aus der Basisklasse aufgerufen.
Im virtuellen Fall schaut unsere Klassenhierarchie wie folgt aus.
class VBase
{
public:
VBase() {}
virtual void method() { cout << "method VBase" << endl; }
};
class VChild : public VBase
{
public:
VChild() : VBase() {}
void method() { cout << "method VChild" << endl; }
};
class VGrandChild : public VChild
{
public:
VGrandChild() : VChild() {}
void method() { cout << "method VGrandChild" << endl; }
};
Nur die Methode der Basisklasse wird explizit als virtual deklariert.
Wieder verwenden wir zuerst Objekte.
void virtualCase1() { cout << "Anlegen eines Objekt" << endl; VGrandChild gc; VBase b = gc; b.method(); }
Die Ausgabe.
method VBase
Jetzt verwenden wir einen Zeiger.
void virtualCase2() { //cout << "Objekt über Basispointer anlegen" << endl; VBase *b = new VGrandChild(); b->method(); delete b; }
Der Aufruf ergibt diesmal:
method VGrandChild
Über den Basiszeiger wird tatsächlich die zum Objekt gehörende Methode aufgerufen. Durch das Schlüsselwort virtual wird wiederum eine VMT angelegt. In dieser stehen die virtuelle Methode der Basisklasse und alle Überschreibungen #der Kindklassen. Beim Compilieren wird lediglich untzersucht, ob in der Basisklasse diese Methode existiert. Zur Laufzeit aber wird die VMT herangezogen und die Methode in der Verebungslinie von unten an aujfwärts gesucht. Wird die Methode gefunden, so wird sie aufgerufen. Dieser Vorgang wird manchmal auch late binding genannt. Wie man sieht braucht man late binding Zeiger, ein Basisobjekt hat diese Eigenschaft nicht. nicht.