Virtuelle Methoden

Basisklassenzeiger

Erinnern Sie sich noch an folgende Aussage im Kapitel über abgeleitete Klassen? "Ein Zeiger vom Typ einer Basisklasse kann Zeiger vom Typ einer abgeleiteten Klasse aufnehmen".

Was passiert, wenn ein Basisklassenzeiger auf ein Objekt der abgeleiteten Klasse verweist und über diesen Zeiger eine Methode aufgerufen wird, die sowohl in der Basisklasse wie auch in der abgeleiteten Klasse vorhanden ist? Sehen wir uns folgendes Beispiel an:

// Beispiel ist lauffaehig, hat aber noch einen
// Fehler um den wir uns gleich kuemmern
#include <print>
#include <string>

// Basisklasse
class Base
{
    std::string text{"Base Klasse"};
public:
    void Print() const
    {
        std::println("Ich bin in der {}", text);
    }
};
// Ableitung
class Sub: public Base
{
    std::string text{"Sub Klasse"};
public:
    void Print() const
    {
        std::println("Ich bin in der {}", text);
    }
};

int main()
{
    // Basisklassen-Zeiger definieren
    Base *pObj;
    // Objekt der abgeleiteten Klasse erstellen
    pObj = new Sub;
    // Objekt ausgeben
    pObj->Print();
    // Und Objekt auch wieder loeschen!!
    delete pObj;
}

Ich bin in der Base Klasse

Da der Zeiger vom Typ der Basisklasse ist, wird Print() der Basisklasse aufgerufen. Aber eigentlich sollte eher die Methode Print() von Sub 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 Print() von Base aufgerufen sondern von Sub.

// Beispiel ist lauffaehig, hat aber noch einen
// Fehler um den wir uns gleich kuemmern
#include <print>
#include <string>

// Basisklasse
class Base
{
    std::string text{"Base Klasse"};
public:
    virtual void Print() const
    {
        std::println("Ich bin in der {}", text);
    }
};
// Ableitung
class Sub: public Base
{
    std::string text{"Sub Klasse"};
public:
    virtual void Print() const
    {
        std::println("Ich bin in der {}", text);
    }
};

int main()
{
    // Basisklassen-Zeiger definieren
    Base *pObj;
    // Objekt der abgeleiteten Klasse erstellen
    pObj = new Sub;
    // Objekt ausgeben
    pObj->Print();
    // Und Objekt auch wieder loeschen!!
    delete pObj;
}

Ich bin in der Sub Klasse

Eine Klasse mit mindestens einer virtuellen Methode wird als polymorphe Klasse bezeichnet.

Pure virtual Methode

Angenommen wir wollen eine weitere Klasse von Base ableiten. Im Eifer des Gefechts wurde aber vergessen, dieser neuen Klasse die Methode Print() 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 Print() der Klasse Base als pure virtual Methode deklariert und damit müssen alle von Base abgeleiteten Klassen diese Methode definieren.

// Beispiel ist lauffaehig, hat aber noch einen
// Fehler um den wir uns gleich kuemmern
#include <print>
#include <string>

// Basisklasse
class Base
{
    std::string text{"Base Klasse"};
public:
    virtual void Print() const = 0;
};
// Ableitung
class Sub: public Base
{
    std::string text{"Sub Klasse"};
public:
    virtual void Print() const
    {
        std::println("Ich bin in der {}", text);
    }
};

int main()
{
    // Basisklassen-Zeiger definieren
    Base *pObj;
    // Objekt der abgeleiteten Klasse erstellen
    pObj = new Sub;
    // Objekt ausgeben
    pObj->Print();
    // Und Objekt auch wieder loeschen!!
    delete pObj;
}

Ich bin in der Sub Klasse

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

Virtueller Destruktor

Erinnern Sie sich ebenfalls noch an einen 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.

In den Beispielen in diesem Kapitel wurde einem Basisklassenzeiger ein dynamisch erstelltes Objekt einer abgeleiteten Klasse zugewiesen. Am Ende des Programms wird das Objekt wieder mittels delete gelöscht.

