Menu

C++ Operatorüberladung: eigene +-, ==- und <<-Operatoren

Die Operatorüberladung in C++ lässt deine eigenen Typen mit eingebauten Operatoren wie +, == und << arbeiten. Lerne die Regeln für Member- vs. Nicht-Member-Funktionen, wie du Vergleichs- und Stream-Operatoren überlädst und welche Fallstricke es rund um Rückgabetypen und den Zuweisungsoperator gibt.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

Lass deine Typen wie eingebaut wirken

Du weißt bereits, dass du mit std::string a + b zum Verketten und cout << s zum Ausgeben schreiben kannst. Das sind keine besonderen Compiler-Tricks - es sind ganz normale Funktionen mit komischen Namen. Die Operatorüberladung ist das Feature, mit dem sich deine Klassen an dieselbe Syntax andocken, sodass sich ein Vector2- oder Money-Typ genau wie ein int addieren, vergleichen und ausgeben lässt.

Der Mechanismus ist einfach, sobald man ihn durchschaut: Ein Ausdruck wie a + b ist eine Kurzschreibweise. Der Compiler schreibt ihn in einen Aufruf einer Funktion namens operator+ um und sucht eine, die zu den Operandentypen passt. Definiere diese Funktion für deine Klasse, und a + b funktioniert plötzlich. Das ist eigentlich eine spezialisierte Form der Funktionsüberladung - es gelten dieselben Namensauflösungsregeln, nur mit operatorförmigen Namen.

Beachte, dass die Funktion beide Operanden per const& nimmt: Arithmetik sollte ihre Eingaben nicht verändern, und Referenzen vermeiden Kopien. Sie gibt einen neuen Vector2 per Wert zurück - p + q muss ein frisches Ergebnis liefern, ohne p oder q anzutasten, genau wie 2 + 3 die 2 nicht verändert.

Member vs. Nicht-Member

Es gibt zwei Orte, an denen man einen Operator definieren kann: als Member der Klasse oder als freie (Nicht-Member-)Funktion. Als Member ist der linke Operand das implizite this, also nimmt ein binärer Operator nur einen expliziten Parameter:

Das const nach der Parameterliste ist wichtig: a + b sollte a nicht verändern, also wird der Member als const markiert. Verwende die Member-Form für Operatoren, die untrennbar mit dem linken Operanden verbunden sind und auf ihm keine Konvertierungen brauchen - +=, [], (), -> und unäre Operatoren wie -x oder ++x.

Der Haken bei Membern: Der linke Operand kann nicht konvertiert werden. Mit dem Member-operator+ oben funktioniert a + 50 (50 wird für die rechte Seite zu Money konvertiert), aber 50 + a lässt sich nicht kompilieren - der linke Operand 50 ist ein int, und du kannst int keine Member-Funktion hinzufügen. Ein Nicht-Member-Operator behebt das, weil beide Operanden explizite Parameter sind und beide konvertiert werden können:

Faustregel: Mache symmetrische binäre Operatoren (+, ==, *) zu Nicht-Membern, damit Konvertierungen auf beiden Seiten funktionieren; mache Operatoren, die den linken Operanden verändern müssen oder an ihn gebunden sind (+=, [], =), zu Membern.

Den Stream-Operator überladen

Der mit Abstand am häufigsten überladene Operator ist << zur Ausgabe. Du kannst ihn nicht zum Member deiner Klasse machen, weil der linke Operand ein std::ostream ist (wie cout), nicht dein Typ - und ostream gehört dir nicht. Er ist daher immer ein Nicht-Member, der den Stream per nicht-konstanter Referenz nimmt und ihn zurückgibt:

Zwei Details machen das möglich. Der Stream wird per Referenz (ostream&) übergeben und zurückgegeben - Streams lassen sich nicht kopieren, und dass derselbe Stream zurückgegeben wird, erlaubt überhaupt erst das Verketten von cout << "p = " << p << "\n". Jedes << gibt den Stream zurück, damit das nächste << etwas hat, an das es sich binden kann. Vergiss das return os;, und die Verkettung bricht.

Vergleichsoperatoren

Um deine Objekte mit ==, < und Co. zu vergleichen, überlädst du die Vergleichsoperatoren. Vor C++20 hast du jeden einzelnen von Hand geschrieben; der entscheidende Fallstrick ist, dass operator< einen bool zurückgeben und eine konsistente Ordnung definieren muss:

