Advanced  Services virtual Back Next Up Home


Virtual bei Destruktoren

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.


Nichtvirtueller Destruktor

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.


Virtueller Destruktor

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.


Die Virtual Method Table

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.


Virtual bei Methoden

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.


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


Virtuelle Methoden

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.

Valid XHTML 1.0 Strict top Back Next Up Home