C++ Kurs

Virtuelle Memberfunktionen

Die Themen:

Basisklassenzeiger
Deklaration von virtuellen Memberfunktionen
Pure virtual Memberfunktionen
Überschreiben von virtuellen Memberfunktionen
Virtueller Destruktor
Beispiel und Übung

Basisklassenzeiger

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.

Ein Zeiger vom Typ der Basisklasse kann auch Zeiger vom Typ einer abgeleiteten Klasse aufnehmen.

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.

Deklaration von virtuellen Memberfunktionen

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!
}

Eine Klasse mit mindestens einer virtuellen Memberfunktion wird auch als polymorphe Klasse bezeichnet.

Wie Sie diese 'Zusammenarbeit' von Basisklassenzeiger und virtuellen Memberfunktionen einsetzen können, soll nun anhand eines kleinen Beispiels demonstriert werden.

Beispiel:

Im Beispiel finden Sie wieder die inzwischen bekannte Basisklasse GBase. Sie enthält der Einfachheit halber nur die Memberfunktion Draw(...), die jetzt als virtuelle Memberfunktion deklariert ist, und nur einen kurzen Text ausgibt.

Von dieser Basisklasse sind zwei weitere Klasse Frame und Bar abgeleitet. Auch diese Klassen enthalten jeweils eine Memberfunktion Draw(...), die aber einen anders lautenden Text ausgeben. Beachten Sie, dass Draw(...) in diesen Klassen nicht explizit als virtuelle Memberfunktion deklariert wird, aber wegen der virtual Deklaration innerhalb der Basisklasse ebenfalls virtuell ist.

Nach den Klassendefinitionen erfolgt die Definition der Funktion DoAnything(...). Diese Funktion erhält als Parameter einen Zeiger vom Typ Basisklasse GBase. Nachdem die Funktion einen Text ausgegeben hat, ruft sie die Memberfunktion Draw(...) über den erhaltenen Basisklassenzeiger auf.

In main() wird dann ein Basisklassenzeiger pBase definiert, indem ein Zeiger auf ein Frame Objekt abgelegt wird. Anschließend wird die Funktion DoAnything(...) aufgerufen, die als Parameter den Basisklassenzeiger pBase erhält. Beachten Sie, dass der Basisklassenzeiger auf ein Objekt vom Typ Frame zeigt.

Nach dem DoAnything(...) einen Text ausgegeben hat, wird über den Basisklassenzeiger die Memberfunktion Draw(...) aufgerufen. Da Draw(...) in der Basisklasse als virtuell deklariert ist, wird jetzt diejenige Draw(...) Memberfunktion aufgerufen, die zu dem im Zeiger abgelegten Objekt gehört, in diesem Fall also Draw(...) von Frame.

Dann wird in main() ein Objekt vom Typ Bar definiert und dessen Adresse an DoAnything(...) übergeben. Da DoAnything(...) einen Basisklassenzeiger GBase als Parameter erwartet, kann an diese Funktion auch ein Zeiger auf ein Objekt einer von der Basisklasse abgeleitete Klasse übergeben werden. Und auch hier ruft die Funktion dann letztendlich die richtige Draw(...) Memberfunktion von Bar auf.

doing anything
Frame Draw()
doing anything
Bar Draw()


// 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);
}

Pure virtual Memberfunktionen

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.

Überschreiben von virtuellen Memberfunktionen

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!
}

Virtueller Destruktor

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.

Zum Schluss dieser Lektion nochmals der Hinweis, dass virtuelle Memberfunktionen nur im Zusammenspiel mit Basisklassenzeigern Sinn machen. In allen anderen Fällen gibt es keinen Unterschied zwischen einer 'normalen' und einer virtuellen Memberfunktion. So können Sie z.B. auch virtuelle Memberfunktionen direkt über das entsprechende Objekt aufrufen.

Beispiel und Übung

Beispiel:

Das Beispiel enthält die vollständigen Klassen der in dieser Lektion behandelten Grafikelemente. Die Basisklasse für alle Grafikobjekte bildet die Klasse Graphic. Diese Klasse enthält u.a. einen virtuellen Destruktor und die pure virtual Memberfunktion Draw(...). Dadurch, dass Draw(...) als pure virtual deklariert ist, müssen alle abgeleiteten Klassen diese Memberfunktion implementieren um ihre Grafik darzustellen.

Von Graphic werden dann die Klassen Circle, Bar und Text abgeleitet.

In main() wird ein Zeigerfeld vom Typ der Basisklasse Graphic definiert, welches Zeiger auf die drei verschiedenen Grafikobjekte aufnimmt. Innerhalb einer Schleife werden dann alle Grafiken ausgegeben und danach wieder gelöscht.

Rechteck zeichnen:
Cursor auf (10,10) Breite=20 Höhe=20
Kreis zeichnen: Cursor auf (40,40) Radius=20
Text ausgeben: Cursor auf (100,100) Text: "My Text"


// 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];
    }
}

Übung:

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:
Position: (10,10)
Grösse : (200,100)
Beschriftung: Fensterobjekt

Daten des CButton-Objekts:
Position: (50,50)
Grösse : (100,50)
Beschriftung: Buttonobjekt
Button-ID: 10

Lösung ansehen!