// Beispiel ist lauffaehig, hat aber noch einen
// Fehler um den wir uns gleich kuemmern
#include <print>

// Basisklasse
class Base
{
public:
    Base()
    { std::println(">>> ctor Base"); }
    ~Base()
    { std::println("<<< dtor Base"); }
};
// Ableitung
class Sub: public Base
{
public:
    Sub()
    { std::println(">>> ctor Sub"); }
    ~Sub()
    { std::println("<<< dtor Sub"); }
};
int main()
{
    // Basisklassen-Zeiger definieren
    Base *pObj;
    // Objekt der abgeleiteten Klasse erstellen
    pObj = new Sub;
    // ... Objekt verarbeiten
    // Und Objekt auch wieder loeschen!!
    delete pObj;
}

>>> ctor Base
>>> ctor Sub
<<< dtor Base

Wie an der Ausgabe zu ersehen ist, wird nur der Destruktor der Basisklasse aufgerufen und nicht ebenfalls der Destruktor der abgeleiteten Klasse.

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

// Nun das Beispiel fehlerfrei
#include <print>

// Basisklasse
class Base
{
public:
    Base()
    { std::println(">>> ctor Base"); }
    virtual ~Base()
    { std::println("<<< dtor Base"); }
};
// Ableitung
class Sub: public Base
{
public:
    Sub()
    { std::println(">>> ctor Sub"); }
    virtual ~Sub()
    { std::println("<<< dtor Sub"); }
};

int main()
{
    // Basisklassen-Zeiger definieren
    Base *pObj;
    // Objekt der abgeleiteten Klasse erstellen
    pObj = new Sub;
    // ... Objekt verarbeiten
    // Und Objekt auch wieder loeschen!!
    delete pObj;
}

>>> ctor Base
>>> ctor Sub
<<< dtor Sub
<<< dtor Base

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 (friend wird in einem der nächsten Kapitel behandelt).

Ü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 der Signatur 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 DoAnything() der abgeleiteten Klasse Sub einen int-Parameter. Wird dann versucht, DoAnything() über einen Basisklassenzeiger aufzurufen, meldet der Compiler einen Fehler.

class Base
{
   ...
public:
   virtual void DoAnything() const;
};
class Sub: public Base
{
   ...
public:
   virtual void DoAnything(int val) const;
};
int main()
{
   Base *pBase;
   pBase = new Sub(...);
   pBase->DoAnything(2); // Das geht nicht mehr!
}

override und final

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

#include <print>
#include <string>

// Basisklasse
class Base
{
    std::string text{"Base Klasse"};
public:
    Base() = default;           // Standard-ctor
    virtual ~Base()= default;   // Standard-dtor
    virtual void Print() const = 0;
};
// Ableitung
class Sub: public Base
{
    std::string text{"Sub Klasse"};
public:
    Sub() = default;            // Standard-ctor
    virtual ~Sub() = default;   // Standard-dtor
    virtual void Print() const override
    {
        std::println("Ich bin in der {}", text);
    }
};

int main()
{
    // Basisklassen-Zeiger definieren
    Base *pObj;
    // Objekt der abgeleiteten Klasse erstellen
    pObj = new Sub;
    // Objekt ausgeben
    pObj->Print();
    // Und Objekt auch wieder loeschen!!
    delete pObj;
}

Ich bin in der Sub Klasse

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.

// Ableitung
class Sub: public Base
{
    std::string text{"Sub Klasse"};
public:
    Sub() = default;            // Standard-ctor
    virtual ~Sub() = default;   // Standard-dtor
    virtual void Print() const final
    {
        std::println("Ich bin in der {}", text);
    }
};

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
{
    std::string text{"Klasse Base"};
public:
     virtual const std::string& GetClass() const
     {
         return text;
     }
};
// Abgeleitete Klasse
class Sub: public Base
{
    std::string text{"Klasse Sub"};
public:
     virtual const std::string& GetClass() const
     {
         return text;
     }
};
// Funktion erhaelt Referenz auf Basisklasse als Parameter
void PrintClass(Base& obj)
{
    // Da GetClass() virtuell ist wird die richtige
    // zum Objekt gehoerende Methode aufgerufen
    std::cout << obj.GetClass() << '\n';
    // auto liefert hier den Typ Basisklasse!!
    // Damit wird der copy-ctor der Basisklasse ausgefuehrt
    // und GetClass() der Basisklasse aufgerufen
    auto lclass = obj;
    std::cout << lclass.GetClass() << '\n';
}

