Virtuelle Methoden

Für die kommenden Beispiele wird angenommen, man habe sich die in Bild 4.1 gezeigte Klassenhierarchie aufgebaut.

Abbildung 4.1: Klassenhierarchie Fahrzeug
\includegraphics[width=12cm]{fahrzeughierarchie.ps}

Ausgehend von einer allgemeinen Klasse Fahrzeug wird relativ grob unterteilt in Radfahrzeug, Kettenfahrzeug, und Wasserfahrzeug.

Diese Klassen werden noch weiter verfeinert; beispielsweise werden Radfahrzeuge in PKW und LKW unterteilt.

Jede Klasse soll die Fähigkeit haben, mit der Methode zeigdich() ihren Inhalt zur Standardausgabe (cout) zu schreiben.

Nach dem bisher gesagten könnte die Hierarchie (in Auszügen) etwa so implementiert werden (es werden nur die Klassen Fahrzeug, Radfahrzeug und PKW gezeigt, sowie ein kleines Testprogramm):

// Time-stamp: "31.10.03 21:09 fahrzeuge0.cpp klaus@wachtler.de"

#include <iostream>
#include <string>

using namespace std;

// Basisklasse für alle Fahrzeuge:
class Fahrzeug
{
protected:
  string     Typbezeichnung;

public:

  // Konstruktor
  Fahrzeug( string Typbezeichnung = "?" )
  {
    this->Typbezeichnung = Typbezeichnung;
  }

  void zeigdich() const
  {
    cout << " Fahrzeug::zeigdich():";
    cout << Typbezeichnung << endl;
  }

};

// Vom Typ Fahrzeug wird Radfahrzeug abgeleitet:
class Radfahrzeug: public Fahrzeug
{
protected:

  int AnzahlRaeder;

public:

  // Konstruktor
  Radfahrzeug( string Typbezeichnung = "?",
               int AnzahlRaeder = 4
            )
    : Fahrzeug( Typbezeichnung )
  {
    this->AnzahlRaeder = AnzahlRaeder;
  }

  void zeigdich() const
  {
    cout << " Radfahrzeug::zeigdich():";
    cout << Typbezeichnung
         << ", "
         << AnzahlRaeder
         << " Räder"
         << endl;
  }

};

// Von Radfahrzeug wird ein PKW abgeleitet:
class PKW: public Radfahrzeug
{
protected:

  int Sitzplaetze;

public:

  // Konstruktor:
  PKW( string Typbezeichnung = "?",
       int AnzahlRaeder = 4,
       int Sitzplaetze = 4
       )
    : Radfahrzeug( Typbezeichnung, AnzahlRaeder )
  {
    this->Sitzplaetze = Sitzplaetze;
  }

  void zeigdich() const
  {
    cout << " PKW::zeigdich():";
    cout << Typbezeichnung
         << ", "
         << AnzahlRaeder
         << " Räder"
         << ", "
         <<  Sitzplaetze
         << " Sitzplätze"
         << endl;
  }
};



// nur zum Probieren:


void tuwas( Fahrzeug f )
{
  f.zeigdich();
}

int main( int nargs, char **args )
{
  Fahrzeug EinHoovercraft( "die gute Wahl, Hoover" );
  Radfahrzeug EinRadlader( "Bagger auf Rädern", 4 );
  PKW Golf( "VW Golf" );
  PKW A6( "Audi A6", 4, 5 );

  cout << "Direkte Ausgabe:" << endl;

  EinHoovercraft.zeigdich();
  EinRadlader.zeigdich();
  Golf.zeigdich();
  A6.zeigdich();

  cout << "Ausgabe mit tuwas():" << endl;

  tuwas( EinHoovercraft );
  tuwas( EinRadlader );
  tuwas( Golf );
  tuwas( A6 );

} /* main( int nargs, char **args ) */

Die Ausgabe davon sieht folgendermaßen aus:

Direkte Ausgabe:
 Fahrzeug::zeigdich():die gute Wahl, Hoover
 Radfahrzeug::zeigdich():Bagger auf Rädern, 4 Räder
 PKW::zeigdich():VW Golf, 4 Räder, 4 Sitzplätze
 PKW::zeigdich():Audi A6, 4 Räder, 5 Sitzplätze
