Bereits in der Grundschule wirst du mit Operatoren konfrontiert. Für gewöhnlich lernst du dort die Klassiker \(+\), \(-\), \(\times\) und \(\div\) kennen. Im Verlauf deiner
Mathematiker-Karriere wird dieses so früh erlernte Wissen allerdings infrage gestellt, denn wieso sollte man den \(+-\)Operator auf das bloße Addieren zweier Zahlen beschränken? Man könnte eine
Addition doch auch auf Objekte erweitern und so z. B. die Summe zweier Vektoren bilden. Wenn du die Multiplikation, die du mit dem Mal-Punkt \(\cdot\) und dem Kreuz \(\times\) darstellen kannst,
allgemein betrachtest, verbirgt sich dahinter eine Verknüpfung, die wie folgt definiert $$\cdot :M\times M\mapsto M, (a,b)\mapsto a\cdot b$$ wobei \(M\) eine Menge ist. Wenn \(M\) z. B. die reellen
Zahlen \(\mathbb{R}\) sind, dann kannst du via $$\cdot:\mathbb{R}\mapsto\mathbb{R}, (a,b)\mapsto a\cdot b$$ die Multiplikation auf den reellen Zahlen definieren. Doch worauf beruft sich die Uni bei
dem Mal-Punkt, der hinter dem geordneten Paar \((a,b)\) auftaucht? Nun, dafür hast du in der Grundschule mal eine riesige Verknüpfungstabelle kennengelernt, in der definiert wurde, dass z. B.
\(2\cdot 4=8\) oder \(5\cdot 5=25\) ist. Man ist bei den Mengen jedoch nicht auf die reellen Zahlen festgelegt und so kann der Multiplikationsoperator in verschiedenen Situationen unterschiedliche
Bedeutungen haben. Und genau so ist es auch in der Programmierung. Deshalb bietet die Programmiersprache C++ die Möglichkeit, Operatoren zu überladen. Im Englischen nennt man das Operator
Overloading.
Mit Hilfe des Überladens von Operatoren kannst du für Objekte selbst definierter Klassen (fast) alle Operatoren mit einer neuen Bedeutung versehen. Diese Bedeutung kapselst du in
speziellen Funktionen bzw. Methoden. Der Grundgedanke ist (wie der Name bereits vermuten lässt) dem des Überladens von Funktionen bzw. Methoden sehr ähnlich.
Insgesamt können diese \(42\) Operatoren überladen werden:
+
|
-
|
*
|
/
|
%
|
^
|
&
|
|
|
~
|
!
|
,
|
=
|
<
|
>
|
<=
|
>=
|
++
|
--
|
<<
|
>>
|
==
|
!=
|
&&
|
||
|
+=
|
-=
|
/=
|
%=
|
^=
|
&=
|
|=
|
*=
|
<<=
|
>>=
|
[]
|
()
|
->
|
->*
|
new
|
new []
|
delete
|
delete []
|
Wenn du z. B. den >-Operator überlädst, könntest du deine Kollegen trollen und ihm die Funktionalität "kleiner als" (<) zuweisen. Du könntest aber auch den Inkrement-Operator ++
dadurch so umschreiben, dass er statt 1 jeweils 2 auf die Zahl addiert, hinter der er steht. Du solltest aber tunlichst davon absehen, den Zugriffsoperator -> zu überladen, da das ganz
gruselige Effekte in der späteren Programmierung haben kann (insbesondere dann, wenn einer deiner Kollegen deinen Code warten bzw. weiterentwickeln darf). Stelle dir (und das gilt grundsätzlich)
am besten vor, dass die Kollegen, die mit deinem Code arbeiten sollen, gewaltbereite Psychopathen sind, die deine Adresse haben. Dann wirst du recht schnell selbstständig vom Überladen dieses
Operators absehen.
Nicht überladen werden können übrigens die vier Operatoren
::
.
.*
und der Elvis-Operator :?/.
Für jeden dieser Operatoren ein eigenes Code-Beispiel bzw. ein sinnvolles Einsatzszenario zu finden, würde den Rahmen dieses Artikels sprengen. Du solltest dennoch wissen, das die Syntax zum
Überladen einzelner Operatoren vom Operator-Typ abhängt. Binäre Operatoren, also Operatoren, die zwischen zwei Operanden stehen (wie bspw. der Modulo-Operator %), werden anders überladen als
unäre Operatoren, die vor einem Operanden stehen (wie der für die logische Verneinung). Manche Operatoren sind (je nach Kontext) binär oder unär (oder auch nichts von beidem, wie z. B. der
new-Operator). Der * bedeutet binär die Multiplikation zweier Variablen, während er unär einer Variable vorangestellt eine Dereferenzierung bewirkt. Achte also auf die
Compiler-Hinweise oder siehe in den Docs nach.
Die von C++ vorgegebene Operatorrangfolge (also die Bindungsstärken bzw. die Reihenfolge, in der die einzelnen Operatoren ausgewertet werden) bleibt übrigens erhalten und kann nicht geändert
werden. Das logische NICHT ! bindet also stärker als z. B. das logische UND &&.
Du sollst nun anhand eines praktischen Beispiels, nämlich für eine Vektorklasse, lernen, wie du die Operatoren + und << (für die Ausgabe) überladen kannst. Du bist damit in der Lage,
Vektoren auf eine intuitive Art und Weise im Code zu addieren bzw. durch das Schreiben auf den Ausgabestrom anzuzeigen.
Für die Vektorklasse importierst du dir zunächst <iostream> und legst den Namespace std fest, um im Code nicht an verschiedenen Stellen ständig das std voranstellen zu müssen. Dann
schreibst du eine einfache Vektor-Klasse, die drei Double-Werte zur Angabe der \(x-\), \(y-\) und \(z-\)Koordinate enthält. Diese werden im Konstruktor der Klasse übergeben:
class Vector{
public:
double x;
double y;
double z;
Vector(double x, double y, double z): x(x), y(y), z(z) {}
};
In der Main kannst du dir nun zwei Vektoren \(v_1=\left(\begin{matrix}1\\2\\3\end{matrix}\right)\) und \(v_2=\left(\begin{matrix}4\\4\\0\end{matrix}\right)\) definieren und diese dann addieren:
int main(){
Vector v1(1,2,3);
Vector v2(4,4,0);
Vector v3 = v1 + v2;
}
Wenn du diesen Code nun ausführst, läufst du direkt in eine Fehlermeldung, da C++ den Operator \(+\) nicht für die von dir definierte Vektorklasse kennt. Diesen musst du erst implementieren. Dafür
definierst du dir eine Funktion, die mit dem Keyword operator eingeleitet wird. Überladene Operatorfunktionen besitzen als Rückgabetyp den Typ der Klasse, in der sie überladen werden (in
diesem Fall also Vector, da der \(+-\)Operator für Vektoren überladen werden soll). An das Keyword operator folgt der Operator, der überladen werden soll (hier das \(+\)). Du erinnerst
dich, dass ich geschrieben habe, dass die Signatur davon abhängt, ob z. B. ein binärer oder unärer Operator vorliegt, oder? Das kommt hier jetzt zum Tragen. Die Signatur für einen binären Operator
erfordert einen Übergabeparameter vom Typ deiner Klasse (hier Vector).
Innerhalb der Funktion definierst du dir einen Ergebnisvektor und berechnest die einzelnen Koordinaten des neuen Vektors durch Addition der jeweiligen Koordinaten des übergebenen Vektors zu den
Objektvariablen. Du musst die Sicht eines Vektors einnehmen, auf den ein anderer Vektor addiert wird. Zum Schluss gibst du den Ergebnisvektor zurück. Es werden für einen binären Operator also nicht
zwei, sondern nur ein Parameter benötigt. Für einen unären Operator dementsprechend keiner. Das this könntest du auch weglassen.
Vector operator+ (const Vector &vector){
Vector result(0,0,0);
result.x = this->x + vector.x;
result.y = this->y + vector.y;
result.z = this->z + vector.z;
return result;
}
Wenn du das Programm jetzt erneut ausführst, ist die Fehlermeldung von vorhin verschwunden. Allerdings weißt du jetzt noch nicht, ob die Addition auch zum richtigen Ergebnis, also dem Vektor
$$v_3=\left(\begin{matrix}1\\2\\3\end{matrix}\right)+\left(\begin{matrix}4\\4\\0\end{matrix}\right)=\left(\begin{matrix}5\\6\\3\end{matrix}\right)$$ führt. Statt einer print()-Fuktion, kannst
du auch den Operator << überladen und deine Vektoren direkt in menschenlesbarer Form auf den Ausgabestrom schreiben. Du überlädst dabei jedoch nicht den <<-Operator der Klasse
Vector (diesen gibt es nämlich nicht), sondern den der Klasse Output-Stream (ostream). Deshalb musst du als Rückgabetyp ostream& verwenden. Als Übergabeparameter erhält
dieser Operator eine Referenz auf den Output-Stream und eine auf den Vektor, der ausgegeben werden soll. Dann baust du dir die gewünschte Darstellung zusammen und gibst am Ende den Output-Stream
zurück.
ostream& operator<<(ostream& stream, const Vector& vector){
stream << "(" << vector.x << "," << vector.y << "," << vector.z << ")";
return stream;
};
Wenn du den Vektor jetzt mit diesem überladenen Operator durch diese Zeile in der main()
ausgeben willst, sollte eigentlich "(5,6,3)" auf die Konsole gedruckt werden. Stattdessen wirst du mit einer Fehlermeldung bestraft. Das liegt daran, dass du den Funktion zum Übleraden deines
Ausgabeoperators innerhalb der Vektorklasse als friend definieren musst.
friend ostream& operator<<(ostream& stream, const Vector& vector){
stream << "(" << vector.x << "," << vector.y << "," << vector.z << ")";
return stream;
};
Alternativ kannst du die Funktion auch einfach außerhalb deiner Klasse definieren und solltest dann auf das friend verzichten. Beim Ausführen dieses Programms wird nun der richtige
Wert ausgegeben.
Das Überladen von Operatoren bietet dir die Möglichkeit, die visuelle Darstellung deines Programms intuitiver zu gestalten.
Das Testprogramm sieht dann final wie folgt aus:
#include <iostream>
using namespace std;
class Vector{
public:
double x;
double y;
double z;
Vector(double x, double y, double z): x(x), y(y), z(z) {}
Vector operator+ (const Vector &vector){
Vector result(0,0,0);
result.x = this->x + vector.x;
result.y = this->y + vector.y;
result.z = this->z + vector.z;
return result;
}
friend ostream& operator<<(ostream& stream, const Vector& vector){
stream << "(" << vector.x << "," << vector.y << "," << vector.z << ")";
return stream;
};
};
int main(){
Vector v1(1,2,3);
Vector v2(4,4,0);
Vector v3 = v1 + v2;
cout << v3;
}