Einführung in C++ Kapitel 10 — Virtuelle Funktionen

Hier betreten wir wieder völliges Neuland mit einer neuen Sprache. Wenn objektorientiertes Programmieren noch (relativ) neu für Dich ist, solltest Du dieses Kapitel sehr sorgfältig durchgehen. Wir haben versucht, die Details dieses neuen und ein wenig einschüchternden Themas klar darzulegen. Solltest Du aber schon Erfahrung im objektorientierten Programmieren besitzen und nur C++ als Programmiersprache erlernen wollen, kannst Du die ersten vier Beispielprogramme getrost überspringen und gleich zum Programm mit dem Namen VIRTL5.CPP gehen.

Ein Ausdruck, der dringend einer Definition bedarf, ist Polymorphismus. Auf deutsch heißt das soviel wie "Vielgestaltigkeit", im Kontext des OOP aber eher "Ähnlichkeit". Objekte sind polymorph, wenn sie sich zwar ähnlich, aber eben doch verschieden sind. Was das genau heißt, werden wir im Laufe dieses Kapitels feststellen.

Wir haben in dieser Einführung schon das Überladen von Operatoren und Funktionen studiert. Das ist in beiden Fällen eine einfache Form von Polymorphismus, da sich eine Einheit auf zwei oder mehrere Dinge bezieht. Die Verwendung von virtuellen Funktionen kann bei gewissen Programmieraufgaben eine große Hilfe sein, wie Du in den nächsten beiden Kapiteln sehen wirst.

Ein einfaches Programm mit Vererbung

Beispielprogramm: VIRTL1.CPP

Das Programm VIRTL1.CPP soll Ausgangspunkt unserer virtuellen Tour sein. Da dieses Programm nichts mit virtuellen Funktionen zu tun hat, mag der Name ein wenig in die Irre führen. Wir haben es so genannt, weil es Teil einer Serie von Programmen ist, die die Verwendung von virtuellen Funktionen illustrieren soll. Das letzte Programm dieses Kapitels wird dann die "richtige" Verwendung von virtuellen Funktionen zeigen.

Das erste Programm ist sehr einfach und, wie Du sicher bemerkt hast, einem Programm, das wir früher in dieser Einführung besprochen haben, ähnlich. Allein, es ist radikal vereinfacht, damit wir uns auf die virtuellen Funktionen konzentrieren können. Die meisten der Funktionen des letzten Kapitels haben wir aus diesem Grund komplett weggelassen, und eine neue Methode haben wir der Elternklasse in Zeile 9 hinzugefügt: Nachricht(). Das Studium dieser Funktion in der Basisklasse und den abgeleiteten Klassen wird uns das gesamte Kapitel hindurch begleiten. Aus diesem Grund gibt es eine Methode mit dem Namen Nachricht() auch in der Klasse Auto und in der neuen Klasse mit dem Namen Boot, die wir in den Zeilen 30 bis 36 definieren.

Dir ist sicherlich aufgefallen, daß die Klasse Laster keine Methode mit dem Namen Message() hat. Das ist natürlich aus Absicht geschehen (haha), um die Verwendung einer virtuellen Methode (oder, wenn Dir das besser gefällt, einer virtuellen Funktion) illustrieren zu können. Da die Methoden der Basisklasse mit dem Schlüsselwort public übernommen werden, ist die Methode mit dem Namen Nachricht() der Basisklasse in der Klasse Laster verfügbar.

Die Methode Nachricht() wurde auch absichtlich so einfach gehalten. Wir sind eben an virtuellen Funktionen interessiert und nicht an Funktionen mit langem, kompliziertem Code.

Einführung in C++ bringt Dich in Partnerschaft mit Amazon.de von Null auf Programmieren in ein paar Klicks.

Zum Beispiel Vererbung:

Objektorientierte Programmierung: Das umfassende Handbuch. Lernen Sie die Prinzipien guter Objektorientierung.
Ungefähr alles nicht nur, das man wissen muss, sondern auch, das man wissen kann, in einem Buch. Dabei ist "Objektorientierte Programmierung" nicht unpraktisch und langweilig, nein: voll mit Beispielen und vor allem verständlichen Erklärungen, macht es einen interessiert — und mit brauchbarer objektorientierung zur besseren Programmiererin.
›› Mehr Softwareentwicklung-Bücher

Das Hauptprogramm ist so einfach wie die Klassen. Wir deklarieren ein Objekt jeder der vier Klassen in den Zeilen 41 bis 44 und rufen die Methode mit dem Namen Nachricht() für jedes Objekt einmal auf. Die Ausgabe auf dem Bildschirm zeigt uns, daß die Methode für jedes Objekt aufgerufen wird mit Ausnahme des Sattelschleppers, dessen Klasse keine Methode diesen Namens hat. Wie wir im letzten Kapitel gesehen haben, wird die Methode mit dem Namen Nachricht() der Elternklasse aufgerufen. Das beweist auch die Ausgabe auf dem Bildschirm, wo für das Objekt mit dem Namen Sattelschlepper "Nachricht vom Vehikel" zu lesen ist.