Ausgabe mit tuwas():
 Fahrzeug::zeigdich():die gute Wahl, Hoover
 Fahrzeug::zeigdich():Bagger auf Rädern
 Fahrzeug::zeigdich():VW Golf
 Fahrzeug::zeigdich():Audi A6

In dem Testprogramm werden Variablen von den Typen Fahrzeug, Radfahrzeug und PKW definiert, und ihr Inhalt direkt ausgegeben,, sowie über eine Funktion, die ein Fahrzeug erhält.

Die direkte Ausgabe (beispielsweise A6.zeigdich();) ist befriedigend; es werden alle Elemente ausgegeben:

Die Ausgabe über die Funktion tuwas( Fahrzeug f ) dagegen ist dürftig: es wird immer nur die Bezeichnung ausgegeben; egal, welches Fahrzeug man übergibt.

Das Problem liegt natürlich darin, daß zum Aufruf der Funktion jedes Fahrzeug vom Compiler implizit in ein temporäres Objekt vom Typ Fahrzeug gecastet wird; dabei gehen alle zusätzlichen Elemente von Radfahrzeug und PKW verloren. In der Funktion wird dann für diese temporäre abgemagerte Kopie die Funktion Fahrzeug::zeigdich() aufgerufen.

Wünschenswert wäre es dagegen, wenn die Funktion tuwas( Fahrzeug f ) ein komplettes Fahrzeug ausgeben könnte, ohne die zusätzlichen Elemente eines Objekts einer abgeleiteten Klasse abzustreifen.

Dazu muß man zwei Dinge ändern:

  1. es muß verhindert werden, daß bei der Übergabe eines von Fahrzeug abgeleiteten Objekts die implizite Wandlung in die Basisklasse Fahrzeug stattfindet.

    Dies kann man leicht erreichen, indem man den call by value (Wertübergabe) durch ein call by reference ersetzt. Man muß also entweder eine Referenz des auszugebenden Werts übergeben, oder einen Zeiger darauf; dann wird vom Compiler keine temporäre Kopie vom Typ Fahrzeug angelegt. Vielmehr bleibt das komplette Objekt erhalten.

  2. Man muß dem Compiler mitteilen, daß die in Fahrzeug definierte Funktion zeigdich() nur für diese Klasse gilt, und abgeleitete Klassen diese Funktion überschreiben werden.

Für diesen Hinweis gibt es das Schlüsselwort virtual. Damit muß man in der Basisklasse (und wahlweise in den Ableitungen) alle Methoden deklarieren, die beim Ableiten überschrieben werden sollen:

// Time-stamp: "31.10.03 21:09 fahrzeuge1.cpp klaus@wachtler.de"

#include <iostream>
#include <string>

using namespace std;

// Basisklasse für alle Fahrzeuge:
class Fahrzeug
{
protected:
  string     Typbezeichnung;

public:

  // Konstruktor
  Fahrzeug( string Typbezeichnung = "?" )
  {
    this->Typbezeichnung = Typbezeichnung;
  }

  virtual ~Fahrzeug()
  {
  }

  virtual void zeigdich() const
  {
    cout << " Fahrzeug::zeigdich():";
    cout << Typbezeichnung << endl;
  }

};

// Vom Typ Fahrzeug wird Radfahrzeug abgeleitet:
class Radfahrzeug: public Fahrzeug
{
protected:

  int AnzahlRaeder;

public:

  // Konstruktor
  Radfahrzeug( string Typbezeichnung = "?",
               int AnzahlRaeder = 4
            )
    : Fahrzeug( Typbezeichnung )
  {
    this->AnzahlRaeder = AnzahlRaeder;
  }

  void zeigdich() const
  {
    cout << " Radfahrzeug::zeigdich():";
    cout << Typbezeichnung
         << ", "
         << AnzahlRaeder
         << " Räder"
         << endl;
  }

};

// Von Radfahrzeug wird ein PKW abgeleitet:
class PKW: public Radfahrzeug
{
protected:

  int Sitzplaetze;

public:

  // Konstruktor:
  PKW( string Typbezeichnung = "?",
       int AnzahlRaeder = 4,
       int Sitzplaetze = 4
       )
    : Radfahrzeug( Typbezeichnung, AnzahlRaeder )
  {
    this->Sitzplaetze = Sitzplaetze;
  }

  void zeigdich() const
  {
    cout << " PKW::zeigdich():";
    cout << Typbezeichnung
         << ", "
         << AnzahlRaeder
         << " Räder"
         << ", "
         <<  Sitzplaetze
         << " Sitzplätze"
         << endl;
  }
};



// nur zum Probieren:


void tuwas( const Fahrzeug &f )  // Parameter jetzt als Referenz!
{
  f.zeigdich();
}

int main( int nargs, char **args )
{
  Fahrzeug EinHoovercraft( "die gute Wahl, Hoover" );
  Radfahrzeug EinRadlader( "Bagger auf Rädern", 4 );
  PKW Golf( "VW Golf" );
  PKW A6( "Audi A6", 4, 5 );

  cout << "Direkte Ausgabe:" << endl;

  EinHoovercraft.zeigdich();
  EinRadlader.zeigdich();
  Golf.zeigdich();
  A6.zeigdich();

  cout << "Ausgabe mit tuwas():" << endl;

  tuwas( EinHoovercraft );
  tuwas( EinRadlader );
  tuwas( Golf );
  tuwas( A6 );

} /* main( int nargs, char **args ) */

(Wenn mindestens eine Funktion virtual deklariert ist, dann muß der Destruktor ebenfalls virtual deklariert werden4.4.)

Als Dank dafür kann tuwas( Fahrzeug f ) die jeweils richtige Funktion zeigdich() aufrufen, ohne den genauen Typ zu kennen (dies funktioniert auch, wenn zum Zeitpunkt des Kompilierens von tuwas( Fahrzeug f ) die abgeleiteten Klassen noch gar nicht geschrieben sind!):

Direkte Ausgabe:
 Fahrzeug::zeigdich():die gute Wahl, Hoover
 Radfahrzeug::zeigdich():Bagger auf Rädern, 4 Räder
 PKW::zeigdich():VW Golf, 4 Räder, 4 Sitzplätze
 PKW::zeigdich():Audi A6, 4 Räder, 5 Sitzplätze
Ausgabe mit tuwas():
 Fahrzeug::zeigdich():die gute Wahl, Hoover
 Radfahrzeug::zeigdich():Bagger auf Rädern, 4 Räder
 PKW::zeigdich():VW Golf, 4 Räder, 4 Sitzplätze
 PKW::zeigdich():Audi A6, 4 Räder, 5 Sitzplätze

Weil die Bindung des Aufrufs von zeigdich() an eine Methode des aktuellen Objekts offensichtlich nicht beim Kompilieren stattfindet, sondern erst zur Laufzeit, heißt der Mechanismus late binding oder dynamic binding. Sofern möglich, wird ein C++-Compiler immer early binding (oder static binding) durchführen; weil early binding etwas Rechenzeit kostet. Im Falle von early binding kann der Compiler beim Aufrufer bereits die Adresse der aufzurufenden Funktion einsetzen. Dagegen wird für late binding für jede Klasse ein Feld mit den Adressen der zugehörigen virtuellen Funktionen angelegt, und jedes Objekt kennt dieses Feld, die sogenannte vtable. Bei der Übergabe von Zeigern auf Objekte oder von Objekten per Referenz kann dann über diese vtable die jeweils richtige Methode gefunden werden.

[Strou IV] vertritt übrigens die Ansicht, daß nur solche virtuellen Funktionen Methoden heißen. Das ist Geschmackssache, aber in jedem Fall sollen alle Methoden als virtual deklariert werden, wenn sie in Ableitungen möglicherweise überladen werden.

Abgeleitete Klassen müssen eine virtuelle Methode nicht überschreiben, wenn sie sie entweder gar nicht benötigen, oder mit der ererbten Version zufrieden sind.

AnyWare@Wachtler.de