Einführung in C++ Kapitel 11 — Mehr über virtuelle Funktionen

Dieses Kapitel ist eine Fortsetzung des im letzten Kapitel behandelten Themas, soll aber eine komplettere Erklärung, was virtuelle Funktionen sind und wie sie in einem Programm verwendet werden können, liefern. Wir werden eine einfache Datenbank mit einer virtuellen Funktion vorstellen, um die Verwendung zu illustrieren. Dann werden wir eine etwas komplexere Verwendung von virtuellen Funktionen zeigen, die deren Leistungsnachweis und Daseinsberechtigung erbringen soll. :)

Wie beginne ich ein objektorientiertes Programmier-Projekt?

Unser objektorientiertes Abenteuer beginnen wir damit, daß wir ein Objekt finden, oder, wie in diesem Fall, eine Klasse von Objekten und sogar einige untergeordnete Objekte, und diese vollständig definieren. Beim Hauptprogramm angelangt, haben wir dann einfaches Spiel mit dem restlichen notwendigen Code, den wir mit den bekannten prozeduralen Techniken schreiben. So fängt also alles an: Wir suchen uns Objekte, die sich vom übrigen Code sinnvoll trennen lassen, programmieren diese und schreiben dann das Hauptprogramm. Wir sollten noch erwähnen, daß Du gerade am Anfang nicht versuchen solltest, aus allem (und jedem???) mit aller Gewalt ein Objekt zu machen. Wähle Dir ein paar Objekte aus; wenn Du dann Erfahrung mit objektorientierten Programmiertechniken gesammelt hast, wirst Du in Deinen späteren Projekten mehr Objekte verwenden. Die meisten Programmiererinnen verwenden in ihrem ersten Projekt zu viele Objekte und schreiben eigenartigen, unleserlichen Code.

Die Person-header-Datei

Beispielprogramm: PERSON.H

Die Datei PERSON.H definiert die Klasse Person. Es findet sich hier nichts Neues, Du solltest also mit dem Verständnis keinerlei Problem haben. Das einzig Erwähnenswerte an dieser Klasse ist, daß wir die Variablen als protected deklarieren, womit sie in den von dieser abgeleiteten Klassen verfügbar sind. Beachte auch, daß die einzige Methode dieser Klasse in Zeile 11 virtuell ist.

Die Person-Implementation

Beispielprogramm: PERSON.CPP

In der Datei PERSON.CPP findet sich die Implementation der Klasse Person und die ist ein wenig eigenartig. Es ist vorgesehen, daß die virtuelle Methode mit dem Namen Zeige() in dieser Datei nie verwendet wird, der C++ Compiler verlangt sie aber, damit er auf sie zurückgreifen kann, wenn eine Subklasse keine Methode mit diesem Namen bereitstellt. Wir werden uns hüten, diese Funktion im Hauptprogramm aufzurufen. Merke Dir aber, daß C++ eine Implementation für alle virtuellen Funktionen verlangt, auch wenn diese auch nie verwendet werden. In unserem Fall geben wir offensichtlich eine Fehlermeldung aus.

Kompiliere dieses Programm, bevor Du zur nächsten Klassendefinition weitergehst.

Die Aufseherin-header-Datei

Beispielprogramm: AUFSHR.H

In der Datei AUFSHR.H findest Du die Definition der drei abgeleiteten Klassen, Aufseherin, Programmiererin und Sekretaer. Aus zwei Gründen sind sie alle in einer Datei. Erstens wollen wir bewiesen haben, daß das funktioniert und zweitens können wir so einige Klassen kombinieren und Du mußt nicht so viel kompilieren. Es macht auch Sinn, diese Klassen zusammenzufassen, da sie alle von einer gemeinsamen Elternklasse abgeleitet sind.

