C++ Kurs

Virtuelle Memberfunktionen

Die Themen:

Basisklassenzeiger

Bevor wir auf virtuelle Memberfunktionen eingehen, erinnern wir uns an folgende Aussage aus dem Kapitel über abgeleitete Klassen. Sie spielt bei virtuellen Memberfunktionen die entscheidende Rolle.

HinweisEin Zeiger vom Typ einer 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 von Typ der Basisklasse GBase definiert. Diesem Zeiger können dann laut obiger Aussage sowohl Verweise auf Frame Objekte als auch auf MyFrame Objekte zugewiesen werden.

PgmHeaderclass 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, aber die im Folgenden gemachten Aussagen gelten 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? Erweitern wir dazu das vorherige Beispiel noch etwas. Fügen wir der Basisklasse GBase sowie der davon abgeleiteten Klasse Frame jeweils eine Memberfunktion Draw(...) hinzu.

PgmHeaderclass 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 einem Basisklassenzeiger ein Objekt der abgeleiteten Klasse zugewiesen wurde und über diesen Zeiger die Memberfunktion Draw(...) aufgerufen wird? Da der Zeiger vom Typ der 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 sogenannte virtuelle Memberfunktionen. Wie dies genau vonstattengeht, das ist Thema dieses Kapitels.

Deklaration von virtuellen Memberfunktionen

Um die dynamische Bindung zu ermöglichen, muss die über den Basisklassenzeiger aufzurufende Memberfunktion zumindest in der Basisklasse als virtuelle Memberfunktion deklariert werden. Dies erfolgt durch voranstellen des Schlüsselwortes virtual vor dem Returntyp der Memberfunktion. Eine in der Basisklasse als virtuell deklarierte Memberfunktion ist in allen abgeleiteten Klassen ebenfalls virtuell, d.h. das Schlüsselwort virtual kann in der abgeleiteten Klasse vor der Memberfunktion stehen, ist aber nicht zwingend erforderlich.

Wird dann zur Programmlaufzeit über den Basisklassenzeiger eine als virtual deklarierte Memberfunktion aufgerufen, so wird stets diejenige Memberfunktion aufgerufen, die zu dem im Basisklassenzeiger abgelegten Objekt gehört. Im Beispiel unten wird jetzt also nicht mehr Draw(...) von GBase sondern von Frame aufgerufen.

PgmHeaderclass 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!
}
HinweisEine Klasse mit mindestens einer virtuellen Memberfunktion wird auch als polymorphe Klasse bezeichnet.

Nachfolgend zur Demonstration dieses Verhaltens ein kleines Beispiel.

BeispielBeispiel:

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 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 ist, aber wegen der virtual Deklaration in 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 ebenfalls einen Text ausgegeben hat, ruft sie die Memberfunktion Draw(...) über den erhaltenen Basisklassenzeiger auf.

In main() wird dann ein Basisklassenzeiger pBase definiert, dem ein Verweis auf ein neu angelegtes Frame Objekt zugewiesen wird. Anschließend wird die Funktion DoAnything(...) aufgerufen, die als Parameter diesen 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.

Anschließend 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 selbstverständlich 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.

PgmHeader// 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);
}
Programmausgabedoing anything
Frame Draw()
doing anything
Bar Draw()

Pure virtual Memberfunktionen

Gehen wir noch einen Schritt weiter. Nehmen wir einmal am, wir wollen für ein neues Grafikobjekt eine von GBase abgeleitete Klasse erstellen. Im Eifer des Gefechts wurde aber vergessen, dieser neuen Klasse die 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 das neue Grafikobjekt zu zeichnen ist, würden wir keine oder eine völlig falsche Darstellung erhalten. Zweifelsohne würde dies beim ersten Testlauf auffallen, schöner wäre es jedoch, wenn wir schon beim Übersetzen des Programms einen Hinweis erhalten würden, dass ein wesentlicher Teil in der Klasse fehlt.

