Objektorientierte Programmierung - Kapitel 4 - Operator - Überladung
Inhalte von Dr. E. Nadobnyh
4.1. Grundlagen und Regeln
Operator-Überladung
Ein Operator ist überladen, wenn er je nach Typ der Operanden eine unterschiedliche Bedeutung hat.
Die meisten Operatoren für eingebauten Datentypen sind schon überladen.
Beispiel:
double a, b; short x, y;
a*b; x*y; //verschiedene Algorithmen
Operatorfunktion
In C++ gibt es die Möglichkeit , Operatoren für die Klassen zu definieren.
Eine Operatorfunktion ist eine Funktion mit einem besonderen Namen. Der Name der Operatorfunktion beginnt mit dem Schlüsselwort operator, dem das Operatorsymbol folgt. Ausdrücke mit Operatoren sind intuitiver und daher schneller zu erfassen als Ausdrücke mit Funktionsaufrufe.
Zwei Definitionsmöglichkeiten
Eine Operatorfunktion kann entweder als globale Funktion oder als Methode einer Klasse definiert werden.
1) Globale Operatorfunktion
class Bruch{ };
Bruch operator+ (Bruch, Bruch );
bzw. Operatorfunktion als Methode
class Bruch
{ public:
Bruch operator+ (Bruch x);
};
4. Operator-Überladung
Aufruf der Operatorfunktion
Der Operator-Aufruf wird in einen Funktionsaufruf umgewandelt:
1) entweder in den Aufruf der globalen Operatorfunktion,
2) oder in den Aufruf der Operatormethode.
Mehrdeutigkeit
Wenn Compiler mehr als eine Aufrufmöglichkeit findet, wird der Aufruf bemängelt.
⇒ Demo 1.
Globale Operatorfunktion
class Bruch; //Vorwärtsdeklaration
Bruch operator+(Bruch a, Bruch b); //Deklaration
class Bruch
{ //Freundschaftserklärung
friend Bruch operator+(Bruch a, Bruch b);
};
main()
{ Bruch a, b, c;
c = a + b; //Aufruf
c=operator+(a,b); //identisch
}
Bruch operator+(Bruch a, Bruch b); //Deklaration
class Bruch
{ //Freundschaftserklärung
friend Bruch operator+(Bruch a, Bruch b);
};
main()
{ Bruch a, b, c;
c = a + b; //Aufruf
c=operator+(a,b); //identisch
}
⇒ Demo 1.2
Operatormethode
class Bruch
{ public: Bruch operator+(Bruch b);
};
main()
{ Bruch a, b, c;
c = a + b; //Aufruf
c=a.operator+(b); //identisch
}
{ public: Bruch operator+(Bruch b);
};
main()
{ Bruch a, b, c;
c = a + b; //Aufruf
c=a.operator+(b); //identisch
}
Operanden
Der zweistellige Operator hat zwei Operanden. Der erste Operand ist ein Objekt, für das die Methode aufgerufen wird.
⇒ Demo 3.
Einsatzregeln
1) Es können bis auf wenige Ausnahmen alle Operatoren überladen werden.
2)Der Funktionsname besteht aus dem Schlüsselwort operator und dem Operatorzeichen.
3) Es können die übliche Operatoren überladen werden (z.B. *, +=, usw.).
Eine Definition von neuen Operatoren ist nicht möglich, z.B. neuen Operator ** zum Potenzieren. Andere Zeichen wie $ usw. sind nicht erlaubt.
⇒ Demo 4. C++ Operatoren
4) Wenigstens ein Argument der Operatorfunktion muss ein class –Objekt sein, oder die Operatorfunktion muss eine Methode sein. Die Bedeutung der Operatoren für Standard-Typen lässt sich nicht umdefinieren.
5) Die Anzahl der Operanden eines Operators (Stelligkeit) kann nicht geändert werden, d.h. ein unärer Operator bleibt unär.
6) Einige Operatoren (z.B.: +, -, * ) können getrennt unär und binär überladen werden.
⇒ Demo 5.
7) Der Operator-Aufruf wird in einen Funktionsaufruf umformuliert.
8) Die vorgegebenen Vorrangregeln können nicht verändert werden.
9) Die Reihenfolge der Auswertung (von links oder von rechts) bei der Operatoren mit gleicher Priorität bleibt erhalten.
10) Überladene Operatoren können keine Default-Parameter haben. (Eine Ausnahme ist der Funktionsoperator.)
⇒ Demo 6 + Demo 7.
Einzelne Operatoren sind nicht überladbar:
Operator | Bedeutung |
sizeof
. .* :: ?: |
Größe von Objekten
Zugriffsoperator (Strukturen, Unions, Klassen) Zugriffsoperator für Elementzeiger Bereichsoperator Auswahloperator |
Operatoren, die nur als Methoden überladen werden können
Operator | Bedeutung |
= [ ] ( ) -> (Typ) |
Zuweisung Indizierung Funktionsaufruf bzw. Funktionsoperator Elementzugriff für Zeiger Typumwandlungsoperator |
4.2. Symmetrie der Operatoren
Symmetrische Operatoren
Einige binäre Operationen z.B. Addition sind kommutativ und ihre beide Operanden sind „gleichberechtigt“.
Beispiel:
Bruch a, b, c;
c=a+b;
c=b+a;
Gemischte Datentypen
Die Verwendung der Operanden vom Datentyp int soll auch möglich sein.
Beispiel:
Bruch a, b, c;
c=a+5;
c=5+a;
Symmetrie der globalen Operatorfunktionen
a) Ohne Konvertierungskonstruktor
Um gemischte Datentypen zu erlauben, kann man noch zwei globalen Operatorfunktionen entwerfen:
Bruch operator+(Bruch a, Bruch b);
Bruch operator+(Bruch a, long b);
Bruch operator+(long a, Bruch b);
b) Mit Konvertierungskonstruktor
Der Compiler konvertiert implizit den aktuellen
Parameter vom Typ long in Typ Bruch.
Bruch operator+(Bruch a, Bruch b);
Bruch (long x);
⇒ Demo 8.
Asymmetrie der Operatormethode
Die Symmetrie geht verloren, wenn die Operatorfunktion als Methode implementiert wird.
Beispiel: Addition einer rationalen und einer ganzen Zahl
Aufruf | Operatormethode |
c = a + 3; c = a.operator+(3); |
Bruch Bruch::operator+(int x); |
c = 3 + b; c = 3 .operator+(b); |
unmöglich |
Wenn die Symmetrie erhalten bleiben soll, dann ist den Operator als globale Funktion zu implementieren.
4.3. Einzelne Operatoren
Shift-Operatoren
In der iostream -Bibliothek wird der Links-Shift-Operator < als ein Ausgabeoperator und der Rechts-Shift-Operator >>
als ein Eingabeoperator interpretiert.
Um die iostream- Bibliothek für die Ein- und Ausgabe von Objekten des benutzerdefinierten Datentyps zu verwenden, sollen diese Operatoren überladen werden.
Globale Operatorfunktion oder Operatormethode
Aufruf | Ersetzung durch | Zusammenfassung |
Bruch c; cout<td> | cout.operator<<(c); | Diese Methode ist nicht implementiert |
operator<<(cout, c); | Die globale Fkt. ist die einzige Möglichkeit |
Rückgabe per Referenz für die Verkettung
#include Bruch a; cout<<“a= “<td> |
ostream & operator<< (ostream & os, const Bruch & a) { ... return os; } |
⇒ Demo 9
Implementierung globaler Operatorfunktion
Ohne der Freundschaftserklärung müssen die zusätzliche öffentliche Zugriffsmethoden vorhanden sein und in der globalen Funktion aufgerufen werden.
class Bruch
{ private: long za, ne; //za-Zaehler, ne-Nenner
public: long getZa(){return za;}
long getNe(){return ne;}
};
ostream & operator<<(ostream & out, const Bruch & b);
ostream & operator<<(ostream & out, const Bruch & b)
{ out<<b.getZa()<<"/"<<b.getNe();
return out;
}
{ private: long za, ne; //za-Zaehler, ne-Nenner
public: long getZa(){return za;}
long getNe(){return ne;}
};
ostream & operator<<(ostream & out, const Bruch & b);
ostream & operator<<(ostream & out, const Bruch & b)
{ out<<b.getZa()<<"/"<<b.getNe();
return out;
}
Globale Operatorfunktion als friend-Funktion
Damit eine globale Operatorfunktion direkt auf private Attribute zugreifen kann, wird sie als friend-Funktion deklariert.
class Bruch
{ private: long za, ne; //za-Zaehler, ne-Nenner
friend
ostream & operator<<(ostream & out, const Bruch & b);
};
ostream & operator<<(ostream & out, const Bruch & b)
{ out<<b.za<<"/"<<b.ne;
return out;
}
{ private: long za, ne; //za-Zaehler, ne-Nenner
friend
ostream & operator<<(ostream & out, const Bruch & b);
};
ostream & operator<<(ostream & out, const Bruch & b)
{ out<<b.za<<"/"<<b.ne;
return out;
}
⇒ Demo 10.
Index-Operator
Für eingebauten Datentypen dient der Index-Operator normalerweise dem Zugriff auf Feldelemente, z.B.:
b[1]=5;
a=b[1];
Im Ausdruck b[i] ist b der linke Operand und i der rechte Operand. Der rechte Operand heißt Index.
Für den nicht überladenen Index-Operator gelten folgende Einschränkungen:
- Der linke Operand muß ein Zeiger oder ein Feldname sein.
- Der rechte Operand muß ein ganzzahliger Ausdruck sein.
- Der Ausdruck b[i] impliziert eine versteckte Zeigerarithmetik: *(b + i)
Index-Operator für eine Klasse
Für eine Klasse muss ein Index-Operator als Methode überladen werden.
class Array
{ public: float & operator[](int i);
};
{ public: float & operator[](int i);
};
Für den überladenen Index-Operator gelten folgende Einschränkungen:
- Der linke Operand muß ein Objekt der Klasse sein.
- Der rechte Operand darf einen beliebigen Datentyp haben.
- Der Ergebnis-Typ ist nicht festgelegt.
- Das Ergebnis soll eine Referenz auf ein Objekt sein.
⇒ Demo 11.
Zuweisungsoperator
Ein existierender Objekt wird durch eine Kopie eines anderen ersetzt.
Für jede Klasse ist ein Standard-Zuweisungsoperator implizit vordefiniert. Die Standardzuweisung erfolgt elementweise: d.h. für jedes Attribut des Objektes wird der entsprechende Zuweisungsoperator aufgerufen.
Der Zuweisungsoperator für Grunddatentypen bewirkt eine bitweise Kopie.
class Array
{ public: Array & operator=(const Array & neu);
};
{ public: Array & operator=(const Array & neu);
};
Es gibt zwei Formulierungen für den Aufruf:
Array a, b;
a = b; //impliziter Aufruf
a.operator=(b); //expliziter Aufruf
a = b; //impliziter Aufruf
a.operator=(b); //expliziter Aufruf
Ein Zuweisungsoperator soll immer das Objekt, für das der Operator aufgerufen wurde, als Referenz zurückliefern. Dies ermöglicht verkettete Zuweisungen.
Array a, b, c; a = b = c;
Im Zuweisungsoperator sollte getestet werden, ob ein Objekt sich selbst zugewiesen wird. Dies ermöglicht so genannter „Zuweisung an sich selbst“.
Array a; a = a;
Array & Array::operator=(const Array & neu)
{ if(this== & neu) return *this;
. . .
return *this;
}
Array & Array::operator=(const Array & neu)
{ if(this== & neu) return *this;
. . .
return *this;
}
typedef ... T;
class Array
{ private: T* z; int n;
public:
Array & operator=(const Array & neu)
{ if(this== & neu) return *this;
delete[] z; //alten Speicher freigeben
n=neu.n; z=new T[n]; //neuen reservieren
for(int i=0; i<n; i++) z[i]=neu.z[i]; //Kopie
return *this;
}
};
class Array
{ private: T* z; int n;
public:
Array & operator=(const Array & neu)
{ if(this== & neu) return *this;
delete[] z; //alten Speicher freigeben
n=neu.n; z=new T[n]; //neuen reservieren
for(int i=0; i<n; i++) z[i]=neu.z[i]; //Kopie
return *this;
}
};
⇒ Demo 12.
Auch der Funktionsaufruf ist ein Operator, der für eigene Klassen definiert werden kann.
Synonyme: Funktionsoperator, Klammeroperator, operator(), Funktionsaufrufoperator.
Im folgenden Prinzipbeispiel wird ein parameterlose unktionsoperator deklariert:
class XXX
{ …
public: void operator() ();
}; Ein Objekt mit der Funktionsoperator wird Funktionsobjekt der Funktor benannt. Die Funktionsobjekte werden in der C++-Standardbibliothek verwendet.
Der Funktionsoperator bringt überraschende Konsequenzen: ein Funktionsobjekt kann wie eine Funktion aufgerufen werden:
Es sieht so aus, als würde eine globale Funktion x1() aufgerufen. Tatsächlich wird der Funktionsoperator des Objektes x1 aufgerufen.
Der explizite Aufruf des Funktionsoperators ist syntaktisch ebenfalls erlaubt:
Die Funktionsobjekte können wie andere Methoden mehrfach überladen werden, z.B.:
class Polynom
{ float a, b, c;
public:
Polynom(float aa, float bb, float cc){a=aa; b=bb; c=cc;}
float operator( )(float x){ return a*x*x + b*x + c; }
float operator( )(float a, float b, float c, float x)
{ return a*x*x + b*x + c; // Funktionswert
}
};
int main()
{ Polynom po(5, 6, 7);
cout<< po(3)<<“ “<<po(3, -2, -7, 5); // 70 58
}
{ float a, b, c;
public:
Polynom(float aa, float bb, float cc){a=aa; b=bb; c=cc;}
float operator( )(float x){ return a*x*x + b*x + c; }
float operator( )(float a, float b, float c, float x)
{ return a*x*x + b*x + c; // Funktionswert
}
};
int main()
{ Polynom po(5, 6, 7);
cout<< po(3)<<“ “<<po(3, -2, -7, 5); // 70 58
}
Ein wichtiger Vorteil ist, dass ein Funktionsobjekt statt Funktionszeiger als Parameter zu einer Funktion übergeben werden könnte. Das ist die objektorientierte Alternative.
Beispiel:
double Doppelt (Polynom p, float x)
{ return 2*p(x);
}
int main()
{ Polynom po(5, 6, 7);
cout<< Doppelt(po, 3)<<" “
<< Doppelt(Polynom(3, -2, -7), 5); // 140 116
}
{ return 2*p(x);
}
int main()
{ Polynom po(5, 6, 7);
cout<< Doppelt(po, 3)<<" “
<< Doppelt(Polynom(3, -2, -7), 5); // 140 116
}
⇒ Demo 13, 14.
4.4. Iterator-Operatoren
Behälter und Iterator
Ein Behälter (container oder collection) ist ein Objekt, das zum Verwalten von anderen Objekten (Elementen) dient.
Jedes Element in einem Behälter besitzt eine bestimmte Position, in der es abgelegt ist. Man kann über die Position direkt auf das einzelne Element zugreifen. Die Darstellung der Position ist von der Implementierung des Behälters abhängig. Zeiger und Indexe sind bekannte Bespiele dafür.
Ein Iterator ist ein Objekt, das eine Position in einem Behälter repräsentiert (verweist, zeigt). Ein Iterator dient dazu, von den Implementierungsdetails des Behälters zu abstrahieren. Begriffsverwirrung: Mit dem Begriff „Iterator“ können auch eine Klasse und ein Konzept bezeichnet werden.
Prinzipbeispiel:
Iterator-Operatoren
1. Operatoren zum Iterieren (Durchwandern). Die Position, welche durch einen Iterator repräsentiert wird, wird zur Position des nächsten (oder vorigen) Elementes im Behälter gewechselt. Beispielsweise wechselt ein Inkrement-Operator++ auf Position des nächsten Elementes.
++ -- + - += -=
2. Vergleichsoperatoren. Zwei Iteratoren können verglichen werden, z.B. der Gleich-Operator== liefert true, wenn zwei Iteratoren auf dasselbe Element verweisen.
== != > <= >=
3. Zugriffsoperatoren. Das Element, welches durch einen Iterator repräsentiert wird, wird gelesen oder geändert (überschrieben). Beispiel: ein Inhalts-Operator* des Iterators liefert die Referenz auf das repräsentierten Element.
* -> [ ]
Iterator als geschachtelte Klasse
Prinzipbeispiel
typedef … T; Typ des Elementes
Anwendung von Iteratoren
1) Die Verbindung zwischen Behälter und Iterator wird mit den Methoden „start“ und „ende“ hergestellt.
Behaelter b;
Behaelter::iterator s= b.start();
Behaelter::iterator e= b.ende();
2) Ein Inkrement-Operator schiebt den Iterator auf die nächste Position: s++;
Vorteile von Iteratoren
1) Der Benutzer muss nicht kennen, welche Datenstruktur der Behälter besitzt.
2) Wenn eine Iterator-Klasse als eine geschachtelte Klasse definiert ist, dann ist sie ein Bestandteil der Behälterdefinition. Dann muss ein Benutzer viele Besonderheitendes Iterators nicht kennen und kann den Iterator über üblichen Namen ansprechen.
Prinzipbeispiel:
main()
{
Behaelter b; //Behaelter-Definition
Behaelter::iterator i; //Iterator-Definition
for (i=b.start( ); i!=b.ende( ); ++i)
cout<<*i<<" ";
}
{
Behaelter b; //Behaelter-Definition
Behaelter::iterator i; //Iterator-Definition
for (i=b.start( ); i!=b.ende( ); ++i)
cout<<*i<<" ";
}
⇒ Demo 13
Inhalts-Operator
Der Inhaltsoperator * (Dereferenzierungsoperator, Verweisoperator, Indirektionsoperator) liefert eine Referenz auf das Element, an dessen Position sich der Iterator befindet.
Prinzipbeispiel:
typedef ... T; //Typ des Elementes
class Behaelter {
...
public:
class iterator {
T* c; //Zeiger auf aktuelles Element
public:
T & operator* (void) {return *c;}
};
class Behaelter {
...
public:
class iterator {
T* c; //Zeiger auf aktuelles Element
public:
T & operator* (void) {return *c;}
};
Die Verwendung des Inhaltsoperators *i bedeutet einen lesenden oder schreibenden Zugriff auf das Element, deren Position der Iterator i enthält.
Beispiel:
typedef … T; //Typ des Elementes
main()
{ Behaelter b;
Behaelter::iterator i= b.start( ); //Initialisierung
T tmp= *i; //lesender Zugriff auf das Element
*i = tmp; //schreibender Zugriff auf das Element
}
main()
{ Behaelter b;
Behaelter::iterator i= b.start( ); //Initialisierung
T tmp= *i; //lesender Zugriff auf das Element
*i = tmp; //schreibender Zugriff auf das Element
}
Anmerkung:
Der Operator * kann getrennt unär (Inhaltsoperator) und binär (Multiplikationsoperator) überladen werden.
Inkrement-Operator
Die Position, welche ein Iterator repräsentiert, wird zur Position des nächsten Elementes im Behälter gewechselt. Dabei ändert sich ein Iterator selbst.
Außerdem liefert dieser Operator einen Iterator als Ergebnis zurück. Präfix-Operator liefert den veränderten Iterator. Postfix-Operator liefert den ursprünglichen Iterator.
Beispiel für Präfix-Operator
class Behaelter
{ struct KN{ ...; KN* next;};
public:
class iterator
{ KN* c; //Zeiger auf aktuellen Knoten
public:
iterator & operator++() { c = c->next; return *this;}
};
};
{ struct KN{ ...; KN* next;};
public:
class iterator
{ KN* c; //Zeiger auf aktuellen Knoten
public:
iterator & operator++() { c = c->next; return *this;}
};
};
CategoryObjektorientierteProgrammierung