Alle drei Klassen haben eine Methode mit dem Namen Zeige() und alle diese Methoden haben wiederum den Rückgabetyp void und dieselbe Anzahl an Parametern wie die gleichnamige Methode der Elternklasse. Diese Ähnlichkeiten sind notwendig, da alle diese Methoden mit demselben Aufruf aufgerufen werden können. Auch die andere Methode der drei abgeleiteten Klassen trägt überall denselben Namen, die Parameter sind aber in Anzahl und Typen verschieden. Deshalb können wir diese Methode nicht als virtuelle Methode verwenden.

Der Rest dieser Datei ist einfach.

Die Aufseherin-Implementation

Beispielprogramm: AUFSHR.CPP

In der Datei AUFSHR.CPP implementieren wir die drei Klassen. Wenn Du Dir den Code kurz ansiehst, wirst Du erkennen, daß die Methoden mit dem Namen InitDaten() einfach alle Elemente mit den als Parametern gegebenen Werten initialisieren.

Die Methode mit dem Namen Zeige() gibt die Daten für jede Klasse auf eine andere Weise aus, da die Daten so verschieden sind. Obwohl also die Schnittstelle zu allen diesen Funktionen dieselbe ist, ist der Code der Implementation recht verschieden. Natürlich hätten wir auch allen möglichen anderen Code schreiben können, die Ausgabe ist nun aber einmal so schön sichtbar und deshalb für Illustrationszwecke am besten geeignet.

Du solltest diese Datei jetzt kompilieren als Vorbereitung auf das nächste Beispielprogramm, das alle vier Klassen, die wir in den letzten vier Dateien definiert haben, verwenden wird.

Das erste Programm

Beispielprogramm: ANGEST.CPP

In der Datei ANGEST.CPP verwenden wir zum ersten Mal die Klassen, die wir in diesem Kapitel entwickelt haben. Wie Du leicht erkennen kannst, handelt es sich um ein einfaches Programm.

Wir beginnen mit einem Array von zehn Zeigern, die alle auf die Basisklasse zeigen. Du erinnerst Dich sicher, daß es für virtuelle Funktionen sehr wichtig ist, daß ein Zeiger auf die Basisklasse zeigt. Die Zeiger, die wir in diesem Array dann speichern, werden allerdings alle auf Objekte der abgeleiteten Klassen zeigen. Wenn wir mit den Zeigern Methoden aufrufen, wird das System die richtige beim Ausführen auswählen und nicht schon beim Kompilieren, wie dies fast alle unsere bisherigen Beispielprogramme getan haben.

In den Zeilen 16 bis 39 erzeugen wir sechs Objekte [tsts] und initialisieren sie mit den Methoden InitData(). Dann weisen wir den Elementen des Arrays von Zeigern auf Person die Zeiger zu. In den Zeilen 41 bis 44 rufen wir schließlich die Methoden mit dem Namen Zeige() auf, um die gespeicherten Daten auf dem Bildschirm auszugeben. Obwohl wir also in Zeile 43 nur einen Funktionsaufruf verwenden, senden wir doch an alle drei Methoden Zeige() in den abgeleiteten Klassen Nachrichten.

Kompiliere dieses Programm und führe es aus, bevor Du in diesem Kapitel weitergehst. Das Linken erfordert auch bei diesem Beispiel, daß Du die die Teile zuvor einzeln kompiliert hast.

Die Klasse der verbundenen Liste

Beispielprogramm: ELEMLIST.H

In der Datei ELEMLIST.H findest Du die Definitionen von zwei weiteren Klassen, die wir zum Erzeugen einer verbundenen List von Angestellten verwenden werden.

Die zwei Klassen sind in einer Datei zusammengefasst, weil sie sehr eng zusammenarbeiten und die eine ohne die andere so gut wie nutzlos ist. Die Elemente der verbundenen Liste enthalten keine Daten, sondern lediglich einen Zeiger auf die Klasse Person, die wir für das letzte Programm entwickelt haben. Die Liste besteht also aus Elementen der Klasse Person, ohne diese Klasse zu modifizieren.