Die Daten der Objekte sind in diesem Kapitel von keinem Belang, also sind alle standardmäßig private und werden in die abgeleiteten Klassen nicht übernommen. Einiges haben wir dennoch übrig gelassen, damit die Klassen zumindest wie Klassen aussehen.

Wenn Du dieses Programm verstanden hast, kompiliere es und führe es aus.

Das Schlüsselwort "virtual"

Beispielprogramm: VIRTL2.CPP

Beim Durchsehen des nächsten Programmes, VIRTL2.CPP wird Dir eine kleine Änderung in Zeile 9 auffallen. Wir haben der Deklaration der Methode Nachricht() in der Elternklasse das Schlüsselwort virtual hinzugefügt.

Du bist wahrscheinlich ein wenig enttäuscht, wenn Du feststellst, daß sich dieses Programm nicht anders verhält als das letzte. Das ist deshalb der Fall, weil wir direkt mit Objekten arbeiten und virtuelle Methoden nichts mit Objekten zu tun haben, sondern nur mit Zeigern auf Objekt, wie wir gleich sehen werden. In Zeile 50 haben wir noch einen zusätzlichen Kommentar hinzugefügt, der illustriert, daß es nicht möglich ist, die Objekte einander zuzuweisen. Sie sind ja Objekte von verschiedenen Klassen. Wir werden gleich sehen, daß einige Zeigerzuweisungen zwischen Objekten verschiedener Klassen erlaubt sind.

Nachdem Du Dir sicher bist, daß virtuelle Funktionen oder Methoden nichts mit den Objekten direkt zu tun haben, kompiliere das Programm und führe es aus.

Wir verwenden Zeiger auf Objekte

Beispielprogramm: VIRTL3.CPP

Sieh Dir das Programm VIRTL3.CPP an, und Du wirst darin eine Wiederholung des vorigen Beispielprogrammes erkennen, allerdings mit einem anderen Hauptprogramm.

In diesem Programm haben wir das Schlüsselwort virtual wieder aus der Methodendeklaration der Elternklasse in Zeile 9 entfernt. Das Hauptprogramm definiert in den Zeilen 41 bis 44 Zeiger auf die Objekte statt der Objekte selbst. Da wir nur Zeiger auf Objekte definiert haben, müssen wir in den Zeilen 46 bis 53 zunächst die Objekte mit dem Operator new erzeugen, bevor wir sie verwenden können. Beim Ausführen des Programmes stellen wir fest, daß sich durch die Verwendung von Zeigern an dem, was das Programm tut, nichts geändert hat. Es operiert genauso wie das erste Programm in diesem Kapitel. Das sollte Dich aber nicht überraschen, da ein Zeiger auf eine Methode mit einem Objekt genauso arbeiten kann wie wir mit dem Objekt selbst arbeiten können.

Kompiliere dieses Beispielprogramm und führe es aus, bevor Du zum nächsten weitergehst. Dir wird wahrscheinlich wieder (unangenehm) aufgefallen sein, daß wir die Beschaffung der Objekte nicht kontrolliert haben, ja sie nicht einmal gelöscht haben. Wie immer spielt das aber in einem so kleinen Programm keine Rolle, da der Speicher mit der Rückkehr zum Betriebssystem automatisch freigegeben wird.

Wir verwenden einen Zeiger und eine virtuelle Funktion

Beispielprogramm: VIRTL4.CPP

Das Programm mit dem Namen VIRTL4.CPP ist mit dem letzten identisch, nur haben wir in Zeile 9 das Schlüsselwort virtual wieder hinzugefügt.

Ich hoffe, Du bist jetzt nicht ganz am Boden zerstört, wenn Du herausfindest, daß dieses Programm — nun mit dem Schlüsselwort virtual — immer noch dasselbe Ergebnis liefert wie das erste. Wir verwenden einfach Zeiger auf die Objekte, wobei in jedem Fall der Typ des Zeigers mit dem des Objektes, auf das er zeigt, übereinstimmt. Die ersten Veränderungen werden wir im nächsten Beispielprogramm bemerken, harre also aus, wir sind fast am Ziel.

Auch dieses Programm solltest Du kompilieren und ausführen.

Die letzten vier Programme haben gezeigt, was virtuell Funktionen nicht tun. Die nächsten beiden sollen zeigen, was sie tun.

Ein einfacher Zeiger auf die Elternklasse

Beispielprogramm: VIRTL5.CPP

Im Beispielprogramm VIRTL5.CPP verwenden wir schon fast eine virtuelle Methode. Habe noch ein ganz wenig Geduld, wir sind beinahe so weit.

