C++ Tutorial

Virtuelle Methoden

Basisklassenzeiger

Bevor wir auf die virtuellen Methoden eingehen, erinnern wir uns an folgende Aussage aus dem Kapitel über abgeleitete Klassen: Ein Zeiger vom Typ einer Basisklasse kann Zeiger vom Typ einer abgeleiteten Klasse aufnehmen.

Sehen wir uns zum Einstieg folgendes Beispiel an:

1: class GBase
2: {
3:    ...
4: public:
5:    void Draw() const;
6: };
7: class Frame: public GBase
8: {
9:    ...
10: public:
11:   void Draw() const;
12: };
13: int main()
14: {
15:    GBase *pBase;
16:    pBase = new Frame(...);
17:    pBase->Draw();
18: }

Was passiert, wenn ein Basisklassenzeiger auf ein Objekt der abgeleiteten Klasse verweist und über diesen Zeiger die Methode Draw() aufgerufen wird? Da der Zeiger vom Typ der Basisklasse ist, wird Draw() der Basisklasse aufgerufen. Aber eigentlich sollte eher die Methode Draw() von Frame aufgerufen werden. Und um dies zu erreichen, wird die dynamische Bindung (späte Bindung, dynamic linking oder late binding genannt) mittels virtueller Methoden eingesetzt.

Deklaration einer virtuellen Methode

Um die dynamische Bindung zu ermöglichen, ist die über den Basisklassenzeiger aufzurufende Methode mindestens in der Basisklasse als virtuelle Methode zu deklarieren. Dies erfolgt durch Voranstellen des Schlüsselwortes virtual vor dem Returntyp der Methode. Eine in der Basisklasse als virtuell deklarierte Methode ist in allen abgeleiteten Klassen automatisch ebenfalls virtuell. Das Schlüsselwort virtual kann (und sollte) in der abgeleiteten Klasse ebenfalls bei der Methode angegeben werden; dies ist aber nicht zwingend erforderlich.

Wird dann zur Programmlaufzeit über einen Basisklassenzeiger eine als virtuell deklarierte Methode aufgerufen, wird diejenige Methode aufgerufen, die zu dem Objekt gehört auf das der Basisklassenzeiger verweist. Im Beispiel wird jetzt nicht mehr Draw() von GBase aufgerufen sondern von Frame.

1: class GBase
2: {
3:    ...
4: public:
5:    virtual void Draw() const;
6: };
7: class Frame: public GBase
8: {
9:    ...
10: public:
11:   virtual void Draw() const;
12: };
13: int main()
14: {
15:    GBase *pBase;
16:    pBase = new Frame(...);
17:    pBase->Draw(); // Draw() von Frame!
18: }

Pure virtual Methode

Angenommen wir wollen für ein weiteres Grafikobjekt eine von GBase abgeleitete Klasse erstellen. Im Eifer des Gefechts wurde aber vergessen, dieser neuen Klasse die Methode Draw() hinzuzufügen. Zweifelsohne würde dies beim ersten Testlauf auffallen. Schöner wäre es, wenn wir schon beim Übersetzen des Programms einen Hinweis erhalten würden, dass ein wesentlicher Teil in der neuen Klasse fehlt.

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

1: class GBase
2: {
3:    ...
4: public:
5:    virtual void Draw() const = 0;
6: };
7: class Frame: public GBase
8: {
9:    ...
10: public:
11:   void Draw() const;
12: };
13: void Frame::Draw() const
14: {...}

Klassen mit pure virtual Methoden werden als abstrakte Klassen bezeichnet. Von einer abstrakten Klasse kann kein Objekt definiert werden, da die Methodendefinitionen fehlen.

Überschreiben einer virtuellen Methode

Die dynamische Bindung über eine virtuelle Methode erfolgt nur dann, wenn die Methode in der Basisklasse und in der abgeleiteten Klasse die gleiche Signatur hat. Unterscheidet sich eine Methode in der abgeleiteten Klasse in den Parametern von der virtuellen Methode der Basisklasse, verdeckt sie die virtuelle Methode der Basisklasse. Ein Aufruf der Methode über einen Basisklassenzeiger ist dann nicht mehr möglich, außer durch eine explizite Typkonvertierung des Zeigers. Im Beispiel erhält die Methode Draw() der abgeleiteten Klasse Frame einen int-Parameter. Wird dann versucht, Draw() über einen Basisklassenzeiger aufzurufen, meldet der Compiler einen Fehler.

1: class GBase
2: {
3:    ...
4: public:
5:    virtual void Draw() const;
6: };
7: class Frame: public GBase
8: {
9:    ...
10: public:
11:   virtual void Draw(int val) const;
12: };
13: int main()
14: {
15:    GBase *pBase;
16:    pBase = new Frame(...);
17:    pBase->Draw(2);        // Das geht nicht mehr!
18: }

Virtueller Destruktor

Erinnern Sie sich noch an folgenden Satz aus dem Kapitel über abgeleitete Klassen: "Der Destruktor einer Klasse sollte entweder public und virtuell sein oder aber protected und nicht-virtuell"? Sehen wir uns das "Warum" einmal an.

Im nachfolgenden Beispiel wird zunächst einem Basisklassenzeiger ein dynamisch erstelltes Objekt einer abgeleiteten Klasse zugewiesen. Am Ende des Programms wird das Objekt wieder mittels delete gelöscht. Würden wir dieses Programm laufen lassen, würden wir feststellen, dass der delete Operator den Destruktor der Basisklasse aufruft und nicht den der abgeleiteten Klasse, wie es eigentlich sein sollte.