Und auch diese Überprüfung kann der Compiler durchführen. Um sicherzustellen, dass alle von einer Basisklasse abgeleiteten Klassen eine bestimmte virtuelle Memberfunktion besitzen, wird innerhalb der Basisklasse die entsprechende Memberfunktion als pure virtual deklariert. Dies wird dadurch erreicht, dass bei der Deklaration der Memberfunktion nach deren Parameterklammer der Zusatz = 0 angehängt wird. Und eine solche pure virtual Memberfunktion darf innerhalb der Basisklasse nur deklariert werden. Im Beispiel ist die Memberfunktion Draw(...) der Klasse GBase als pure virtual Memberfunktion deklariert und damit müssen alle von GBase abgeleiteten Klassen diese Memberfunktion definieren.

PgmHeaderclass 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 dieser 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 aber 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 Parametern ü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 eine explizite Typkonvertierung des Zeigers. Im Beispiel erhält die Memberfunktion Draw(...) der abgeleiteten Klasse Frame nun einen int-Parameter. Wird dann versucht, Draw(...) über einen Basisklassenzeiger aufzurufen, so meldet der Compiler jetzt einen Fehler.

PgmHeaderclass 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

In den bisherigen Beispielen haben wir zwar Objekte dynamisch erstellt, aber diese noch nicht gelöscht. Im nachfolgenden Beispiel 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 wir dieses Programm nun laufen lassen würden, würden wir feststellen, dass der delete Operator den Destruktor der Basisklasse aufruft und nicht den der abgeleiteten Klasse, wie es eigentlich sein sollte.

PgmHeaderclass GBase
{
    ....
  public:
    ~GBase();
};
class Frame: public GBase
{
    ....
  public:
    ~Frame();
};
int main()
{
    GBase *pBase;
    pBase = new Frame(...);
    ....
    delete pBase;
}

Damit delete in diesem Fall den richtigen Destruktor aufruft, muss auch der Destruktor als virtuell deklariert werden. Dabei ist aber zu beachten, dass der Destruktor der abgeleiteten Klassen immer auch den Destruktor der Basisklasse aufruft. Für das Beispiel bedeutet dies, dass zuerst der Destruktor von Frame und dann der von GBase ausgeführt wird. Aber das sollte aus dem Kapitel über Konstruktor und Destruktor ja bereits bekannt sein.

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

Nicht erlaubt ist, einen Konstruktor als virtuelle Memberfunktionen zu deklarieren. Außerdem können virtuelle Memberfunktionen niemals static- oder friend-Memberfunktionen (wird nachher gleich noch behandelt) sein.

override und final

Wird in einer abgeleiteten Klasse eine Basisklassen-Memberfunktion überschrieben, so kann durch Angabe des Schlüsselworts override nach der Parameterklammer bei der Deklaration der überschreibenden Memberfunktion explizit darauf hingewiesen werden.

PgmHeaderclass GBase
{
    ....
  public:
    virtual void Draw();
};
class Frame: public GBase
{
    ....
  public:
    void Draw() override;
};

override bietet keine erweiterte Funktionalität gegenüber dem Überschreiben der Memberfunktion ohne die Angabe von override. Enthält die Basisklasse aber keine entsprechende Memberfunktion, so meldet der Compiler einen Fehler. d.h. override hilft hier etwaige Tippfehler zu erkennen.

Soll eine virtuelle Memberfunktion in weiter abgeleiteten Klassen nicht mehr überschrieben werden können, so kann diese Funktion als final Funktion deklariert werden. Das Schlüsselwort final steht auch hier wieder nach der Parameterklammer der Funktion.

PgmHeader// Basisklasse mit der pure virtuellen Memberfunktion Draw()
class Base
{
  public:
    virtual void Draw() = 0;
};
// Abgeleitete Klasse von Base, ueberschreibt Draw() als final Memberfunktion.
class Circle: public Base
{
  public:
    // Verhindert das weitere Ueberschreiben der Memberfunktion
    void Draw() final
    {
        cout << "circle" << endl;
    }
};
// Abgeleitete Klasse von Circle
// Diese Klasse ist so nicht einsetzbar, da die Basisklasse
// Circle die Memberfunktion Draw() als final Funktion deklariert

class MyCircle: public Circle
{
  public:
    void Draw()
    {
        cout << "mycircle" << endl;
    }
};

Der Einsatz von final sollte immer gründlich geprüft werden, da damit eine weitere, vollständige Ableitung von der Klasse verhindert wird.

HinweisZum Schluss dieses Kapitels 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 ÜbungUnd hier geht's zum Beispiel und zur Übung.