Dieses Beispielprogramm ist eine weiter Variante unseres Programmes ohne das Schlüsselwort virtual, aber mit einem komplett veränderten Hauptprogramm. Hier definieren wir nur einen einzigen Zeiger auf eine Klasse, und diese Klasse ist die Basisklasse unserer Klassenhierarchie. Wir verwenden den Zeiger mit jeder der vier Klassen und achten auf die Ausgabe der Methode Nachricht().

Ein kurzer Exkurs soll uns erklären, warum ein Zeiger, den wir als auf eine spezifische Klasse zeigend deklariert haben, plötzlich auf eine andere zeigen kann. Wenn wir von einem Vehikel sprechen (zugegeben: wir tun das nicht allzu oft), kann es sich dabei um ein Auto, einen Lastwagen, ein Motorrad oder alles mögliche, das der Fortbewegung dient, handeln. Wir beziehen uns auf eine sehr umfassende Form eines Objektes. Wenn wir aber von einem Auto sprechen, meinen wir eben keinen Lastwagen, kein Motorrad oder irgendetwas anderes, diese Möglichkeiten sind ausgeschlossen. Der umfassendere Begriff des Vehikels kann sich also auf viele Arten von Fortbewegungsmitteln beziehen, der spezifischere Terminus des Autos bezieht sich nur auf eine Art von Vehikel, nämlich ein Auto.

Denselben Gedankengang können wir nun auf C++ übertragen. Wenn wir demnach eine Zeiger auf ein Vehikel haben, kann dieser Zeiger auf alle die spezielleren Objekte zeigen, und das ist in der Tat erlaubt in C++. Ebenso können wir aber einen Zeiger auf ein Auto nicht benutzen, um auf eine der anderen Klassen zu zeigen, auch nicht auf die Klasse Vehikel, da der Zeiger der Klasse Auto zu spezifisch und eingeschränkt ist.

Die C++ Zeiger Regel

Die Regel ist die folgende: Ein Zeiger, der per Deklaration auf die Basisklasse zeigt, kann auf ein Objekt einer von der Basisklasse abgeleiteten Klasse zeigen, aber ein Zeiger auf eine abgeleitete Klasse kann nicht auf ein Objekt der Basisklasse oder einer anderen von der Basisklasse abgeleiteten Klasse zeigen. In unserem Programm dürfen wir also einen Zeiger auf Vehikel, die Basisklasse, deklarieren und diesen Zeiger auf Objekte der Basisklasse oder einer der abgeleiteten Klasse zeigen lassen.

Genau das tun wir im Hauptprogramm. Wir definieren einen einzelnen Zeiger, der auf die Klasse Vehikel zeigt und verwenden ihn, um auf Objekte aller Klassen zu zeigen, in derselben Reihenfolge wie in den letzten Beispielprogrammen. In jedem Fall erzeugen wir das Objekt, senden eine Nachricht an die Methode mit dem Namen Nachricht() und löschen das Objekt, bevor wir zur nächsten Klasse weitergehen. Wie Du sicher bemerkt hast, senden wir die Nachricht immer an dieselbe Methode, nämlich die Methode mit dem Namen Nachricht(), die Teil der Basisklasse Vehikel ist. Das ist der Fall, weil der Zeiger mit einer Klasse verbunden ist. Obwohl der Zeiger eigentlich auf Objekte von vier verschiedenen Klassen zeigt, verhält sich das Programm als würde er immer auf ein Objekt der Basisklasse zeigen, da der Zeiger von diesem Typ ist.

Das nächste Programm wird schließlich und endlich etwas tun, was Du in keinem C Programm oder C++ Programm in dieser Einführung gesehen hast. Wenn Du also dieses Programm kompiliert und ausgeführt hast, werden wir uns unserer ersten virtuellen Funktion widmen.

Eine wirkliche virtuelle Funktion

Beispielprogramm: VIRTL6.CPP

Schließlich sind wir doch zu einem Programm mit einer virtuellen Funktion, die sich auch wie eine virtuelle Funktion benimmt und den Polymorphismus illustriert, vorgedrungen. Es ist das Programm mit dem Namen VIRTL6.CPP.

Das Programm ist mit dem letzten identisch, nur haben wir in Zeile 9 das Schlüsselwort virtual wieder eingefügt, um die Methode Nachricht() zu einer virtuellen Methode zu machen. Es genügt, das Schlüsselwort virtual in der Basisklasse zu verwenden, die entsprechende Methode in den abgeleiteten Klassen wird vom System automatisch als virtual deklariert. In diesem Programm verwenden wir wieder den einzelnen Zeiger auf die Basisklasse, um Objekte zu erzeugen, zu verwenden, und zu löschen. Dies tun wir mit demselben Code wie im letzten Programm. Allein durch das Schlüsselwort virtual in Zeile 9 ist dieses Programm aber vom vorigen komplett verschieden.

