C++ Kurs

Virtuelle Basisklassen

Die Themen:

Problem bei Mehrfach-Ableitung
Virtuelle Basisklassen
Virtuelle Basisklassen und Konstruktore
Beispiel

Problem bei Mehrfach-Ableitung

Um die Bedeutung von virtuellen Basisklassen zu demonstrieren, sehen Sie sich einmal die nachfolgende Klassenhierarchie an. Oberste Basisklasse ist die Klasse CWinBase. Davon abgeleitet werden die beiden Klassen CFrame (Implementierung eines Fensters mit Rahmen) und die Klasse CMenu (Implementierung eines Fenstermenüs). Aus diesen beiden Klassen wird dann eine neue Klasse CWindow gebildet, die ein Rahmenfenster mit Menü darstellen soll.

Wenn Sie den Kurs bisher aufmerksam durchgearbeitet haben, dann sollten Sie jetzt leichte Zweifel an dieser Klassenhierarchie haben. Da sowohl CFrame wie auch CMenu von CWinBase abgeleitet sind, erben diese beiden Klassen natürlich die Eigenschaften von CWinBase. Wird nun aus CFrame und CMenu eine neue Klasse gebildet, so enthält die neue Klasse die Eigenschaften von CWinBase in zweifacher Ausführung, einmal von CFrame und einmal von CMenu. Und das ist bestimmt nicht das, was mit der Ableitung erreicht werden sollte. Das nachfolgende Bild veranschaulicht diesen Sachenverhalt nochmals.

Virtuelle Basisklassen

Und auch in diesen Fall hilft uns das Schlüsselwort virtual weiter. Wird eine Klasse von mehreren Basisklassen abgeleitet, die wiederum selbst von einer gemeinsamen Basisklasse abgeleitet sind, so kann durch virtuelles Ableiten von der Basisklasse vermieden werden, dass die Eigenschaften der obersten Basisklasse mehrfach weitervererbt werden. In unserem Beispiel müssen also die Klassen CFrame und CMenu virtuell von CWinBase abgeleitet werden.

Virtuelle Basisklassen und Konstruktore

Sehen wir uns dieses virtuelle Ableiten in der Praxis an. Um eine Klasse virtuelle abzuleiten, wird vor dem Zugriffrecht der Ableitung das Schlüsselwort virtual gestellt. Beachten Sie, dass nur CFrame und CMenu virtuell abgeleitet sind und nicht mehr die endgültige Klasse CWindow.


// Oberste Basisklasse
class CWinBase
{....};
// 1. virtuell abgeleitete Klasse
class CFrame: virtual public CWinBase
{....};
// 2. virtuell abgeleitete Klasse
class CMenu: virtual public CWinBase
{....};
// Endgültige Klasse
class CWindow: public CFrame, public CMenu
{....};

Eine Besonderheit beim virtuellen Ableiten müssen Sie jedoch beachten. Bei einer 'normalen' Ableitung ruft der Konstruktor einer abgeleiteten Klassen die Konstruktoren seiner Basisklassen auf.  Sie können Sie sich diesen Sachverhalt jetzt nochmals hier ansehen. Dieser Sachverhalt trifft bei virtuell abgeleiteten Klassen aber nur dann zu, wenn die Basisklassen einen Standard-Konstruktor besitzen. Benötigt der Konstruktor der Basisklasse jedoch Parameter, so ist der Konstruktor der abgeleiteten Klasse selbst dafür verantwortlich, den Konstruktor der Basisklasse aufzurufen. Für unser Beispiel ergibt sich damit der unten dargestellte Konstruktor der Klasse CWindow.


CWindow::CWindow(...):
   CWinBase(...), CFrame(...), CMenu(...)   // Aufrufe der ctors
{
    ....
}

Für die Aufruf-Reihenfolge der Konstruktoren gilt, dass zuerst der Konstruktor der obersten Basisklasse ausgeführt wird und danach die Konstruktoren der nachfolgenden Basisklassen und zwar in der Reihenfolge, in der die Basisklassen bei der Klassendefinition angegeben wurden. Halten Sie diese Reihenfolge auch bei der Initialisiererliste der 'untersten' Klasse ein, da Sie ansonsten je nach Compiler eventuell eine Reihe Warnungen erhalten.

Beispiel

Beispiel:

Im Beispiel werden drei Klassen für Grafikobjekte definiert. Die Klassen CCircle und CBar dienen zur Darstellung eines Kreises bzw. eines Rechtecks. Die beiden Klassen gemeinsamen Eigenschaften sind in deren Basisklasse CGraphic abgelegt; im Beispiel der Einfachheit halber nur die Position.

Aus den Klassen CCircle und CBar wird eine neue Klasse CCBar gebildet, die ein abgerundetes Rechteck mit einer Beschriftung realisiert. Damit die Basisklasse CGraphic in der neuen Klasse CCBar aber nur einmal vorhanden ist, müssen die Klassen CCircle und CBar virtuell von CGraphic abgeleitet werden. Damit muss aber der Konstruktor von CCBar explizit den Konstruktor der obersten Basisklasse CGraphic aufrufen.

In main() wird dann ein Zeigerfeld vom Typ der Basisklasse CGraphic definiert, in dem Zeiger auf die drei verschiedenen Grafikobjekte abgelegt werden. Innerhalb einer Schleife werden dann alle Grafiken ausgegeben und anschließend wieder gelöscht.

