Advanced Java Services | operatoroverloading |
Sinnvoll eingesetztes Operatoroverloading kann die Lesbarkeit eines Programms deutlich verbessern. Für die meisten Opertoren gibt es zwei Möglichkeiten der Überladung, sie können entweder als friend-Funktionen überladen werden oder innerhalb von Klassen als Methoden (member-Funktionen).
Wir werden den Operator + überladen. Das Schema ist aber für alle binären Operatoren dasselbe.
In der Headerdatei
class Xxx
{
//...
public:
//...
friend Xxx & operator+(const Xxx & a, const Xxx & b);
}
Implementierung in der cpp-Datei außerhalb der Klasse
Xxx & operator+(const Xxx & a, const Xxx & b) { //... }
In der Headerdatei
class Xxx
{
//...
public:
//...
Xxx & operator+(const Xxx & a);
}
Implementierung innerhalb der Klasse, der erste Operand ist *this der zweite Operand b
Xxx & Xxx::operator+(const Xxx & b) { //... }
Wir nennen den zu überladenen Operator @. Das Schema ist für alle unären Operatoren dasselbe.
In der Headerdatei
class Xxx
{
//...
public:
//...
friend Xxx & operator@(const Xxx & a);
}
Implementierung in der cpp-Datei außerhalb der Klasse
Xxx & operator@(const Xxx & a) { //... }
In der Headerdatei
class Xxx
{
//...
public:
//...
Xxx & operator@();
}
Implementierung innerhalb der Klasse, der Operand ist *this
Xxx&Xxx::operator@() { //... }
Für diese Operatoren gilt zwar das Schema für unäre Operatoren, jedoch müssen die Fälle "++x" und "x++" bzw. "--x" und "x--" unterschieden werden.
Das folgende kleine Beispiel überlädt den Prefixoperator einmal als member und einmal als friend.
// member class Xxx { public: int a; Xxx & operator++(); }; Xxx & Xxx::operator++() { ++a; return *this; } // friend class Yyy { public: int b; friend Yyy & operator++(Yyy &); }; Yyy& operator++(Yyy & y) { ++y.b; return y; } int main() { Xxx x; x.a = 17; cout << ++x.a << endl; // 18 Yyy y; y.b = 17; cout << ++y.b << endl; // 18 return 0; }
Das folgende kleine Beispiel überlädt den Postfixoperator einmal als member und einmal als friend. Zur Unterscheidung des vorigen Falls wird ein zusätzlicher Parameter vom Typ int übergeben der nie verwendet wird.
// member class Xxx { public: int a; Xxx & operator++(int i); }; Xxx & Xxx::operator++(int i) { a++; // hier egal; return *this; } // friend class Yyy { public: int b; friend Yyy & operator++(Yyy &, int i); }; Yyy & operator++(Yyy & y, int i) { ++y.b; return y; } int main() { Yyy y; y.b = 17; cout << y.b++ << endl; // 17 cout << y.b << endl; // 18 return 0; }
Für die Pre- und Postfixüberladungen wird die Variante mit Memberfunktionen bevorzugt.
Der Operator << kann nur über eine friend-Funktion überladen werden. Sie wird der Klasse wie folgt bekanntgemacht.
class Xxx { //.. public: //.. friend std::ostream& operator<< (std::ostream& out, const Xxx& xxx); };
Das Schema der Implemetierung
std::ostream& operator<< (std::ostream& out, const Xxx& xxx) { out << ... return out; }
Der Indexoperator kann nur über Memberfunktionen überladen werden. Es gibt eine Memberfunktion für Lesen und eine für Schreiben.
class Xxx
{
//...
// [] überladen
Xxx& operator[](int i); // schreibender Zugriff
const Xxx& operator[](const int i) const; // lesender Zugriff
};
Implementierung
// schreiben Person& PersonList::operator[](int i) { //... } // lesen const Person& PersonList::operator[](const int i) const { //... }
Es ist allerdings compilerabhängig, ob die lese-Variante verwendet wird . VS 2010 und MInGW verwenden immer nur die schreibende Variante.
Mit dem function-call Operator ist es möglich ein Object wie eine Funktion zu verwenden. Dies wird gerne verwendet im Zusammenhang mit Templates. Beispiele hierzu findet man bei Uniquepointer und bei Sharedpointer. Der function-call Operator wird leicht verwechselt mit einem Konstruktoraufruf, hier muß man genau zwischen der Initialisierung und der Ebene der Statements unterscheiden. Ein einfaches Beispiel wird den Unterschied deutlich zeigen.
Unsere Beispielklasse ( die Operatorfunktion operator() wird auch gerne Functor genannt)
class Xxx { private: int x; public: Xxx(int x = 0) : x(x) { cout << "Xxx(int x = 0) " << endl; } // function-call operator mit einem Argument void operator()(int i) { cout << "operator()(int)" << endl; } // function-call operator mit zwei Argumenten in der Parameterliste int operator()(int i, int j) { cout << "operator()(int, int)" << endl; return i+j; } };
Das Programm
/* * Verwendet Xxx */ void beispiel1() { cout << "oberloading function call" << endl << endl; // // Konstruktoraufrufe Xxx foo; Xxx bar(17); // function call Aufrufe foo(17); bar(71); cout << foo(17, 42) << endl << endl; }
Die Ausgabe
Das Beispiel verwendet zwei Überladungen des function-call Operators. Die Parameterlisten können beliebig sein, solange sie unterscheidbar sind.
Der function-call Operator kann auch als Indexoperator fungieren, wenn der Returntyp eine Referenz ist. In diesem Falle macht er den Indexoperator überflüssig. Das nächste Beispiel zeigt dies.
class IntArray { private: int *ptr; public: IntArray(size_t size) : ptr(new int[size]) { cout << "IntArray(size_t size)" << endl; } ~IntArray() { delete[] ptr; } // schreiben int& operator[](int i) { cout << "operator[](int) write" << endl; return ptr[i]; } // function call int& operator()(int index) { cout << "operator()(int)" << endl; return ptr[index]; } };
Das Programm
/* * verwendet IntArr */ void beispiel2() { cout << "oberloading function call" << endl << endl; // IntArray intarr(10); // write operator[] intarr[5] = 17; // read operator[] cout << intarr[5] << endl; // write operator() intarr(6) = 42; // read operator() cout << intarr(6) << endl; }
Die Ausgabe
Der cast-Operator kann nur als nichtstatische member-Funktion überladen werden. Die Überladung hat die folgende Form
operator <zieldatentyp>()
Man beachte, daß es keinen Returntyp gibt. Trotzdem muß der Funktionsrumpf mit einer Return-Anweisung enden.
In der folgenden Header-Datei deklarieren wir eine Klasse Test. Die Klasse hüllt ein int ein und enthält einen cast-Operator sowie eine friend-Funktion für die Ausgabe.
#pragma once
#include <iostream>
using namespace std;
class Test
{
int a;
public:
Test(void) : a(0)
{
cout << "default constructor" << endl;
}
Test(int i) : a(i)
{
cout << "1-arg ctor" << endl;
}
// overloaded int cast , no return type, must be a non static member function, converts Test to int
operator int() const
{
cout << "operator int()" << endl;
return a;
}
friend ostream & operator<<(ostream &out, const Test& p)
{
cout << "operator<<()" << endl;
return out << p.a ;
}
virtual ~Test(void)
{}
};
Ein Beispielprogramm
int main() { printf("overloading the cast-operator\n\n"); Test t(17); int a = t; cout << t << endl; }
Die Ausgabe
Man beachte, daß in diesem Fall die Funktion opertor<< nicht notwendigerweise überladen werden muß, da für die Ausgabe auch der cast-Operator verwendet werden kann. Kommentiert man die friend-Funktion aus, so ergibt sich die folgende Ausgabe.
Ein automatischer cast kann unerwartete Nebeneffekte provozieren. Mit Hilfe des Schlüsselworts explicit kann der Entwickler implizites casten unterbinden. In diesem Fall muß der cast dann explizit gesetzt werden. Durch ergänzen von explicit am Anfang der Signatur kann man den impliziten cast verhindern. Hat man die Ausgabe überladen braucht man an dieser Stelle aber keinen expliziten cast. Das folgende Beispiel zeigt dies.
Wir ändern im Header nur den cast-Operator und ergänzen hier explicit.
... // overloaded int cast, using explicit to prevent implicit casts explicit operator int() const { cout << "operator int()" << endl; return a; } ...
In main muß nun explizit gecastet werden.
int main()
{
printf("overloading the cast-operator\n\n");
Test t(17);
int a = (int)t;
cout << t << endl;
}
Im folgenden kleinen Beispiel werden new und delete für eine Klasse überladen. In diesem Falle werden die Operatorfunktionen vom Compiler implizit static angelegt.
Die Klasse
class Test { public: int i; Test(int i=0) : i(i) {} // allocate void * operator new(size_t size) // implizit static { cout << "new size = " << size << endl; return malloc(size); } // delete void operator delete(void * mem) // implizit static { cout << "delete " << endl; free(mem); } };
Ein Testprogramm
int main() { cout << "new and delete" << endl; // Test * pt = new Test(42); cout << pt->i << endl; delete pt; return 0; }
Die Ausgabe
Verwendet man new für Standarddatentypen, etwa mit int * arr = new int;, so wird ein globales new bzw. ein globales delete verwendet. Auch diese Operatorfunktionen kann man überschreiben. In einfacher Form sieht das so aus:
// global void* operator new(size_t sz) { cerr << "allocating " << sz << " bytes\n"; void* mem = malloc(sz); if (mem) return mem; else throw std::bad_alloc(); } void operator delete(void* ptr) { cerr << "deallocating at " << ptr << endl; free(ptr); }
Diese Funktionen sind dem Compiler bekannt, eine Deklaration ist nicht notwendig. Zum Testen verwendet man new und delete wie bisher.
int main() { cout << "new and delete global" << endl; // int * arr = new int; delete arr; return 0; }
Die Ausgabe
Der Zuweisungsoperator hat die folgende feststehende Signatur.
Xxx& operator=(const Xxx& a)
Hat eine Klasse mehrere Datenmember, so muß meistens der Zuweisungsoperator überladen derden, da der Defaultzuweisungsoperator lediglich eine flache Kopie erzeugt. In diesem Zusammenhang reicht dann auch der Defaultkopierkonstruktor nicht aus und muß ebenfalls überladen werden. Ebenso muß in dieser Situation meist Speicher freigegeben werden was bedeutet, daß auch der Destruktor selbst geschrieben werden muß. Dies hat zu der in der Überschrift erwähnten "rule of three" geführt.
rule of three
Falls man eine der drei Funktionen Defaultkopierkonstruktor, Defaultzuweisungsoperator, Destruktor überschreibt,so ist es in der Regel notwendig, auch die beiden anderen Funktionen selbst zu implementieren.
Auch der Kopierkonstruktor hat eine feststehende Signatur.
Xxx(const Xxx& a)
Im nächsten Beispiel zeigt die Folgen einer flachen Kopie, die der Defaultzuweisungsoperator macht.
class X { public: char* name; X(char* name) { this->name = new char[strlen(name)+1]; strcpy(this->name, name); } };
Eine kleine Testfunktion zeigt den Fehler
void testX()
{
X x("hallo");
cout << x.name << endl; // hallo
X y = x;
cout << y.name << endl; // hallo OK
x.name[1] = 'e';
cout << x.name << endl; // hello
cout << y.name << endl; // hello NOK !!
}
Der Fehler wird behoben in dem wir die rule of three beherzigen.
class XX { public: char* name; // constructor XX(char* name) { this->name = new char[strlen(name)+1]; strcpy(this->name, name); } // copy-constructor XX(const XX& a) { this->name = new char[strlen(a.name)+1]; strcpy(this->name, a.name); } // Zuweisungsoperator XX& operator=(const XX& a) { this->name = new char[strlen(a.name)+1]; strcpy(this->name, a.name); } // Destruktor ~XX() { delete name; } void println() { cout << this->name << endl; } };
Die Testfunktion zeigt den Erfolg
void testXX()
{
XX xx("hallo");
xx.println(); // hallo
XX yy = xx;
yy.println(); // hallo OK
xx.name[1] = 'e';
xx.println(); // hello OK
yy.println(); // hallo OK !!
}
Die die meisten Operatoren überladen werden können, hier eine Tabelle der nicht überladbaren Operatoren.
Präzedenz | Operator | Überladbar | Bezeichnung | Assoziativität | Operandentyp |
---|---|---|---|---|---|
1 | Bereichsauflösung [ C++ ] | ||||
:: | nein | Bereichsauflösung | links | Klasse, Struktur, Union, Member | |
. | nein | Punktoperator | links | Klasse, Struktur, Union, Member | |
const_cast<>( ) | nein | Constcast [ C++ ] | links | Variable beliebigen Datentyps | |
dynamic_cast<>( ) | nein | Dynamiccast [ C++ ] | links | Variable beliebigen Datentyps | |
reinterpret_cast<>( ) | nein | Reinterpretcast [ C++ ] | links | Variable beliebigen Datentyps | |
static_cast<>( ) | nein | Staticcast [ C++ ] | links | Variable beliebigen Datentyps | |
typeid( ) | nein | typeid [ C++ ] | links | Variable beliebigen Datentyps | |
3 | Unäre Operatoren | ||||
sizeof | nein | Größenoperator | rechts | alle | |
15 | bedingter Ausdruck | ||||
? : | nein | bedingter Ausdruck | rechts | boolesch ? alle : alle | |
17 | Exception werfen [ C++ ] | ||||
throw | nein | Exception werfen | rechts | Exceptionobjekte |