Alle sechs Vergleiche (==, !=, <, <=, >, >=) von Hand zu schreiben ist mühsam und fehleranfällig. C++20 hat den Dreiwege-Vergleichsoperator <=> (das „Raumschiff") eingeführt. Setzt du ihn zusammen mit == auf default, werden alle Vergleiche für dich generiert:

= default weist den Compiler an, die Member in Deklarationsreihenfolge zu vergleichen, was genau der lexikografischen Ordnung entspricht, die du von Hand schreiben würdest. Auf modernen Compilern ist das die bevorzugte Variante.

Der Zuweisungsoperator und seine Tücken

operator= (Kopierzuweisung) ist besonders: Der Compiler erzeugt einen für dich, und bei einfachen Klassen ist dieser Standard korrekt. Du musst nur dann einen eigenen schreiben, wenn deine Klasse eine Ressource verwaltet - rohen Speicher, ein Datei-Handle -, bei der eine Kopie Member für Member falsch wäre. Die kanonische Signatur gibt *this per Referenz zurück, damit sich Zuweisungen verketten lassen (a = b = c):

In dieser kurzen Funktion lauern zwei Tücken. Erstens die Selbstzuweisungsprüfung if (this == &other): Ohne sie würde a = a delete[] data ausführen und dann aus dem soeben freigegebenen other.data lesen - undefiniertes Verhalten. Zweitens kommt es auf die Reihenfolge an - in einer handgeschriebenen Version darfst du den alten Puffer nicht löschen, bevor du den neuen sicher kopiert hast (eine echte Implementierung allokiert oft zuerst oder verwendet das copy-and-swap-Idiom, sodass eine fehlgeschlagene Allokation das Objekt unversehrt lässt).

Ein allgemeinerer Fallstrick: Überlade Operatoren nicht auf überraschende Weise. Ein operator+, der heimlich seinen linken Operanden verändert, oder ein operator==, der nicht symmetrisch ist, verwirrt jeden Leser und bricht Standardbibliothekscode, der die üblichen Bedeutungen voraussetzt. Überlade Operatoren nur, wenn die Operation für deinen Typ wirklich „additionsartig" oder „gleichheitsartig" ist.

Als Nächstes: Zugriffsspezifizierer

Beachte, wie jedes Beispiel seine Datenmember private gehalten und Verhalten über eine kleine öffentliche Oberfläche - Konstruktoren, Operatoren und ein paar Methoden - bereitgestellt hat. Diese Grenze zwischen dem, was für die Außenwelt sichtbar ist, und dem, was im Inneren der Klasse verborgen bleibt, wird von Zugriffsspezifizierern gesteuert: public, private und protected. Als Nächstes sehen wir uns genau an, was jeder erlaubt, warum private-Daten mit öffentlichen Methoden der Standard für gute Kapselung sind und wie protected in die Vererbung passt.

Häufig gestellte Fragen

Was ist Operatorüberladung in C++?

Mit der Operatorüberladung kannst du festlegen, was eingebaute Operatoren wie +, == oder << für deine eigenen Typen bedeuten. Du schreibst eine speziell benannte Funktion - operator+, operator== usw. - und der Compiler ruft sie auf, sobald der Operator mit Operanden deiner Klasse auftaucht. So verkettet string + string und so gibt cout << obj ein eigenes Objekt aus.

Sollten Operatoren in C++ Member-Funktionen oder Nicht-Member-(friend-)Funktionen sein?

Verwende eine Member-Funktion, wenn der linke Operand deine eigene Klasse ist und keine Konvertierungen braucht (z. B. +=, [], ()). Verwende eine Nicht-Member-Funktion (oft friend), wenn der linke Operand ein eingebauter Typ sein kann oder wenn du symmetrische Konvertierungen auf beiden Seiten willst - das ist für operator<< zwingend, weil der linke Operand ein std::ostream ist und nicht deine Klasse.

Welche C++-Operatoren lassen sich nicht überladen?

Du kannst :: (Bereichsauflösung), . (Memberzugriff), .* (Member-Zeiger-Zugriff), ?: (ternär) und sizeof nicht überladen. Du kannst auch keine völlig neuen Operatoren erfinden und weder die Stelligkeit noch die Priorität eines Operators ändern - + ist immer binär mit derselben Priorität, egal ob es int-Werte oder dein Vector2 addiert.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S