Zwei interessante Aspekte dieser Datei müssen wir noch erwähnen. Der erste ist die partielle Deklaration in Zeile 7, die es uns erlaubt, die Klasse mit dem Namen AngestelltenListe zu verwenden, bevor wir sie überhaupt deklarieren. Die komplette Deklaration steht in den Zeilen 22 bis 30. Das zweite interessante Konstrukt ist die Freundklasse in Zeile 17, wo wir der gesamten Klasse mit dem Namen AngestelltenListe freien Zugriff auf die Variablen der Klasse AngestelltenElement gestatten. Das ist notwendig, weil die Methode mit dem Namen AngestellteHinzu() auf die Zeiger in AngestelltenElement zugreifen können muß. Wir hätten eine zusätzliche Methode der Klasse AngestelltenElement definieren können und mit dieser auf die Zeiger zugreifen können, aber diese beiden Klassen arbeiten so gut und eng zusammen, daß es kein Problem darstellt, wenn wir in unserer Mauer ein Loch lassen. Die Privatsphäre wird ja vor allen anderen Funktionen und Klassen des Programmes gewahrt.

Die einzige Methode der Klasse AngestelltenElement haben wir inline implementiert. Zwei Methoden der Klasse AngestelltenListe sind noch undefiniert, wir brauchen also eine Implementation für diese Datei.

Die Implementation der verbundenen Liste

Beispielprogramm: ELEMLIST.CPP

Die Datei mit dem Namen ELEMLIST.CPP ist die Implementation der verbundenen Liste und sollte kein Problem sein, wenn Dir klar ist, wie eine einfach verbundene Liste funktioniert. Alle neuen Elemente werden am Ende der aktuellen Liste angefügt, um die Liste einfach zu gestalten. Ein alphabetischer Sortiermechanismus, um die Angestellten nach dem Namen zu sortieren, könnte natürlich hinzugefügt werden. Wenn der benötigte Speicher nicht beschafft werden kann, stoppt das Programm einfach. Das ist für ein "ordentliches" Programm natürlich nicht akzeptabel. Die Fehlerbehandlung ist ein wichtiges Thema, mit dem Du Dich früher oder später auseinandersetzen müssen wirst.

Die Methode zum Anzeigen der Liste durchwandert diese einfach und ruft in Zeile 30 für jedes Element einmal die Methode mit dem Namen Zeige() auf.

Ist Dir aufgefallen, daß nirgendwo in dieser Klasse auch nur die Existenz der drei abgeleiteten Klassen erwähnt ist? Nur die Basisklasse wird genannt. In der Link-Liste existieren die drei Subklassen also nicht. Trotzdem sendet diese Klasse - wie wir sehen werden - Nachrichten an die drei Subklassen. Genau so funktioniert der dynamische Aufruf von Methoden. Nachdem wir uns ein Programm angesehen haben, das die verbundene Liste verwendet, werden wir noch mehr darüber zu sagen haben.

Wir verwenden die verbundene Liste

Beispielprogramm: ANGEST2.CPP

Das Programm ANGEST2.CPP ist unser bestes Beispiel für dynamischen Methodenaufruf in dieser Einführung, aber doch ein sehr einfaches Programm.

Dieses Programm ist dem Programm ANGEST.CPP sehr ähnlich, die wenigen Änderungen machen aber eine insgesamt bessere Illustration aus. In Zeile 7 definieren wir ein Objekt der Klasse AngestelltenListe und beginnen unsere verbunden Liste. In diesem Programm benötigen wir nur dieses eine Objekt. Für alle Elemente der Liste beschaffen wir den Speicherbereich, füllen ihn an und senden das Element an die Liste, um es ihr anzufügen. Der Code ist dem des vorigen Programmes bis Zeile 40 in der Tat sehr ähnlich.

