Basisklassenzeiger
Deklaration von virtuellen Memberfunktionen
Pure virtual Memberfunktionen
Überschreiben von virtuellen Memberfunktionen
Virtueller Destruktor
Beispiel und Übung
Bevor wir auf die virtuellen Memberfunktionen eingehen, sehen Sie sich nochmals folgenden Sachverhalt an, der in der Lektion über abgeleitete Klassen schon kurz erwähnt wurde. Er spielt bei virtuellen Memberfunktionen die entscheidende Rolle.
|
|
Dieser Sachverhalt ist unten nochmals dargestellt. Dort wird von der Basisklasse GBase zunächst die Klasse Frame und von dieser wiederum die Klasse MyFrame abgeleitet. In main() wird dann ein Zeiger auf die Basisklasse GBase definiert. Diesem Zeiger können dann laut obiger Aussage sowohl Zeiger auf die Klasse Frame wie auch MyFrame zugewiesen werden.
class GBase
{
....
};
class Frame: public GBase
{
....
};
class MyFrame: public Frame
{
....
}
int main()
{
GBase *pBase;
....
pBase = new Frame(...);
....
pBase = new MyFrame(...);
....
}
|
In den weiteren Beispielen wird der Übersichtlichkeit wegen nur noch eine einstufige Ableitung verwendet. Die im Folgenden gemachten Aussagen gelten aber ebenso für mehrstufige Ableitungen.
Soweit, so gut. Vielleicht fragen Sie sich nun, für was das Ganze mit den Basisklassenzeigern eigentlich gut sein soll? Lassen Sie uns dazu das vorherige Beispiel noch etwas erweitern. Fügen wir der Basisklasse GBase sowie der davon abgeleiteten Klasse Frame jeweils eine Memberfunktion Draw(...) hinzu.
class GBase
{
....
public:
void Draw();
};
class Frame: public GBase
{
....
public:
void Draw();
};
int main()
{
GBase *pBase;
pBase = new Frame(...);
pBase->Draw();
}
|
Was passiert aber nun, wenn dem Basisklassenzeiger ein Objekt der abgeleiteten Klasse zugewiesen wurde und dann über diesen Zeiger die Memberfunktion Draw(...) aufgerufen wird? Da der Zeiger vom Typ Basisklasse ist, wird auch Draw(...) der Basisklasse aufgerufen. Aber eigentlich sollte hier wohl doch die Memberfunktion Draw(...) der abgeleiteten Klasse aufgerufen werden. Und dies ist auch möglich! Die Lösung dazu heißt dynamische Bindung (auch späte Bindung, dynamic linking oder late binding genannt) und erfolgt über so genannte virtuelle Memberfunktionen. Wie dies genau geht, das ist Thema dieser Lektion.
Um die dynamische Bindung zu ermöglichen, muss die über einen Basisklassenzeiger aufzurufende Memberfunktion 'nur' als virtuelle Memberfunktion deklariert werden. Dies erfolgt durch voranstellen des Schlüsselwortes virtual vor dem Returntyp der Memberfunktion. Wird dann zur Programmlaufzeit über einen Basisklassenzeiger eine Memberfunktion aufgerufen, die sowohl in der Basisklasse wie auch in abgeleiteten Klasse als virtual deklariert ist, so wird immer diejenige Memberfunktion aufgerufen, die zu dem im Basisklassenzeiger abgelegten Objekt gehört. Im Beispiel unten wird nun also nicht mehr Draw(...) von GBase sondern von Frame aufgerufen. Sie müssen dazu nicht einmal mehr in Frame die Memberfunktion Draw(...) als virtual deklarieren, denn eine einmal in einer Basisklasse als virtuell deklarierte Memberfunktion ist automatisch in allen davon abgeleiteten Klassen ebenfalls virtuell.
class GBase
{
....
public:
virtual void Draw();
};
class Frame: public GBase
{
....
public:
virtual void Draw();
};
int main()
{
GBase *pBase;
pBase = new Frame(...);
pBase->Draw(); // Draw() von Frame!
}
|
|
|
Wie Sie diese 'Zusammenarbeit' von Basisklassenzeiger und virtuellen Memberfunktionen einsetzen können, soll nun anhand eines kleinen Beispiels demonstriert werden.
|
|
doing anything |
// Headerdatei einbinden #include <iostream> using std::cout; using std::endl; // Definition der Basisklasse mit der virtuellen // Memberfunktion Draw(...) class GBase { public: virtual void Draw() const { cout << "GBase Draw()\n"; } }; // Definition der von GBase abgeleiteten Klasse Frame // Draw(...) ist virtuelle Memberfunktion! class Frame: public GBase { public: void Draw() const { cout << "Frame Draw()\n"; } }; // Definition einer weiteren Klasse Bar, ebenfalls abgeleitet // von GBase class Bar: public GBase { public: void Draw() const { cout << "Bar Draw()\n"; } }; // Beliebige normale Funktion die als Parameter // einen Basisklassenzeiger erhält void DoAnything(const GBase *pObj) { cout << "doing anything\n"; // Aufruf der Draw(...) Memberfunktion, die zu dem im // Basisklassenzeiger abgelegten Objekt gehört! pObj->Draw(); } // main() Funktion int main() { // Definition des Basisklassenzeigers GBase *pBase; // Frame Objekt im Basisklassenzeiger ablegen pBase = new Frame; // Funktion mit Basisklassenzeiger aufrufen DoAnything(pBase); //.... hier muss das Frame-Objekt noch gelöscht werden! // Bar Objekt definieren Bar myBar; // Funktion mit Adresse des Bar Objekts aufrufen // Beachten Sie, dass DoAnything(...) einen Basisklassen- // zeiger als Parameter besitzt! DoAnything(&myBar); } |
Gehen wir jetzt einen Schritt weiter. Stellen Sie sich einmal vor, Sie wollen für ein neues Grafikobjekt eine neue, von GBase abgeleitete Klasse erstellen. Im Eifer des Gefechts vergessen Sie aber, der Klasse für Ihr neues Grafikobjekt eine Memberfunktion Draw(...) zum Zeichnen des Objekts hinzuzufügen. In diesem Fall würde über den Basisklassenzeiger die Memberfunktion Draw(...) der Basisklasse aufgerufen werden. Da die Basisklasse aber selbstverständlich nicht wissen kann, wie Ihr neues Grafikobjekt zu zeichnen ist, würden Sie keine oder eine völlig falsche Darstellung erhalten. Zweifelsohne würden Sie dies beim ersten Testlauf bemerken. Schöner wäre es jedoch, wenn Sie schon beim Übersetzen des Programms einen Hinweis erhalten würden, dass ein wesentlicher Teil in Ihrer Klasse fehlt.
Und auch diese Überprüfung kann Ihnen der Compiler abnehmen. Um zu erreichen, dass alle von einer Basisklasse abgeleiteten Klassen eine bestimmte Memberfunktion besitzen müssen, wird innerhalb der Basisklasse die entsprechende Memberfunktion als pure virtual deklariert. Dies wird dadurch erreicht, dass bei der Deklaration der virtuellen Memberfunktion nach deren Parameterklammer der Zusatz = 0 angehängt wird. Eine pure virtual Memberfunktion darf innerhalb der Basisklasse nur deklariert werden, d.h. Sie besitzt niemals einen Funktionsrumpf {....}. Im Beispiel wurde die Memberfunktion Draw(...) der Klasse GBase als pure virtual Memberfunktion deklariert, und damit müssen alle von GBase abgeleiteten Klassen diese Memberfunktion definieren.
class GBase
{
....
public:
virtual void Draw() = 0;
};
class Frame: public GBase
{
....
public:
void Draw();
};
void Frame::Draw()
{
....
}
|
Von einer Klasse, die mindestens eine pure virtual Memberfunktion enthält, kann kein Objekt definiert werden, da ja die Definition der Memberfunktion fehlt. Klassen mit pure virtual Memberfunktionen werden auch als abstrakter Datentyp (ADT) bezeichnet.
Die dynamische Bindung über virtuelle Memberfunktionen erfolgt nur dann, wenn die Memberfunktionen in der Basisklasse und in der abgeleiteten Klasse die gleiche Signatur haben, d.h. sie müssen im Namen und in den Parameter übereinstimmen. Unterscheidet sich eine Memberfunktion in der abgeleiteten Klasse in den Parametern von der virtuellen Memberfunktion der Basisklasse, so verdeckt sie die virtuelle Memberfunktion der Basisklasse. Ein Aufruf der Memberfunktion über einen Basisklassenzeiger ist dann nicht mehr möglich, außer durch explizite Typkonvertierung des Zeigers. Aber das sollten Sie nach Möglichkeit vermeiden! Im Beispiel erhält die Memberfunktion Draw(...) der abgeleiteten Klasse Frame nun einen int-Parameter. Wird dann wie im Beispiel versucht Draw(...) über einen Basisklassenzeiger aufzurufen, so meldet Ihnen der Compiler jetzt einen Fehler.
class GBase
{
....
public:
virtual void Draw();
};
class Frame: public GBase
{
....
public:
virtual void Draw(int val);
};
int main()
{
GBase *pBase;
pBase = new Frame(...);
pBase->Draw(2); // Das geht nicht mehr!
}
|
Vielleicht ist Ihnen bei den bisherigen Beispielen aufgefallen, dass bis jetzt zwar Objekte erstellt aber noch nicht gelöscht wurden. Sehen Sie sich dazu wieder ein Beispiel an. Dort wird zunächst wie gewohnt einem Basisklassenzeiger ein dynamisch erstelltes Objekt einer abgeleiteten Klasse zugewiesen. Am Ende des Programms wird das Objekt dann wieder mittels delete gelöscht. Wenn Sie dieses Programm nun laufen lassen würden, würden Sie feststellen, dass der delete Operator den Destruktor der Basisklasse aufruft und nicht den der abgeleiteten Klasse, wie es eigentlich sein sollte.
class GBase
{
....
public:
~GBase();
};
class Frame: public GBase
{
....
public:
~Frame();
};
int main()
{
GBase *pBase;
pBase = new Frame(...);
....
delete pBase;
}
|
Doch auch hier hilft uns das Schlüsselwort virtual weiter. Deklarieren Sie den Destruktor der Basisklasse einfach als virtuell und schon werden die richtigen Destruktoren in der richtigen Reihenfolge aufgerufen. Sie wissen doch hoffentlich noch, dass bei abgeleiteten Klassen zuerst die Destruktoren der abgeleiteten Klassen ausgeführt werden und erst zum Schluss der Destruktor der Basisklasse. Für das Beispiel bedeutet dies, dass zuerst der Destruktor von Frame und dann der von GBase ausgeführt wird.
class GBase
{
....
public:
virtual ~GBase();
};
class Frame: public GBase
{
....
public:
virtual ~Frame();
};
int main()
{
GBase *pBase;
pBase = new Frame(...);
....
delete pBase;
}
|
Die Angabe des Schlüsselworts virtual beim Destruktor der Klasse Frame ist übrigens optional, da ja alle Memberfunktionen die in der Basisklasse als virtuell deklariert wurden in den abgeleiteten Klassen ebenfalls virtuell sind. Ebenso können Destruktoren auch als pure virtual deklariert werden. In diesem Fall müssen Sie aber, im Gegensatz zu 'normalen' pure virtual Memberfunktionen, den Destruktor in der Basisklasse noch explizit definieren.
Beachten Sie, dass es nicht erlaubt ist, Konstruktore als virtuelle Memberfunktionen zu deklarieren und virtuelle Memberfunktionen niemals static- oder friend-Memberfunktionen (wird nachher gleich noch behandelt) sein können.
|
|
|
|
Rechteck zeichnen: |
// Beispiel zu virtuellen Memberfunktionen // Zuerst Dateien einbinden #include <iostream> #include <string> #include <utility> using std::cout; using std::endl; using std::string; // Definition der Basisklasse Graphic // Graphic enthält die für alle Grafikelemente notwendigen X/Y Koordinaten. class Graphic { short xPos, yPos; // Koordinaten der Grafik public: // ctor Graphic(short x, short y); // virtueller dtor virtual ~Graphic() {} // pure virtual Memberfunktion zum Zeichnen der Grafik virtual void DrawIt() const = 0; // Koodinaten als pair zurückliefern std::pair<short,short> GetCurPos () const; }; // Definition der Memberfunktionen // Konstruktor Graphic::Graphic(short x, short y): xPos(x), yPos(y) { } // Grafikposition zurückliefern std::pair<short,short> Graphic::GetCurPos() const { return std::make_pair(xPos, yPos); } // Definition der Klasse Circle class Circle: public Graphic { short radius; // Kreisradius public: Circle (short, short, short); void DrawIt() const; }; // Definition der Memberfunktionen // Konstruktor der Klasse Circle Circle::Circle(short x, short y, short r): Graphic(x, y), radius(r) { } // Kreis zeichnen void Circle::DrawIt() const { cout << "Kreis zeichnen:\n"; std::pair<short,short> coord = GetCurPos(); cout << "Cursor auf (" << coord.first << "," << coord.second << ") "; cout << "Radius=" << radius << endl; } // Definition der Klasse Bar class Bar: public Graphic { short width; // Breite und Höhe short height; public: Bar (short, short, short, short); void DrawIt() const; }; // Definition der Memberfunktionen // Konstruktor der Klasse Bar Bar::Bar(short x, short y, short w, short h): Graphic(x, y), width(w), height(h) { } // Rechteck zeichnen void Bar::DrawIt() const { cout << "Rechteck zeichnen:\n"; std::pair<short,short> coord = GetCurPos(); cout << "Cursor auf (" << coord.first << "," << coord.second << ") "; cout << "Breite=" << width; cout << " Höhe=" << height << endl; } // Definition der Klasse Text class Text: public Graphic { string sText; // abzulegender Text public: Text (short, short, const char *const); void DrawIt() const; }; // Definition der Memberfunktionen // Konstruktor der Klasse Text Text::Text (short x, short y, const char *const pT): Graphic(x, y), sText(pT) { } // Text ausgeben void Text::DrawIt() const { cout << "Text ausgeben:\n"; std::pair<short,short> coord = GetCurPos(); cout << "Cursor auf (" << coord.first << "," << coord.second << ") "; cout << "Text: \"" << sText << "\"\n"; } // main() Funktion int main() { // Feld für 3 Grafikobjekt definieren Graphic *pObjects[3]; // Nun nacheinander ein Rechteck, einen Kreis und // einen Text im Feld ablegen pObjects[0] = new Bar(10,10,20,20); pObjects[1] = new Circle(40,40,20); pObjects[2] = new Text(100,100,"My Text"); // Alle drei Objekte ausgeben for (short index=0; index<3; index++) { // Objekte ausgeben pObjects[index]->DrawIt(); // und gleich wieder löschen delete pObjects[index]; } } |
Es
ist sind die rechts dargestellten Klassen zu realisieren. Von
einer Klasse CWinBase ist zunächst die Klasse CWindow
und davon wiederum die CButton abzuleiten.
Die Klasse CWinBase soll die grundlegenden Eigenschaften aller Fenster enthalten wie Größe und Position. Für Ausgabe der Eigenschaften des Fensters wird die Memberfunktion Draw(...) verwendet.
Die Klasse CWindow repräsentiert einen bestimmten Fenstertyp. Diese von CWinBase abgeleitete Klasse enthält zusätzlich noch die Eigenschaft Beschriftung für den Fenstertitel. Zur Darstellung des Fensters wird wieder die Memberfunktion Draw(...) verwendet.
Von CWindow abgeleitet ist schließlich die Klasse CButton zur Darstellung eines Buttons. Ein Button ist im Prinzip ein Sonderfall eines Fensters, welches beim Anklicken irgend welche Aktionen auslöst. Zur Unterscheidung der Buttons erhält jeder Button seine eigene Nummer, die Button-ID. Und auch hier wird ebenfalls eine Memberfunktion Draw(...) eingesetzt um den Button-Eigenschaften auszugeben.
Beachten Sie bitte, dass im Bild rechts nicht alle notwendigen Memberfunktionen angegeben sind. So sind eventuelle Konstruktore und Destruktoren noch selbstständig zu implementieren.
Definieren Sie dann in main() ein entsprechendes Feld für die Aufnahme von Zeiger auf Objekte vom Typ CWindow und CButton. Erstellen Sie dann von CWindow und CButton dynamisch Objekte und legen deren Zeiger im erwähnten Feld ab. Geben Sie die Eigenschaften beider Objekte dann aus.
|
Daten des CWindow-Objekts: Daten des CButton-Objekts: |