![]() |
|
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. [:)] |
|
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 Objkete, die sich vom übrigen Code sinnvoll trennen lassen, programmieren diese und schreiben dann das Hauptprogramm. Wir sollten noch erwähnen, daß Sie gerade am Anfang nicht versuchen, aus allem (und jedem???) mit aller Gewalt ein Objekt zu machen. Wählen Sie ein paar Objekte aus; wenn Sie dann Erfahrung mit objektorientierten Programmiertechniken gesammelt haben, verwenden Sie in Ihren späteren Projekten mehr Objekte. Die meisten Programmierer verwenden in Ihrem ersten Projekt zu viele Objekte und schreiben eigenartigen, unleserlichen Code. |
|
Beispielprogramm: PERSON.H Die Datei PERSON.H definiert die Klasse Person. Es findet sich hier nichts Neues, Sie sollten 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. Beachten Sie auch, daß die einzige Methode dieser Klasse in Zeile 11 virtuell ist. |
|
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++ Kompiler 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. Merken Sie sich aber, daß C++ eine Implementation für alle virtuellen Funktionen verlangt, mögen diese auch nie verwendet werden. In unserem Fall geben wir offensichtlich eine Fehlermeldung aus. Kompilieren Sie dieses Programm, befor Sie zur nächsten Klassendefinition weitergehen. |
|
Beispielprogramm: AUFSHR.H In der Datei AUFSHR.H finden Sie 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 Sie müssen 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 Aufrufen aufgerufen werden können. Auch die andere Methode den 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. |
|
Beispielprogramm: AUFSHR.CPP In der Datei AUFSHR.CPP implementieren wir die drei Klassen. Wenn Sie sich den Code kurz ansehen, werden Sie 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. Sie sollten 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. |
|
Beispielprogramm: ANGEST.CPP In der Datei ANGEST.CPP verwenden wir zum ersten Mal die Klassen, die wir in diesem Kapitel entwickelt haben. Wie Sie einfach erkennen können, ist ein einfaches Programm. Wir beginnen mit einem Array von zehn Zeigern, die alle auf die Basisklasse zeigen. Sie erinnern sich 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. Kompilieren Sie dieses Programm und führen Sie es aus, bevor Sie in diesem Kapitel weitergehen. Das Linken erfordert auch bei diesem Beispiel, daß sie die die Teile zuvor einzeln kompiliert haben. |
|
Beispielprogramm: ELEMLIST.H In der Datei ELEMLIST.H werden Sie die Definitionen von zwei weiteren Klassen finden, die wir zum Erzeugen einer verbundenen List von Angestellten verwenden werden. Die zwei Klassen sind in einer Datei zusammgefaßt, 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 8, die es uns erlaubt, die Klasse mit dem Namen AngestelltenListe zu verwenden, bevor wir sie überhaupt deklarieren. Diese komplette Deklaration steht in den Zeilen 23 bis 31. Das zweite interessante Konstrukt ist die Freundklasse in Zeile 18, 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. |
|
Beispielprogramm: ELEMLIST.CPP Die Datei mit dem Namen ELEMLIST.CPP ist die Implementation der verbundenen Liste und sollte klar sein, wenn Ihnen 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 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 Sie sich früher oder später auseinandersetzen müssen. 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 Ihnen 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. |
|
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 ingesamt bessere Illustration aus. In Zeile 7 definieren wir ein Objekt der Klasse AngestelltenListe und beginnen usere 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 ausgiebt. 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. |
|
Stellen Sie sich vor, wir kommen auf die Idee, unser Programm - jetzt fix und fertig und fehlerbereinigt und funktionstüchtig und überhaupt - um eine weitere Klasse 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, stellen Sie sich 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 das Paradies für den Fehlerteufel dar. Wir müssen unser Beispielprogramm nicht einmal neu kompilieren, um seinen Einsatzbereich zu erweitern. Es sollte Ihnen 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 möglich. Wenn Sie aufgepaßt haben, ist Ihnen 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 AngestelltenListe um einen Destruktor erweitern. |
|
Beispielprogramm: ANWGER1.CPP Das Beispielprogramm ANWGER1.CPP illustriert die Methode, die beim Erstellen eines Gerüstes für eine Anwendung zur Anwendung kommt. Wenn Sie viel programmieren, wird Ihnen so etwas sicherlich bei Programmen für ein Betriebssystem mit GUI (Graphical User Interface - Graphische Benutzeroberfläche; etwa: OS/2 PM, X Windows, MS Windoze,...) 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?). Beachten Sie, 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 schließlich ein Objekt der neuen Klasse in Zeile 42 verwenden, mischen sich also Teile der Basisklasse mit neu geschriebenen Methoden. 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. Der Autor 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 getan. Das Anwendungsgerüst kann noch viele weitere schon programmierte Funktionen enthalten, wie zum Beispiel solche zur Textverarbeitung oder zum Darstellen von Dialogen. Sie werden es sehr interessant und nützlich finden, Anwendungsgerüste nach allen ihren Funktionen zu durchforsten. |
|
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. Sie können 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 Kompiler 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. Kompilieren Sie das Programm und führen Sie es aus. Dann verändern Sie den Code ein wenig, um zu sehen, welche Fehler der Kompiler ausgiebt, wenn Sie die Regeln, die wir für abstrakte Klassen aufgestellt haben, verletzen. |
|
||
![]() Copyright © 1997 Heinz Tschabitscher - Letzte Änderung, 21. April 1997 Heinz Tschabitscher - semper@doubt.com - Bitte senden Sie Kommentare, Kritik und Anregungen. |
||
[ so". ]