In Zeile 43 senden wir eine Nachricht an die Methode ZeigeListe(), die die gesamte Personalliste ausgibt. Die Klasse für die verbundene List, wie wir sie in den Dateien ELEMLIST.H und ELEMLIST.CPP definiert haben, hat keinerlei Kenntnis von den Subklassen, übergibt aber die Zeiger auf diese Klassen an die richtigen Methoden und das Programm verhält sich so, wie wir es erwarten.

Wozu ist das alles gut? [Zawosbrauchides?]

Stell Dir vor, wir kommen auf die Idee, unser Programm — jetzt fix und fertig und fehlerbereinigt und funktionstüchtig und überhaupt — um eine weitere Klasse zu erweitern. Wir könnten zum Beispiel eine Klasse Beraterin hinzufügen, weil wir eine Beraterin in unserer Firma brauchen.

Wir müßten natürlich zuerst die Klasse mit ihren Methoden schreiben. Die verbundene Liste braucht aber nichts davon zu erfahren, daß wir ihr eine weitere Klasse untergejubelt haben und wir müssen das Programm also überhaupt nicht verändern, damit es auch die Klasse Beraterin miteinbezieht. In diesem spezifischen Fall ist die verbundene Liste sehr klein und einfach zu verstehen, stell Dir aber vor, der Code wäre umfangreich und komplex wie bei einer großen Datenbank. Es wäre sehr schwierig, jede Referenz auf die Subklassen zu aktualisieren und die neue Subklasse überall hinzuzufügen. Dieses umständliche Verfahren stellte natürlich auch ein Paradies für den Fehlerteufel dar. Wir müssen unser Beispielprogramm nicht einmal neu kompilieren, um seinen Einsatzbereich zu erweitern.

Es sollte Dir klar sein, daß es möglich wäre, während der Abarbeitung des Programmes neue Typen zu definieren, sie dynamisch zu erzeugen und auch gleich zu verwenden. Wir müßten dazu den Code auf verschiedene Module aufteilen, die dann parallel ablaufen. Das wäre nicht einfach, aber durchaus möglich.

Wenn Du aufgepaßt hast, ist Dir wahrscheinlich aufgefallen, daß wir weder die Elemente der Liste noch die Liste selbst "zerstören". Wir müßten also die Klasse AngestelltenListe um eine Methode LoeschePerson und die Klasse Person um einen Destruktor erweitern.

Ein Anwendungsgerüst

Beispielprogramm: ANWGER1.CPP

Das Beispielprogramm ANWGER1.CPP illustriert die Methode, die beim Erstellen eines Gerüstes für eine Anwendung zur Anwendung kommt. Wenn Du viel programmierst, wird Dir so etwas sicherlich bei Programmen für ein Betriebssystem mit GUI (Graphical User Interface - Graphische Benutzeroberfläche; etwa: Mac OS X, X Windows, MS Windows,...) nützlich sein. Es kann also nicht schaden, damit vertraut zu sein.

Die Klasse CForm ist die Basisklasse für unser triviales, aber wichtiges Beispiel und besteht aus vier Methoden, aber keinen Datenelementen. Die Methode mit dem Namen ZeigeForm() ruft die anderen drei auf, um unsere kleine Form (Platon: Idee) am Bildschirm auszugeben. Es ist also an diesem Programm nichts Besonderes, außer, daß es Das Gerüst für alle momentan verfügbaren Anwendungsgerüste ist (ein Meta-Gerüst also?). Beachte, daß drei der Methoden in den Zeilen 9 bis 11 als virtual deklariert werden.

Das Interessante passiert, wenn wir in Zeile 27 die Klasse in unsere neue Klasse mit dem Namen CMeineForm importieren und neue Methoden für zwei der Basisklassenmethoden schreiben. Wir haben so viel Funktionalität aus der Basisklasse übernommen, wie uns angenehm war und neue Methoden geschrieben, wo das in der Basisklasse vorhandene nicht den gewünschten Zweck erfüllt. Wenn wir in Zeile 42 schließlich ein Objekt der neuen Klasse verwenden, mischen sich also Teile der Basisklasse mit neu geschriebenen Methoden.

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