1: class GBase
2: {
3:    ...
4: public:
5:    ~GBase();
6: };
7: class Frame: public GBase
8: {
9:    ...
10: public:
11:   ~Frame();
12: };
13: int main()
14: {
15:    GBase *pBase;
16:    pBase = new Frame(...);
17:    ...
18:    delete pBase;
19: }

Damit delete in diesem Fall den richtigen Destruktor aufruft, ist der Destruktor als virtuell zu deklarieren.

1: class GBase
2: {
3: ...
4: public:
5: virtual ~GBase();
6: };
7: class Frame: public GBase
8: {
9: ...
10: public:
11: virtual ~Frame();
12: };
13: int main()
14: {
15: GBase *pBase;
16: pBase = new Frame(...);
17: ...
18: delete pBase;
19: }

Wäre dagegen der Destruktor der Basisklasse als protected und nicht-virtuelle Methode definiert, würde der Compiler beim Übersetzen einen Fehler melden, da der Zugriff auf den Destruktor gesperrt ist.

Nicht erlaubt ist, einen Konstruktor als virtuelle Methoden zu deklarieren. Außerdem kann eine virtuelle Methode keine static- oder friend-Methode sein (wird gleich behandelt).

override und final

Durch Angabe des Schlüsselworts override nach der Parameterklammer wird eine virtuelle Methode explizit als überschreibende Methode gekennzeichnet.

1: class GBase
2: {
3:    ...
4: public:
5:   virtual void Draw() const;
6: };
7: class Frame: public GBase
8: {
9:    ...
10: public:
11:   void Draw() const override;
12: };

override stellt sicher, dass die überschreibende virtuelle Methode in der Basisklasse ebenfalls als virtuelle Methode deklariert ist. Ist dies nicht der Fall, meldet der Compiler einen Fehler.

Soll eine virtuelle Methode in weiteren abgeleiteten Klassen nicht mehr überschrieben werden dürfen, ist die Funktion als final-Funktion zu deklarieren. Das Schlüsselwort final steht ebenfalls nach der Parameterklammer der Funktion.

1: // Basisklasse mit der pure virtuellen
2: // Methode Draw()
3: class Base
4: {
5: public:
6:    virtual void Draw() const = 0;
7: };
8: // Abgeleitete Klasse von Base,
9: // ueberschreibt Draw() als final Methode.
10: class Circle: public Base
11: {
12: public:
13:    // Verhindert weiteres Ueberschreiben der Memberfkt
14:    void Draw() const final
15:    {...}
16: };
17: // Abgeleitete Klasse von Circle
18: // Ungültige Ableitung, da die Basisklasse Circle die
19: // Methode Draw() als final Funktion deklariert
20: class MyCircle: public Circle
21: {
22: public:
23:    void Draw() const
24:    {...}
25: };

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

Object slicing

Object slicing tritt auf, wenn ein Objekt einer abgeleiteten Klasse einem Objekt vom Typ der Basisklasse zugewiesen oder in ein solches kopiert wird. Wie der Begriff 'slicing' (=zerschneiden) vermuten lässt, werden dabei nicht alle Eigenschaften übernommen. Da dies auf den ersten Blick manchmal nicht gleich ersichtlich ist, sehen wir uns ein kleines Beispiel dazu an.

#include <iostream>
// Basisklasse
class Base
{
public:
   virtual char GetClass()
   { return 'B'; }
};
// Abgeleitete Klasse
class Derived: public Base
{
public:
   virtual char GetClass() override
   { return 'D'; }
};
// Funktion erhaelt Referenz auf Basisklasse als Parameter
void PrintClass(Base& obj)
{
   std::cout << obj.GetClass() << '\n';
   // Uebergebenes Objekt einem Basisklassen-Objekt
   // zuweisen (Ausfuehrung des copy-ctor!)
   auto lclass = obj;
   std::cout << lclass.GetClass() << '\n';
}
int main()
{
   Derived theClass;       // Objekt abgeleitete Klasse
   PrintClass(theClass);   // an Funktion uebergeben
}

D
B

Der Funktion PrintClass() besitzt einen Parameter vom Typ Referenz auf Basisklasse. Dieser Funktion wird in main() ein Objekt vom Typ der abgeleiteten Klasse übergeben. Zur Kontrolle wird in PrintClass() die Funktion GetClass() des übergebenen Objekts aufgerufen, welche wie erwartet den Buchstaben D zurückliefert. Anschließend wird ein Objekt der Basisklasse erstellt und mit dem Inhalt des übergebenen Objekts initialisiert (Aufruf des Kopierkonstruktors). Wird über dieses Objekt die Funktion GetClass() aufgerufen, wird die Basisklassen-Funktion aufgerufen anstelle der Funktion der abgeleiteten Klasse.

Wenn Sie solche Überraschungen vermeiden wollen, sollte in der Basisklasse der Zuweisungsoperator und der Kopierkonstruktor explizit gelöscht werden.

Base(Base&) = delete;
Base& operator = (const Base&) = delete;

Ebenfalls Obacht geben müssen Sie, wenn Sie Operatoren, insbesondere Vergleichsoperatoren, in der Basisklasse als virtuell definieren. In einem solchen Fall kann es passieren, dass nicht die angedachte Funktion der abgeleiteten Klasse aufgerufen wird, sondern die der Basisklasse.

Zum Schluss dieses Kapitels nochmals der Hinweis, dass virtuelle Methoden nur im Zusammenspiel mit Basisklassenzeiger Sinn machen. In allen anderen Fällen gibt es keinen Unterschied zwischen einer normalen und einer virtuellen Methode. So können virtuelle Methoden auch direkt über ein entsprechendes Objekt aufgerufen werden.


Copyright 2024 © Wolfgang Schröder
E-Mail mit Fragen oder Kommentaren zu dieser Website an: info@cpp-tutor.de
Impressum & Datenschutz