Da die Methode Nachricht() in der Basisklasse als virtuelle Methode deklariert worden ist, wird jedesmal, wenn wir diese Methode mit einem Zeiger auf die Basisklasse aufrufen, eigentlich die Methode einer der abgeleiteten Klassen ausgeführt. Das ist aber nur der Fall, wenn in der abgeleiteten Klasse eine Methode mit diesem Namen existiert und der Zeiger auf ein Objekt dieser Klasse zeigt. Beim Ausführen des Programmes erhalten wir das gleich Ergebnis wie in den Programmen, in denen wir die Methoden der abgeleiteten Klassen direkt aufgerufen haben. Jetzt aber verwenden wir eine Zeiger auf den Typ der Basisklasse für die Methodenaufrufe.

Einführung in C++ bringt Dich in Partnerschaft mit Amazon.de von Null auf Programmieren in ein paar Klicks.

Zum Beispiel wirklich komplett:

Der C++-Programmierer: C++ lernen - professionell anwenden - Lösungen nutzen
Eines der jedenfalls besseren Bücher zur Einführung in C++ ist umfassend und verständlich, übersichtlich und gelungen. Halbwegs hübsch anzuschauen ist es auch.
›› Mehr C++-Bücher

Obwohl also der Code in den Zeilen 44, 48, 52 und 56 derselbe ist, entscheidet das System anhand des Typs, auf den der Zeiger zeigt, welche Methode ausgeführt wird. Diese Entscheidung fällt nicht beim Kompilieren, sondern erst beim Ausführen des Programmes. Das kann in manchen Programmiersituationenen sehr nützlich sein. Eigentlich passieren hier nur drei verschiedene Aufrufe, da die Klasse Laster keine Methode mit dem Namen Nachricht() hat und das System einfach die Methode der Basisklasse verwendet. Aus diesem Grund muß eine virtuelle Funktion eine Implementation in der Basisklasse haben, die verwendet werden kann, wenn für eine oder mehrere abgeleitete Klassen keine verfügbar ist. Beachte, daß die Nachricht eigentlich an einen Zeiger auf das Objekt gesendet wird, aber das ist eine gewisse Haarspalterei, die uns jetzt nicht allzu wichtig sein sollte.

Es ist vielleicht nicht offensichtlich, Du hast aber wahrscheinlich bemerkt, daß die Struktur der virtuellen Funktion in der Basisklasse und in jeder der abgeleiteten Klassen identisch ist. Der Rückgabetyp und die Anzahl sowie die Typen der Parameter müssen für alle Funktionen identisch sein, da wir ja mit der gleichen Anweisung eine jede der Funktionen aufrufen können.

Ist das wirklich wichtig?

Auf den ersten Blick mag dieses Programm nichts Weltbewegendes tun, virtuelle Funktionen sind aber ein sehr nützliches Konstrukt, wie wir auch im nächsten Kapitel zeigen wollen. Dort werden wir eine Personalliste für eine kleine Firma entwickeln.

Bei der Verwendung des Schlüsselwortes virtual bindet das System die Methodenaufrufe erst zur Laufzeit, wird es nicht verwendet, geschieht dies beim Kompilieren. Im ersteren Fall weiß der Compiler nicht, welche Methode schlußendlich auf die Nachricht reagieren wird, da der Typ des Zeigers beim Kompilieren nicht bekannt ist. Im zweiten Fall entscheidet der Compiler bei der Kompilation welche Methode auf die Nachricht, die er an den Zeiger schickt, antworten wird.

Kompiliere dieses Programm und führe es aus, bevor Du zum nächsten Kapitel weitergehst, wo wir ein praktisches Beispiel für diese Technik geben wollen.

Programmieraufgaben

  1. Ändere VIRTL3.CPP so, daß die Objekte vor dem Programmende gelöscht werden.
  2. Erweitere VIRTL6.CPP um eine Methode Nachricht() für die Klasse Laster und beachte, daß nun diese Methode anstatt der der Elternklasse verwendet wird.

(weiter »)

Einführung in C++ bringt Dich in Partnerschaft mit Amazon.de von Null auf Programmieren in ein paar Klicks.

Zum Beispiel Objektorientierung:

Objektorientierte Programmierung: Das umfassende Handbuch. Lernen Sie die Prinzipien guter Objektorientierung.
Ungefähr alles nicht nur, das man wissen muss, sondern auch, das man wissen kann, in einem Buch. Dabei ist "Objektorientierte Programmierung" nicht unpraktisch und langweilig, nein: voll mit Beispielen und vor allem verständlichen Erklärungen, macht es einen interessiert — und mit brauchbarer objektorientierung zur besseren Programmiererin.
›› Mehr Softwareentwicklung-Bücher

[ und ]