int main()
{
    Sub theClass;           // Objekt abgeleitete Klasse
    PrintClass(theClass);   // an Funktion uebergeben
}

Klasse Sub
Klasse Base

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 Text "Klasse Sub" zurückliefert. Anschließend wird in Zeile 32 versucht eine Kopie des übergebenen Objekts zu erstellen. Unglücklicherweise (aber logischerweise) liefert auto hier aber zum Datentyp der Basisklasse und damit wird nur der Teil kopiert, der in der Basisklasse vorhanden ist.

Wenn Sie solche Überraschungen vermeiden wollen, sollte in der Klasse, die 'nur' Basis für davon abgeleiteten Klassen ist, 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.

Übungen

vmeth_01:

Erstellen Sie eine Basisklasse Vehicle, welche die string-Eigenschaften für den Hersteller und das Modell eines Fahrzeugs enthält. Für die Ausgabe dieser Eigenschaften ist eine Methode zu erstellen.

Von dieser Basisklasse sind zwei weitere Klassen abzuleiten: eine für Verbrennerautos und eine für Elektoautos. Der Einfachheit halber enthält die Klasse für die Verbrennerautos nur die Eigenschaft Tankinhalt und die für Elektroautos die Eigenschaft Batteriekapazität. Für beide Klassen ist ebenfalls eine Methode für die Ausgabe der Daten zu implementieren.

Definieren Sie alle drei Klassen in einem Modul. Verhindern Sie, dass von der Klasse Vehicle Objekte definiert werden können.

Folgende Fahrzeuge sind in einem Feld abzulegen:

VW IQ.3 mit 58 kW Batterie
Porsche 911 mit 90 ltr Tank
Renault ZOE mit 52 kW Batterie
BMW M3 mit 59 ltr Tank

Geben Sie in einer Schleife die Fahrzeugdaten aus.

Fahrzeuge:
VW IQ.3, Batteriekapazitaet: 58 kWh
Porsche 911, Tankinhalt: 90 ltr
Renault ZOE, Batteriekapazitaet: 52 kWh
BMW M3, Tankinhalt: 59 ltr

vmeth_02:

Diese Übung eine Abwandlung der Übungen abgel_02 zu den abgeleiteten Klassen.

Zum Abspeichern von zwei verschiedenen Artikel ist eine Anwendung zu erstellen.

Der erste Artikeltyp ist ein Monitor mit den Eigenschaften Artikelnummer, Monitorhersteller und Monitordiagonale.

Der zweite Artikeltyp ist eine Tastatur mit den Eigenschaften Artikelnummer, Hersteller der Tastatur und das Tastaturlayout (deutsch oder englisch).

Die Artikelnummer ist unabhängig vom Artikeltyp soll beim Ablegen eines Artikels fortlaufend inkrementiert werden.

Definieren Sie ein Feld für 3 Artikel, in dem beide Artikeltypen abgelegt werden können. Legen Sie in dem Feld zwei Artikel vom Typ Monitor und einen Artikel vom Typ Tastatur ab.

Die Ausgabe der Artikel soll mithilfe eines Ausgabestreams erfolgen, d.h., der Operator << ist entsprechend zu überladen.

Hinweis: Sie müssen den Operator << für die Basisklasse überladen und dort eine Methode für die Ausgabe aufrufen. friend-Funktionen können nicht virtuell sein.

Geben Sie die Artikel zunächst auf die Standardausgabe aus und legen sie anschließend in einer Datei ab.

Art-Nr.: 1, Monitor: ASUS, Diagonale: 27
Art-Nr.: 2, Tastatur: Microsoft, Typ: deutsch
Art-Nr.: 3, Monitor: Alienware, Diagonale: 32