Zum Beispiel Gerüste:

Design Patterns: Entwurfsmuster als Elemente wiederverwendbarer objektorientierter Software (mitp Professional)
Programmieren ist Problemlösen. Dieser Klassiker hilft dabei und hat Antworten auf die Fragen, die mit "Wie mache ich...?" beginnen.
›› Mehr Softwareentwicklung-Bücher

In einem so einfachen Beispiel ist das nicht sonderlich attraktiv, wenn wir aber bedenken, wie diese Technik in einem wirklichen Anwendungsgerüst verwendet wird, erscheint es sehr nützlich. Die Autorin des Anwendungsgerüstes schreibt ein komplettes Programm, das alle Notwendigkeiten und das Management der Fenster übernimmt und teilt dieses Programm auf virtuelle Funktionen auf, so wie wir es hier getan haben. Wir suchen uns dann wieder die Teile aus diesem Kuchen, die wir brauchen können und schreiben die Teile neu, die wir ändern wollen. Ein großes Stück Programmierarbeit ist schon für uns erledigt worden.

Das Anwendungsgerüst kann noch viele weitere schon programmierte Funktionen enthalten, wie zum Beispiel solche zur Textverarbeitung oder zum Darstellen von Dialogen. Du wirst es sehr interessant und nützlich finden, Anwendungsgerüste nach allen ihren Funktionen zu durchforsten.

Eine rein virtuelle Funktion

Beispielprogramm: ANWGER2.CPP

Das Beispielprogramm ANWGER2.CPP zeigt eine rein virtuelle Funktion. Eine reine virtuelle Funktion wird deklariert, indem wir ihr den Wert 0 zuweisen, wie in Zeile 10. Eine Klasse, die eine oder mehrere rein virtuelle Funktionen enthält, kann nicht zum Erzeugen von Objekten verwendet werden. Das stellt sicher, daß für jeden Aufruf eine Funktion verfügbar ist und keiner von der Basisklasse beantwortet werden muß, wozu sie in Ermangelung einer Funktionsimplementation auch gar nicht in der Lage ist.

Du kannst kein Objekt einer Klasse erzeugen, die eine oder mehrere rein virtuelle Funktionen beinhaltet, da eine Nachricht an eine rein virtuelle Funktion nicht behandelt werden könnte. Der Compiler stellt sicher, daß die beiden Regeln eingehalten werden. Wenn eine Klasse eine abstrakte Klasse ererbt, ohne die rein virtuelle Methode/n zu überschreiben, wird sie selbst zur abstrakten Klasse, mit der keine Objekte erzeugt werden können.

Da unser Beispiel eine abstrakte Basisklasse verwendet, ist es nicht mehr möglich, ein Objekt der Basisklasse zu verwenden, wie wir dies im vorigen Programm getan haben. Aus diesem Grund ist ein Teil des Programmes auskommentiert.

Abstrakte Klassen finden in vielen Bibliotheken und Anwendungsgerüsten Anwendung. Kompiliere das Programm und führe es aus. Verändere dann den Code ein wenig, um zu sehen, welche Fehler der Compiler ausgibt, wenn Du die Regeln, die wir für abstrakte Klassen aufgestellt haben, verletzt.

Programmieraufgabe

  1. Erweitere die Dateien AUFSHR.H und AUFSHR.CPP um eine Klasse mit dem Namen Beraterin, dann die Datei ANGEST2.CPP um Code, der die neue Klasse verwendet. Du mußt die verbundene Liste nicht neu kompilieren, um die neue Klasse zu verwenden.

(weiter »)

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

Zum Beispiel mehr "modernes" C++:

Effektives modernes C++
So schwer ist das Programmieren nicht, wie Du hoffentlich auch an und in dieser Einführung siehst. Und auch bessere Programme schreiben ist leichter als man glaubt, wenn man den Meyers intus hat.
›› Mehr C++-Bücher

[ so". ]