--- Definition CBar ---
Konstruktor CGraphic
Konstruktor CBar
--- Definition CCircle ---
Konstruktor CGraphic
Konstruktor CCircle
--- Definition CCBar ---
Konstruktor CGraphic
Konstruktor CCircle
Konstruktor CBar
Konstruktor CCBar
Rechteck-Parameter
Position: 10,10
Grösse : 20,20
Kreis-Parameter
Position: 40,40
Radius : 20
CCBar besteht aus:
Kreis-Parameter
Position: 50,50
Radius : 50
Rechteck-Parameter
Position: 50,50
Grösse : 100,100
Beschriftung: Rechteck mit Text


// Beispiel zu virtuellen Basisklassen

// Zuerst Dateien einbinden
#include <iostream>
#include <string>

using std::cout;
using std::endl;
using std::string;

// Definition der Klasse CGraphic
// CGraphic ist eine abstrakte Klasse da sie die 'pure virtual'
// Memberfunktion Draw() enthält.
// CGraphic speichert die allen Grafikelementen gemeinsamen
// X/Y Koordinaten.
class CGraphic
{
  protected:
    short   nXPos, nYPos;            // Koordinaten der Grafik
    CGraphic(short nX, short nY);    // protected-Konstruktor
  public:
    virtual ~CGraphic()              // virtueller dtor!
    {}
    virtual void Draw() const = 0;
};
// Definition der Memberfunktionen
// Konstruktor
CGraphic::CGraphic(short nX, short nY)
{
    // Koordinaten der Grafik ablegen
    nXPos = nX;
    nYPos = nY;
    cout << "Konstruktor CGraphic\n";
}

// Definition der Klasse CCircle
// CCircle ist abgeleitet von CGraphic und enthält als
// zusätzliches Datum den Radius eines Kreises
class CCircle: virtual public CGraphic
{
    short   nRadius;                 // Kreisradius
  public:
    CCircle (short, short, short);
    void Draw() const;
};
// Definition der Memberfunktionen
// Konstruktor
CCircle::CCircle(short nX, short nY, short nR):
    CGraphic(nX, nY)                 // Konstruktor Basisklasse
{
    // Radis ablegen
    cout << "Konstruktor CCircle\n";
    nRadius = nR;
}
// Gibt die Kreisdaten aus
void CCircle::Draw() const
{
    cout << "Kreis-Parameter\n";
    cout << "Position: " << nXPos << "," << nYPos << endl;
    cout << "Radius : " << nRadius << endl;
}

// Definition der Klasse CBar
// CBar ist abgeleitet von CGraphic und enthält als
// zusätzliches Datum die Breite und Höhe des Rechtecks
class CBar: virtual public CGraphic
{
    short   nWidth, nHeight;         // Grösse des Rechtecks
  public:
    CBar (short, short, short, short);
    void Draw() const;
};
// Definition der Memberfunktionen
// Konstruktor der Klasse CBar
CBar::CBar(short nX, short nY, short nW, short nH):
    CGraphic(nX, nY)                 // Konstruktor Basisklasse
{
    // Ausdehnung ablegen
    cout << "Konstruktor CBar\n";
    nWidth = nW;
    nHeight = nH;
}
// Gibt die Daten des Rechtecks aus
void CBar::Draw() const
{
    cout << "Rechteck-Parameter\n";
    cout << "Position: " << nXPos << "," << nYPos << endl;
    cout << "Grösse : " << nWidth << "," << nHeight << endl;
}

// Definition der Klasse CCBar
// CCBar ist abgeleitet von CCircle und CBar und damit auch
// von CGraphic
class CCBar: public CCircle, public CBar
{
    string sText;
  public:
    CCBar(short, short, short, const char *const);
    void Draw() const;
};
// Definition der Memberfunktionen
// Konstruktor von CCBar
// Beachten Sie die Aufrufe der Konstruktoren der Basisklassen.
// Obwohl CCBar nicht direkt von CGraphic abgeleitet ist muß der
// Konstruktor von CGraphic gesondert aufgerufen werden
CCBar::CCBar(short nX, short nY, short nW, const char *const pszT):
    CGraphic(nX, nY),                // ctor virtuelle Basisklasse
    CCircle(nX, nY, nW>>1),          // ctor Basisklasse
    CBar(nX, nY, nW, nW),            // ctor Basisklasse
    sText(pszT)                      // string initialisieren
{
    cout << "Konstruktor CCBar\n";
}
// Gibt Daten des beschrifteten Rechtecks aus
// Ruft die Memberfunktionen Draw() der Klassen CBar und CCircle auf
// Beachten Sie, daß unbedingt die Angabe des Zugriffsspezifizieres
// beim Aufruf der Memberfunktionen notwendig ist
void CCBar::Draw() const
{
    cout << "CCBar besteht aus:\n";
    CCircle::Draw();
    CBar::Draw();
    cout << "Beschriftung: " << sText << endl;
}

// main() Funktion
int main()
{
    // Feld für 3 Zeiger auf Grafikobjekte definieren
    // Das Feld nimmt Basisklassenzeiger auf!
    CGraphic *pCObject[3];
    // Kreis erstellen und Zeiger ablegen
    cout << "--- Definition CBar ---\n";
    pCObject[0] = new CBar(10,10,20,20);
    // Rechteck erstellen und Zeiger ablegen
    cout << "--- Definition CCircle ---\n";
    pCObject[1] = new CCircle(40,40,20);
    // Beschriftetes Rechteck erstellen und Zeiger ablegen
    cout << "--- Definition CCBar ---\n";
    pCObject[2] = new CCBar(50,50,100,"Rechteck mit Text");

    // Alle Grafiken ausgeben und dann wieder löschen
    for (short iIndex=0; iIndex<3; iIndex++)
    {
        pCObject[iIndex]->Draw();
        delete pCObject[iIndex];
    }
}

Übung:

Auf eine Übung wird an dieser Stelle verzichtet!