Für die kommenden Beispiele wird angenommen, man habe sich die in Bild 4.1 gezeigte Klassenhierarchie aufgebaut.
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:
Fahrzeug::zeigdich()
nur die Bezeichnung ausgegeben
Radfahrzeug::zeigdich()
die Bezeichnung sowie die Anzahl Räder ausgegeben,
PKW::zeigdich()
die Bezeichnung, die Anzahl Räder ausgegeben, sowie die Anzahl der
Sitzplätze 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:
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.
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