Späte Bindung oder Polymorphismus
Mit diesem Kapitel kommen wir zum letzten der grundlegenden Punkte der Objekt Orientierten Programmierung. Nach der Kapselung, den Hilfsmitteln zur natürlichen Programmierung und der Vererbung bleibt der Polymorphismus noch offen. Die wörtliche Übersetzung aus dem Griechischen ist Vielgestaltigkeit, was uns aber auch nicht schlauer macht. Ein anderer, mehr technisch orientierter Ausdruck dafür, ist die späte Bindung.
Bindung Objekt - Methode
Kommen wir an dieser Stelle noch einmal auf den Begriff der Bindung zurück. Die Bindung gibt an, welches Objekt mit welcher Methode bearbeitet werden soll. Wie wir schon gesehen haben, wird intern die Bindung mit Hilfe eines Zeigers this realisiert. Dies ist aber nur ein Teil der Bindung. Neben der Adreßermittlung für this prüft der Compiler noch den verwendeten Datentyp. Wenn wir ein Programm haben, das für alle verwendeten Klassen eine print()-Methode zur Verfügung stellt, dann muß der Compiler auch sicherstellen, daß die richtige Methode ausgewählt und ihr der this-Zeiger übergeben wird.
Kann der Compiler während der Übersetzungszeit sowohl die Adresse des Objektes als auch seinen Typ ermitteln, spricht man von früher Bindung.
Im Bild 13-1 sehen Sie die beiden Möglichkeiten für eine frühe Bindung. Entweder kennt der Compiler beim Aufruf einer Methode das Objekt mit Namen. Dann weiß er natürlich die Klasse (den Datentyp), zu dem das Objekt gehört. Weiter kennt er die Adresse des Objektes. Die Adresse kann er nun an den Zeiger this der Methode weitergeben. Mit Hilfe des bekannten Datentyps kann er die richtige aus möglichen Methoden auswählen. Wegen der Überlagerung könnten ja beliebig viele unterschiedliche Methoden gleichen Namens existieren. (Zeile 10)
|
Späte Bindung oder Polymorphismus Seite 185 |
Rahmen1Die zweite Möglichkeit ergibt sich, wenn wir mit einem Zeiger arbeiten. Beim Aufruf der Methode "print()" holt der Compiler die Adresse des Objektes aus dem Zeiger und gibt sie an this weiter. Natürlich erwartet er, daß der Programmierer den Zeiger korrekt vorbelegt hat. Den Typ, den der Compiler zur Auswahl aus den möglichen Methoden benötigt, nimmt er vom Zeigertyp. In unserem Fall haben wir einen Zeiger auf ratio. Daher wählt der Compiler die Methode print() der Klasse ratio (Zeile 15).
Das gerade benutzte Beispiel wollen wir auf ein Feld von Zeigern (Bild 13-2) erweitern. Felder werden oft zur Verwaltung eingesetzt. Alle in einem Programm verwendeten Objekte einer Klasse könnten in einem Feld verwaltet werden. Dies wird man insbesondere dann tun, wenn die Objekte dynamisch angelegt werden. Momentan soll das Feld nur Objekte einer einzigen Klasse enthalten.
|
Seite 186 Einsteigerseminar C++ |
Rahmen3Späte Bindung
Die späte Bindung stellt erst während der Laufzeit fest, welches Objekt mit welcher Methode bearbeitet werden soll. Nehmen wir dazu wieder das Feld und laden es mit Adressen unterschiedlicher Objekte.
Nehmen Sie als Beispiel ein Zeichenprogramm. Der Benutzer legt nacheinander Kreise, Rechtecke oder Polygone an. Das Zeichenprogramm muß alle Objekte dynamisch anlegen und verwaltet die gewählten Objekte in einer Tabelle. Beim Auffrischen des Bildschirms müssen dann nacheinander alle Objekte gezeichnet werden. Dazu brauchen wir die Möglichkeit, mit einem Zeiger auf beliebige Objekte zeigen zu können; und mit diesem Zeiger dann auf Methoden. Dies ist dann späte Bindung.
|
Späte Bindung oder Polymorphismus Seite 187 |
Um die späte Bindung realisieren zu können, benötigen wir zwei Dinge:
In C++ kennen wir die Vererbung. Definitionsgemäß kann ein Zeiger auf eine Basisklasse auch auf Objekte von abgeleiteten Klassen zeigen. Damit haben wir zwar keinen völlig allgemeinen Zeiger, aber immerhin einen Zeiger auf eine Gruppe von Objekten. In unserem Feldbeispiel heißt das, daß das Feld Adressen von beliebigen, voneinander abgeleiteten Objekten aufnehmen kann. Ein wirklich allgemeiner Zeiger wäre typlos. Dies ist bei Zugriffen auf Objekte nicht gestattet. void * kann daher als Typ des Zeigers nicht verwendet werden.
Um die zweite Forderung erfüllen zu können, müssen wir in einem C++ - Objekt eine Zusatzinformation ablegen, die während der Laufzeit, also mit Programmcode, ausgewertet werden kann. Diese Zusatzinformation beinhaltet den Typ. Sie heißt . Im Normalfall fehlt sie in C++. - Objekten
C++ unterscheidet sich hier grundsätzlich von anderen Objekt Orientierten Sprachen wie Smalltalk. In einem Objekt in C++ gibt es erst einmal keine Metainformationen. Diese Metainformationen sind Informationen über das Objekt. Denkbare Metainformationen wären der Name des Objektes, ein Verweis auf die Klasse und deren Aufbau etc. Die Größe eines Objektes läßt sich in C++ mit dem sizeof-Operator ermitteln. Er liefert exakt die Summe der Elemente (sofern beim Übersetzen die Ausrichtung auf Wortgrenzen (alignment) abgeschaltet ist).
Schauen wir uns zuerst ein klassisches Beispiel für die späte Bindung an und kommen anschließend auf die Realisierung zurück.
Das Beispiel soll eine allgemeine, graphische Klasse definieren. Hier ist es ein Punkt am Bildschirm. Von dieser Basisklasse leiten wir eine Linie und ein Quadrat ab. Mit diesen drei Klassen können wir dann in einem Hauptprogramm beliebige Objekte anlegen und deren Adressen in einem Feld verwalten. Und schließlich sollen mit einer Schleife alle Objekte am Bildschirm angezeigt werden.
|
Seite 188 Einsteigerseminar C++ |
Rahmen5Die Basisklasse definiert (Bild 13-3) zwei Eigenschaften, die den Ort des Punktes angeben. Um den Methoden der abgeleiteten Klassen den Zugriff zu ermöglichen, wurden sie in den protected-Abschnitt der Klasse gelegt.
Die Methode print() wurde in der Zeile 16 ausdrücklich für die späte Bindung vorgesehen. Das Schlüsselwort virtual teilt dem Compiler die geänderte Behandlung mit. (Auch die Destruktoren müssen virtual sein.)
Die Implementierung (Bild 13-4) entspricht dem gewohnten Bild. Der Konstruktor belegt die Eigenschaften vor und die Methode "print()" gibt mit der Funktion putchar() ein Sternchen am Bildschirm aus. Die Positionierung des Cursors übernimmt das Makro POS. Das Makro sendet mit der printf()-Funktion eine ANSI-Steuersequenz zum Bildschirm. In der Makroexpansion werden dann die richtigen Werte für Zeile und Spalte eingesetzt.
|
Späte Bindung oder Polymorphismus Seite 189 |
Rahmen7Leiten wir nun die erste Klasse linie ab. Zur Definition einer Linie benötigen wir zumindest einen Anfangs- und einen Endpunkt.
|
Seite 190 Einsteigerseminar C++ |
Um uns das Leben nicht unnötig zu erschweren, soll die Linie nur waagrecht oder senkrecht laufen. Damit sind entweder die Spalten oder die Zeilen der beiden Endpunkte gleich. Den Startpunkt liefert grafik.
|
Späte Bindung oder Polymorphismus Seite 191 |
Rahmen11
Die Zeile wird durch eine Reihe von Sternchen markiert, die mit putchar() wie bei grafik ausgegeben werden. In abgeleiteten Klassen finden wir keinen Hinweis auf eine späte Bindung.
|
Seite 192 Einsteigerseminar C++ |
Rahmen13Beim Quadrat gehen wir analog zur Linie vor. Wieder wird die Basisklasse grafik geerbt. Diesmal gibt grafik den linken oberen Punkt an.
Fortsetzung des Listings nächste Seite.
Der Konstruktor initialisiert die eigenen Eigenschaften, die nun die rechte untere Ecke des Quadrates angeben. Die Koordinaten der linken, oberen Ecke werden an den Konstruktor der Basisklasse weitergegeben.
|
Späte Bindung oder Polymorphismus Seite 193 |
In der "print()"- Methode werden die vier Seiten des Quadrates mit vier for- Schleifen realisiert. Man hätte das quadrat auch mit vier Objekten der Klasse linie realisieren können.
Mit Hilfe der bisher definierten Klassen kann nun das Hauptprogramm geschrieben werden.
Das Hauptprogramm (Bild 13-9) simuliert den beschriebenen Anwendungsfall Zeichenprogramm. Da unser Zeichenprogramm mit Punkten, Linien und Rechtecken umgehen können soll, holt sich das Programm mit include"- Anweisungen zuerst die Klassendefinitionen. Das Verwaltungsfeld für alle gewünschten Objekte hat den Datentyp Zeiger auf Basisklasse", hier also grafik *.
Beim Ablauf wird der Bildschirm gelöscht, um eine vernünftige Anzeige zu gewährleisten. Mit legen wir einige Objekte an und speichern deren Adressen im Verwaltungsfeld. Dies soll die Benutzereingabe simulieren.
Und schließlich gibt eine Schleife alle gültigen Objekte nacheinander am Bildschirm aus. Dies simuliert den Vorgang Bildschirm neu aufbauen.
|
Seite 194 Einsteigerseminar C++ |
Rahmen18Die entscheidende Stelle ist der Schleifenkörper. Mit Hilfe eines Zeigers aus dem Verwaltungsfeld rufen wir eine Methode "print()" auf. In einem Zeiger des Feldes können wir die Adressen von verschiedenen Objekten speichern, die zu verschiedenen Klassen gehören. Welches print() aus welcher Klasse nehmen wir?
In der Basisklasse haben wir die Methode "print()" mit virtual für die späte Bindung vorgemerkt. In den abgeleiteten Klassen wurde die Methode print() immer wieder erneut definiert. Da die Basisklassenmethode virtual war, merkt sich der Compiler dies für die abgeleiteten Methoden ebenfalls.
|
Späte Bindung oder Polymorphismus Seite 195 |
Mit dem Schlüsselwort virtual wird bewirkt, daß der Compiler in alle Objekte, die die Basisklasse benutzen, eine Zusatzinformation ablegt. Sie gibt den Typ des Objektes an. Die Objekte mit später Bindung werden dadurch größer als ohne späte Bindung. Beim Aufruf der virtuellen Methode "print()" im Hauptprogramm holt sich der Programmcode die Typinformation aus dem Objekt, auf das der Zeiger gerade zeigt, und wählt damit die richtige Methode aus.
Damit die späte Bindung funktionieren kann, sind in C++ folgende Voraussetzungen notwendig:
Technisch wird die späte Bindung über Sprungtabellen realisiert. In unserem Beispiel werden intern die Adressen aller "print()"-Methoden in einer virtuellen Methoden-Tabelle (VMT) gespeichert. Beim Zugriff dient die im Objekt abgelegte Typinformation zur Auswahl aus der Tabelle.
Sollte in einer abgeleiteten Klasse keine eigene Definition der virtuellen Methode erfolgen, dann nimmt der Compiler für die Klasse die entsprechende Methode der übergeordneten Klasse. Würde im obigen Beispiel die "print()"- Methode der linie fehlen, dann würde der Compiler die Methode der Klasse nehmen, die linie geerbt hat, in unserem Fall wäre das schon die Basisklasse.
Späte Bindung und Botschaftenkonzept
Mit Hilfe der späten Bindung wurde das Botschaftenkonzept der OOP realisiert. Nun können wir - gedanklich - auf ein beliebiges Objekt zeigen und sagen: drucke dich. Die Vorstellung, daß Objekte ein Eigenleben haben und die richtige Reaktion auf eine solche Anweisung selbst herausfinden, hilft gekapselte Systeme zu entwickeln.
|
Seite 196 Einsteigerseminar C++ |
Vielleicht ließe sich dieses Konzept auch auf andere Sprachen übertragen. Viele professionelle Programmierer haben auch bisher schon ihre Datentypen sauber in Informationsdateien definiert. In der Entwicklung ist es aber wichtig, daß der Compiler nun die richtige Verwendung überprüft und einige Dienstleistungen zur Verfügung stellt. Auch bisher kannte man Sprungtabellen für Funktionen. Neu ist hier, daß der Compiler garantiert, daß in einer solchen Tabelle alle Einträge besetzt sind und kein Indexüberlauf möglich ist.
Das Vorbild - Atomium in Brüssel
Beim Botschaftenkonzept fällt mit immer das Atomium in Brüssel ein. Einzelne Objekte im Raum werden durch Kommunikationskanäle verbunden. Die Steuerung des Gesamtsystems geschieht durch den Austausch von Botschaften.
Es ist sicher kein Zufall, daß an vielen Stellen in der Welt versucht wird, dieses Konzept in neuen Multi-CPU-Maschinen oder in netzbasierten Clustern zu realisieren. Die gesunkenen Hardwarepreise machen solche verteilten Systeme möglich. (So arbeiten z. B. die Erfinder von UNIX oder Andrew Tanenbaum, der Entwickler von MINIX, daran.)
|
Späte Bindung oder Polymorphismus Seite 197 |
Hinweis zur Benutzung der Vererbung mit Polymorphie
Im Kapitel über Vererbung haben wir über den logisch sinnvollen Aufbau von Klassen gesprochen. Im Zusammenhang mit der Polymorphie spielt die Vererbung eine leicht geänderte Rolle. Es muß hier nicht zwingend nach einer logischen Beziehung zwischen den Klassen gesucht werden.
Die Vererbung stellt hier einfach die Mechanik zur Verfügung, um die Polymorphie zu realisieren.
Hinweise zur Weiterarbeit
1) Schreiben Sie einen kleinen Autosimulator. Die Basisklasse sei Auto. Die abgeleiteten Klassen heißen wie die Autohersteller. Die virtual-Methode soll beschleunige() sein. Jede beschleunige()-Methode gibt die nach 10 s erreichte Geschwindigkeit aus. Legen Sie entsprechende Objekte an. Rufen Sie für alle Objekte die virtual-Methode mit später Bindung.
2) Suchen Sie eigene Klassenhierarchien mit gemeinsamen virtual-Methoden.
3) Definieren Sie eine Klasse Weltzeit, die die Greenwich Meantime (GMT/UTC/Zulu) verwalten soll. Leiten Sie davon mehrere Klassen ab, die jeweils eine Zeitzone (MEZ u.a.) repräsentieren. Schreiben Sie für alle Klassen die Methode GibZeit(), die die Ortszeit ausgibt. Verwenden Sie wieder die Polymorphie.
4) Welche Roll spielte eigentlich grafik außer, daß es eine Basisklasse ist? Ersetzen Sie die grafik-Klasse durch eine allgemeine Basisklasse, die nur die Methoden deklariert, die virtual sein sollen. Schließen Sie die Deklaration nicht sofort mit einem ; ab, sondern fügen Sie am Ende ein = 0; an. Solchermaßen deklarierte Methoden sind leer; sie können nicht in der eigenen Klassen definiert werden, sondern müssen in abgeleiteten Klassen jeweils definiert werden (pure virtual functions).
|
Seite 198 Einsteigerseminar C++ |
Welche Vorteile bietet dieses Verfahren ?
Im nächsten Kapitel
Nachdem nun alle Grundelemente der OOP besprochen sind, können wir uns noch einem Problem der Softwareerstellung zuwenden. Die Frage ist, was muß man tun, um den großen Vorrat an C-Funktionen auch in C++ weiterverwenden zu können?
Das nächste Kapitel ist daher der mehrsprachigen Programmierung gewidmet. Funktionen, die in C geschrieben wurden sollen nun in C++ weiterverwendet werden können.
|
Späte Bindung oder Polymorphismus Seite 199 |