Aufrufen virtueller Methoden in Konstruktoren für Basisklassen
Nach oben zu C++- und Object Pascal-Modelle
Virtuelle Methoden, die im Rumpf von Delphi-Stil-Basisklassenkonstruktoren (Klassen, die in Object Pascal implementiert sind) aufgerufen werden, werden wie in C++ entsprechend dem Laufzeittyp des Objekts gebunden. Weil C++Builder das Object Pascal-Modell (sofortiges Setzen des Laufzeittyps des Objekts) mit dem C++-Modell (Erstellen der Basisklasse vor dem Erstellen der abgeleiteten Klasse) kombiniert, kann das Aufrufen von virtuellen Methoden aus Basisklassenkonstruktoren für Klassen im Delphi-Stil subtile Nebeneffekte haben. Die Auswirkungen werden im Folgenden beschrieben, sowie in einem Beispiel einer instantiierten Klasse, die von mindestens einer Basisklasse abgeleitet ist, illustriert. In den folgenden Erläuterungen wird die instantiierte Klasse als abgeleitete Klasse bezeichnet.
Inhaltsverzeichnis
Object Pascal-Modell
In Object Pascal können Programmierer das Schlüsselwort inherited verwenden, das den Aufruf von Basisklassenkonstruktoren von jeder Stelle im Rumpf eines abgeleiteten Klassenkonstruktors aus ermöglicht. Wenn die abgeleitete Klasse virtuelle Methoden überschreibt, die vom Erstellen des Objekts oder der Initialisierung von Daten-Membern abhängig sind, kann das folglich vor Aufruf des Basisklassenkonstruktors und der virtuellen Methoden geschehen.
C++-Modell
In der C++-Syntax gibt es das Schlüsselwort inherited nicht, um den Basisklassenkonstruktor jederzeit während der Erstellung der abgeleiteten Klasse aufzurufen. Für das C++-Modell ist die Verwendung von inherited nicht nötig, weil der Laufzeittyp des Objekts der Typ der aktuellen, zu erstellenden Klasse ist und nicht die abgeleitete Klasse. Daher sind die aufgerufenen virtuellen Methoden diejenigen der aktuellen Klasse, nicht der abgeleiteten Klasse. Folglich ist es nicht erforderlich, Daten-Member zu initialisieren oder das abgeleitete Klassenobjekt einzurichten, bevor diese Methoden aufgerufen werden.
C++Builder-Modell
In C++Builder haben die Objekte im Delphi-Stil während der Aufrufe der Klassenkonstruktoren den Laufzeittyp der abgeleiteten Klasse. Daher wird, wenn der Basisklassenkonstruktor eine virtuelle Methode aufruft, die abgeleitete Klassenmethode aufgerufen, sofern die abgeleitete Klasse sie überschreibt. Wenn diese virtuelle Methode von etwas aus der Initialisierungsliste oder dem Rumpf des abgeleiteten Klassenkonstruktor abhängig ist, wird die Methode aufgerufen, bevor dies ausgeführt wird. Ein Beispiel: CreateParams ist eine virtuelle Member-Funktion, die direkt im Konstruktor von TWinControl aufgerufen wird. Wenn Sie eine Klasse von TWinControl ableiten und CreateParams überschreiben, so dass es von etwas in Ihrem Konstruktor abhängig ist, wird dieser Code nach dem Aufruf von CreateParams verarbeitet. Diese Situation gilt für alle abgeleiteten Klassen einer Basis. Ein Beispiel: Eine Klasse C ist von B abgeleitet, die selbst von A abgeleitet ist. Bei Erstellen einer Instanz von C, würde A auch die überschriebene Methode von B aufrufen, wenn B die Methode überschreibt, aber C nicht.
Achten Sie auf virtuelle Methoden, wie CreateParams, die in Konstruktoren nicht direkt, sondern indirekt aufgerufen werden.
Ein Beispiel: Aufrufen virtueller Methoden
Das folgende Beispiel vergleicht C++-Klassen und Klassen im Delphi-Stil, die überschriebene virtuelle Methoden haben. Das Beispiel illustriert, wie Aufrufe dieser virtuellen Methoden aus Basisklassenkonstruktoren in beiden Sprachen gelöst werden. MyBase und MyDerived sind Standard-C++-Klassen. MyRTLBase und MyRTLDerived sind von TObject abgeleitete Klassen im Delphi-Stil. Die virtuelle Methode what_am_I() wird in beiden abgeleiteten Klassen überschrieben, wird aber nur in den Basisklassenkonstruktoren, nicht in den abgeleiteten Klassenkonstruktoren aufgerufen.
# include <sysutils.hpp >
# include <iostream.h >
# include <stdio.h>
// Klassen, nicht im Delphi-Stil
class MyBase {
public:
MyBase() {
what_am_I();
}
virtual void what_am_I() {
cout << "I am a base" << endl;
}
};
class MyDerived : public MyBase {
public:
virtual void what_am_I() {
cout << "I am a derived" << endl;
}
};
// Klassen im Delphi-Stil
class MyRTLBase : public TObject {
public:
__fastcall MyRTLBase() {
what_am_I();
}
virtual void __fastcall what_am_I() {
cout << "I am a base" << endl;
}
};
class MyRTLDerived : public MyRTLBase {
public:
virtual void __fastcall what_am_I() {
cout << "I am a derived" << endl;
}
};
int main(void) {
MyDerived d; // instantiation of the C++ class
MyRTLDerived *pvd = new MyRTLDerived; // instantiation of the Delphi style class
getchar();
return 0;
}
Die Ausgabe dieses Beispiels lautet
I am a base
I am a derived
wegen der Unterschiede in den Laufzeittypen von MyDerived und MyRTLDerived während der Aufrufe ihrer jeweiligen Basisklassenkonstruktoren.
Konstruktorinitialisierung von Daten-Membern für virtuelle Funktionen
Weil Daten-Member in virtuellen Funktionen verwendet werden können, müssen Sie wissen, wann und wie diese initialisiert werden. In Object Pascal werden alle nicht-initialisierten Daten mit Null initialisiert. Dies gilt beispielsweise für Basisklassen, deren Konstruktoren nicht mit inherited aufgerufen werden. Im Standard-C++ gibt es keine Garantie für den Wert von nicht-initialisierten Daten-Membern. Die folgenden Typen von Klassendaten-Membern müssen in der Initialisierungsliste des Klassenkonstruktors initialisiert werden:
- Referenzen
- Daten-Member ohne Standardkonstruktor
Trotzdem ist der Wert dieser Daten-Member oder derjenigen, die im Rumpf des Konstruktors initialisiert werden, undefiniert, wenn die Basisklassenkonstruktoren aufgerufen werden. In C++Builder wird der Speicher für Klassen im Delphi-Stil mit Null initialisiert.
Eine virtuelle Funktion, die von dem Wert von Member-Variablen abhängt, die im Rumpf des Konstruktors oder in der Initialisierungsliste initialisiert werden, könnte sich so verhalten, als ob die Variablen mit Null initialisiert wären. Daher wird der Basisklassenkonstruktor aufgerufen, bevor die Initialisierungsliste verarbeitet wird oder in den Konstruktorrumpf eingetreten wird. Das folgende Beispiel zeigt diesen Sachverhalt:
# include <sysutils.hpp>
class Base : public TObject {
public:
__fastcall Base() {
init();
}
virtual void __fastcall init() {
}
};
class Derived : public Base {
public:
Derived(int nz) : not_zero(nz) {
}
virtual void __fastcall init() {
if (not_zero == 0)
throw Exception("not_zero is zero!");
}
private:
int not_zero;
};
int main(void)
{
Derived *d42 = new Derived(42);
return 0;
}
Dieses Beispiel löst eine Exception im Konstruktor von Base aus. Weil Base vor Derived erstellt wird, und not_zero noch nicht mit dem Wert 42 initialisiert wurde, der an den Konstruktor übergeben wurde. Denken Sie daran, dass Daten-Member Ihrer Klassen im Delphi-Stil nicht initialisiert werden können, bevor deren Basisklassenkonstruktoren aufgerufen werden.