Überlagerung von Operatoren
Im Kapitel über das Typkonzept haben wir sowohl die vordefinierten wie auch die selbstdefinierten Operationen besprochen. Für die vordefinierten Datentypen hat die Sprache einen Satz von Operatorsymbolen zur Verfügung. Bei privaten Datentypen schreibt der Programmierer eine Menge von Unterprogrammen zur Bearbeitung. Im Falle der Klassen schreiben wir eine spezielle Form der Funktionen, die Methoden.
Mit den Methoden haben wir die exakte Typüberprüfung, die bei den vordefinierten Datentypen schon lange selbstverständlich war, auch für private Datentypen eingeführt.
Ein Operatorsymbol, wie + oder =, ist nicht auf einen Typ festgelegt. Wir dürfen die Zuweisung = auf "int", char, double oder sogar auf Objekte und Strukturen anwenden. Ähnliches gilt für +, mit dem jedoch keine Objekte bearbeitet werden können.
|
Überlagerung von Operatoren Seite 97 |
Hinter einem Operatorsymbol stehen also unterschiedliche Operationen. Der Compiler muß nun bei der Übersetzung des Quelltextes selbst herausfinden, welche Operation gerade benötigt wird. Er entscheidet sich mit Hilfe der Umgebung, in der das Operatorsymbol steht. Man sagt daher auch, daß Operatoren kontextsensitiv sind, d.h. sie erhalten ihre exakte Bedeutung erst aus den beiden Informationen, welches Symbol benutzt wurde und in welcher Umgebung es steht.
Im Bild 7-2 wurde das Symbol für die Zuweisung verwendet. Der Einfachheit halber wurde die Umgebung, also die rechte und linke Seite, jeweils vom gleichen Typ gewählt. Die Variablen von Typ "char" und int können wahrscheinlich mit Hilfe von Maschinenbefehlen zugewiesen werden. Beide Datentypen haben in Registern Platz. Damit kann die Zuweisung mit einfachen "move"-Befehlen übersetzt werden. Im Fall der beiden double-Variablen geht das nicht. Um acht Byte zu kopieren, benötigen wir entweder mehrere Maschinenbefehle oder ein allgemeines Unterprogramm zum Kopieren von Daten beliebiger Größe.
Mit Hilfe eines Debuggers läßt sich leicht herausfinden, wie der jeweilige Compiler in diesem Fall übersetzt.
|
Seite 98 Einsteigerseminar C++ |
Der Compiler kann die Operation, die mit einem Symbol angefordert wird, entweder mit Code oder durch Aufruf eines Unterprogramms realisieren. Auch wir haben bei den privaten Datentypen die gewünschten Operationen durch eigene Unterprogramme realisiert. Es wäre schön, wenn wir dem Compiler sagen könnten, daß er bei einem +-Symbol, das zwischen ratio-Objekten steht, nun selbständig eine Methode der Klasse ratio rufen soll.
Dazu müssen wir dem +-Symbol eine oder mehrere neue Bedeutungen hinzufügen. In C++ ist das möglich und wird Überlagerung von Operatoren genannt. Bisweilen findet sich auch der Begriff Überladen von Operatoren (engl. overloading).
Wir benötigen folgende Schritte zur Überlagerung:
Werfen wir zuerst einen Blick auf ein Detail im Compiler.
|
Überlagerung von Operatoren Seite 99 |
Viele Compiler benutzen interne Tabellen, in denen das Operatorsymbol und für jeden Typ die notwendige Aktion gespeichert ist. Trifft der Compiler auf ein Symbol, z.B. ein +-Zeichen, dann stellt er fest, in welcher Umgebung es steht und sucht sich dann aus der Zuordnungstabelle die gewünschte Aktion heraus. Diese internen Tabellen können mit Hilfe des Schlüsselwortes operator in C++ ergänzt werden.
Dazu müssen wir in C++ eine Zuordnung Symbol-Typ-Aktion definieren. Als Typ dient eine Klasse, als Symbol nehmen wir zuerst einmal ein +-Zeichen und die Aktion soll eine Methode sein. Der Name der neuen Methode kann mit der Kombination aus dem Schlüsselwort operator und dem gewünschten Symbol gebildet werden.
Das Schlüsselwort operator trägt in die internen Tabellen die gewünschte Zuordnung ein.
Im Bild 7-4 haben wir eine Methode der Klasse ratio definiert. Sie muß wie jede andere Methode in der Klassendefinition deklariert werden.
Mit dieser Voraussetzung können wir nun die Klasse ratio neu definieren. Als überlagerte Operatoren nehmen wir die vier Grundrechenarten und die Zuweisung. Weitere Methoden, wie "print(), sollten zusätzlich deklariert werden.
Bei diesen Methoden können wir die Referenzübergabe aus dem Kapitel 6 (Parameterübergaben) benutzen. Die Methoden für die vier Grundrechenarten sollen die Operanden nicht verändern. Niemand hindert uns, dies doch zu tun. Um aber der normalen +-Operation möglichst nahe zu kommen, sollten wir die Operanden nur lesen. Daher brauchen wir ein Zwischenergebnis. Ebenso wie bei addiere dürfen wir keine Rückgabe mit Referenzen auf eine lokale Variable einführen. Anders liegt der Fall bei der Zuweisung. Hier sollen wir den linken Operanden verändern.
|
Seite 100 Einsteigerseminar C++ |
Aufruf der Operatormethoden
Vorausgesetzt, wir haben die obige Klasse ratio definiert und alle Methoden geschrieben, dann kann der Compiler erkennen, daß ein =-Symbol zwischen ratio-Objekten steht und die selbstdefinierte Operatormethode aufrufen.
Der Methodenaufruf geschieht mit einer Bindung an den linken Operanden. Bei Operatoren, die einen zweiten Operanden benötigen, wie bei der Zuweisung, wird der zweite Operand als Parameter übergeben.
|
Überlagerung von Operatoren Seite 101 |
Die Methoden liefern auch einen Rückgabetyp. Im einfachsten Fall wird er gar nicht verwendet. Es gibt in C und C++ viele Stellen, wo ein Rückgabewert zwar bereitgestellt, aber fast nie abgeholt wird. Ein Beispiel ist die "printf()"-Funktion, die als Rückgabewert die Anzahl der ausgegebenen Zeichen oder im Fehlerfall EOF liefert. Ausgewertet wird dieser Rückgabewert sehr selten. Doch wann brauchen wir den Rückgabewert? Schauen wir uns dazu eine Addition von drei Objekten im folgenden Bild 7-7 an.
Wenn ein Ausdruck mehrere Operatorsymbole umfaßt, brauchen wir Spielregeln, um die Reihenfolge der Bearbeitung festzulegen. Operatoren haben Prioritäten. Eine Operatorpriorität heißt z. B.: Punkt vor Strich. Multiplikation und Division haben eine höhere Priorität als die Addition oder Subtraktion. Die Zuweisung hat eine sehr geringe Priorität. Nur der Komma-Operator zum Trennen von Anweisungen hat eine noch geringere.
In unserer Formel werden wir zuerst addieren und dann das Ergebnis zuweisen.
|
Seite 102 Einsteigerseminar C++ |
Die Addition tritt zweimal auf. Die Reihenfolge legt jetzt die sogenannte Abarbeitungsrichtung (Assoziativität) fest. In C und C++ haben nur die Zuweisung und die unären (unär = mit einem Operanden) Operatoren die Abarbeitungsrichtung von rechts nach links; alle anderen von links nach rechts. Bei der Zuweisung muß zuerst das rechts stehende Ergebnis berechnet werden, bevor ein Ergebnis zugewiesen werden kann.
Fangen wir daher mit der Addition der Objekte B und C an. Das +-Zeichen hat eine höhere Priorität als die Zuweisung und innerhalb der Additionskette wird von links nach rechts abgearbeitet. Die Methode liefert die Summe als Zwischenergebnis in einer anonymen Zwischenvariablen, die vom Compiler intern verwaltet wird. Nur für die Erläuterung wird der Name ZwiErg1" verwendet. Dieses erste Zwischenergebnis addieren wir dann mit dem Objekt D" zu einem zweiten Zwischenergebnis. Die letzte Operation ist dann die Zuweisung. Deren Rückgabe wird nicht weiter benutzt.
|
Überlagerung von Operatoren Seite 103 |
Eine Formel wird schrittweise berechnet, wobei in einem folgenden Schritt das bisherige Zwischenergebnis weiter verarbeitet wird.
Das Zwischenergebnis wird in der Methode durch die return-Anweisung bereitgestellt. Dabei wird eine anonyme Zwischenvariable erzeugt, die das Ende des Unterprogramms überlebt. (Die einzelnen Schritte haben wir bereits im Kapitel 6 (Parameterübergabe) behandelt.)
Im Beispiel werden Objekte übergeben. Innerhalb einer Methode steht uns automatisch mit this ein Zeiger auf das gerade bearbeitete Objekt zur Verfügung. Er wird beim Aufruf entsprechend vorbesetzt. Der Parameter p1" wird angelegt und mit dem Kopier-Konstruktor initialisiert. Die return"-Anweisung legt ein dynamisches Zwischenergebnis an, das mit dem Rückgabeobjekt initialisiert wird. Wieder tritt der Kopier-Konstruktor in Aktion. Das anonyme Zwischenergebnis wird bei der anschließenden Zuweisung verwendet. Jetzt wird es nicht mehr benötigt und kann gelöscht werden. Der Compiler wird dies zu einem ihm passenden Zeitpunkt automatisch tun.
|
Seite 104 Einsteigerseminar C++ |
Es gibt neben der beschriebenen, sehr allgemeinen Methode noch andere Möglichkeiten, Objekte zurückzugeben. Es kann daher bei Ihrem Compiler auch geringfügig anders ablaufen.
Gibt man an eine Methode ein Objekt nicht per Wertübergabe, sondern per Referenz zurück, dann genügen zumeist die Register, um die verwendete Adresse aufzunehmen. Die Parameterobjekte und das Rückgabeobjekt entfallen. Und mit den Objekten entfallen auch die Kopier-Konstruktoraufrufe. Die Objekt-Übergabe per Referenz ist erheblich schneller als die Wertübergabe. Wo es möglich ist, wurde daher in den Operatormethoden der Klasse ratio die Referenz verwendet.
Bei der Diskussion der Implementierung werden wir noch einmal auf den Unterschied zwischen Referenz- oder Wertübergabe zurückkommen.
Liste der Operatoren
Sie können mit Hilfe des operator-Schlüsselwortes kein neues Symbol definieren und auch keine Eigenschaft der Symbole ändern. Die Symbole, die Anzahl der Operanden, die Prioritäten und die Abarbeitungsrichtung bleiben erhalten. Man kann nur zu vorhandenen Symbolen zusätzliche Aktionen hinzufügen.
Die verwendbaren Operatoren können Sie der folgenden Liste entnehmen. Auf den ersten Blick erscheinen new und delete sowie die runden und die eckigen Klammern etwas merkwürdig. Aber auch das sind Operatoren und können überlagert werden.
Überlagern wir die eckigen Klammern, können wir Zugriffe auf Felder schützen, indem wir die Grenzen abprüfen. Mit einem neuen new und delete kann man die Speicherverwaltung selbst in die Hand nehmen.
Hier eröffnen sich für den Programmierer viele Möglichkeiten, ein Programm für den Anwender sehr einfach und benutzerfreundlich zu gestalten. Aber auch der schönste Compiler kann nicht verhindern, daß ein Programmierer hinter dem +-Symbol eine Subtraktion versteckt.
Mit der Möglichkeit der Überlagerung von Operatoren können wir nun für ratio, complex oder auch matrix das +-Zeichen (und die anderen auch) definieren. Für den Anwender ist die Benutzung von Operatorsymbolen sehr einfach. Blättern Sie kurz zurück. Im Bild 7-5 sehen Sie als Schlußzeile eine Addition von Bruchzahlen in der für ganze Zahlen gewohnten Schreibweise.
|
Überlagerung von Operatoren Seite 105 |
Das Beispiel ratio
Die Klasse ratio ist wieder wegen ihres einfachen Aufbaus gut geeignet, die Möglichkeiten der Überlagerung von Operatoren zu zeigen. Für ratio wollen wir die vier Grundrechenarten und die Zuweisung überlagern. Obwohl die Zuweisung nicht notwendig wäre, da sie der Compilervorgabe entspricht, ist sie zur Demonstration mit aufgenommen. Beachten Sie bei den Methoden auch die Verwendung der Rückgabewerte.
|
Seite 106 Einsteigerseminar C++ |
Im nächsten Listing folgt die Implementierung. Danach kommen wir zur Diskussion der Klasse.
In der Zeile 4 der Informationsdatei (Bild 7-10) wird durch die bedingte Übersetzung verhindert, daß die gleiche Informationsdatei in einem Übersetzungsvorgang mehrfach eingelesen wird. Die Namen der Eigenschaften sind wieder sehr kurz, um die Buchbreite besser zu nutzen. Nach dem Konstruktor folgen die vier Grundrechenmethoden und die Zuweisung. Um die Ergebnisse ausgeben zu können, wurde noch die Methode "print()" vorgesehen.
|
Überlagerung von Operatoren Seite 107 |
|
Seite 108 Einsteigerseminar C++ |
Eine Neuerung gibt es noch im privaten Teil der Klasse. Hier wurde eine private Methode kuerzen() vorgesehen. Sie kann nur innerhalb der anderen Methoden aufgerufen werden. Geschrieben wurde sie der Kürze halber in der Implementierung nicht (siehe auch: Wiener/Pinson im Literaturverzeichnis).
Das #endif schließt die bedingte Übersetzung.
In der Implementierung wurde in allen Methoden der Parameter mit einer Referenz übergeben. Damit muß der Compiler kein Objekt anlegen und mit dem Kopierkonstruktor initialisieren. Die Übergabe ist damit erheblich schneller. Allerdings steigt die Gefahr der Seiteneffekte. In den Methoden könnten wir das aktuelle Objekt versehentlich verändern.
In den Methoden für die vier Grundrechenarten wird das Ergebnis immer mit Wertübergabe zurückgegeben. Anders liegt der Fall bei der Zuweisung. Wir sind in der Methode an das links der Zuweisung stehende Objekt gebunden. Es lebt länger als der Methodendurchlauf dauert. Und der Parameter ist eine Referenz auf ein nicht-lokales Objekt. Ein lokales Zwischenergebnis haben wir auch nicht. Schließlich soll die Zuweisung ja im Gegensatz zu den anderen Methoden, das eigene Objekt verändern. Daher können wir ein Objekt der Klasse ratio per Referenz zurückgeben. Aber welches?
|
Überlagerung von Operatoren Seite 109 |
Wir geben das Objekt zurück, an das wir gebunden sind und das wir verändert haben. Als einzigen Hinweis auf dieses Objekt haben wir this: einen Zeiger auf das Objekt. Also müssen wir dereferenzieren, das * verwenden, um nicht den Zeiger, sondern das Objekt, auf das wir zeigen, zurückzugeben. Es schaut ein bißchen ungewöhnlich aus, aber es funktioniert. Dies ist eine der ganz seltenen Stellen, wo wir this benötigen.
Die private Methode kuerzen() würde unsere Ausgaben schöner ausschauen lassen. Um das Beispiel nicht zu groß werden zu lassen, wurde sie hier nicht geschrieben.Wenden wir uns dem Hauptprogramm im Bild 7-12 zu. Hier können wir den Erfolg der überlagerten Operatoren testen. Mit #include wird die Informationsdatei für die Standard-Ein- und Ausgabe und die Klassendefinition eingelesen. Die printf()-Anweisung löscht den Bildschirm mit einer ANSI-Steuersequenz und gibt eine Überschrift aus.
Nacheinander werden danach die vier Grundrechenarten ausprobiert. Die Tabulatoren sorgen für eine ausgerichtete Bildschirmausgabe.
Die Zuweisung wird bei den Rechenoperationen zwar verwendet, aber nie deren Rückgabewert. Um auch die Rückgabe der Zuweisung zu testen, gibt es die Zeile 24. Der Zuweisungsoperator hat in C und C++ keine Sonderstellung. Man kann ihn in einer Kettenzuweisung benutzen. Und dabei wird auch der Rückgabewert unbedingt benötigt. Die Kettenzuweisung folgt in ihrem Ablauf dem Bild 7-7 (Reihenfolge der Formelauswertung).
Die Überlagerung von Operatoren kann im Falle von aufwendigeren Klassen auch etwas schwieriger werden. Definieren wir dazu auch die Klasse Zeile neu und führen hier ebenfalls die überlagerte Zuweisung und die Addition (Konkatenieren) ein.
|
Seite 110 Einsteigerseminar C++ |
Die Informationsdatei für das Zeilenbeispiel finden Sie auf der folgenden Seite.
|
Überlagerung von Operatoren Seite 111 |
Rahmen26 Beim Anlegen von Objekten der Klasse Zeile wird im Konstruktor ein Speicherbereich dynamisch angelegt.
Rahmen28
|
Seite 112 Einsteigerseminar C++ |
Wollen wir nun eine Zuweisung durchführen, dann verweist der Zeiger Inhalt auf einen verwendeten Speicherbereich. Die Zuweisung muß zuerst den vorhandenen Speicher freigeben und kann danach erst mit der eigentlichen Zuweisung beginnen.
Typwandlung mit dem Schlüsselwort operator
Das Schlüsselwort "operator" dient noch einem anderen Zweck. Mit operator" und einem Datentypnamen kann man Konvertierungsroutinen definieren. Bei der Konvertierungsroutine entfällt die Angabe des Rückgabetyps. Die Routine wird auf ein Objekt der Klasse angewendet und liefert den Typ zurück, der Teil des Methodennamens ist.
Häufig werden solche Konvertierungen verwendet, um den Zustand der Klasse in Schleifen oder Entscheidungen abzufragen. Abfragen akzeptieren entweder einen ganzzahligen Ausdruck oder einen Zeiger auf void.
|
Überlagerung von Operatoren Seite 113 |
Im Hauptprogramm kann dann in einer Abfrage die Typkonvertierung benutzt werden. Im Beispiel 15 sind zwei Arten der Typkonvertierung gezeigt. Entweder geben wir die gewünschte Konvertierung durch Voranstellen eines Typs bekannt (cast) oder die Konvertierung erfolgt in der zweiten if-Abfrage automatisch.
|
Seite 114 Einsteigerseminar C++ |
Hinweise zur Weiterarbeit
1) In der Klasse Zeile wurde der +-Operator für Zeilen deklariert. Schreiben Sie ihn. Achten Sie besonders darauf, wo der Speicherplatz für beide Zeilen zusammen liegen soll und berücksichtigen Sie den Rückgabewert. Kann er mit einer Referenz zurückgegeben werden?
2) Fügen Sie in die Konstruktoren und den Destruktor "printf()"-Anweisungen ein. Experimentieren Sie dann mit Weglassen bzw. Hinzufügen der Referenz bei Parameterübergaben und Objektrückgaben.
3) Prüfen Sie die "operator=()"-Methode für die Klassen "ratio" und "Zeile", ob sie in allen Fällen genauso funktionieren, wie Zuweisungen mit "int".
4) Die Implementierung der "operator=()"-Methode für die Klasse "Zeile", hat eine Beschränkung; sie funktioniert nicht, wenn ein Objekt sich selbst zugewiesen wird. ("ZielObjekt = ZielObjekt";). Wieso nicht? Wie kann man diese Beschränkung aufheben?. Ist das sinnvoll?
5) Schreiben Sie eine private Methode "kuerzen()", und rufen Sie sie am Ende jeder mathematischen Methode auf. Zur Demonstration könnte es genügen, Zähler und Nenner zu halbieren, wenn beide geradzahlig sind.
|
Überlagerung von Operatoren Seite 115 |
Im nächsten Kapitel
Bei den handelsüblichen C++ Compilern wird eine Klassenbibliothek für die Ein- und Ausgabe mitgeliefert. Für den Benutzer wird damit eine neue Möglichkeit bereitgestellt, mit den Standard E/A- Kanälen zu arbeiten. Jeder E/A-Kanal wird mit einem Objekt realisiert. Dazu gibt es einen ganzen Satz von fertigen Methoden und überlagerte Operatoren.
Durch die Kapselung können dem Anwender viele Dienstleistungen geboten werden, ohne daß er mit der Komplexität der internen Realisierung vertraut sein muß. Dieses Anwendungsbeispiel wollen wir im folgenden Kapitel näher betrachten.
|
Seite 116 Einsteigerseminar C++ |