C++ Tutorial

Überladen des Zuweisungsoperators

Für Klassen lassen sich fast alle Operatoren so umdefinieren, dass sie im Zusammenspiel mit Objekten eine beliebig definierbare Aktion ausführen. Zum Einstieg in das Überladen von Operatoren sehen wir uns das Überladen des Zuweisungsoperators '=' an.

Um den Zuweisungsoperator '=' für eine Klasse zu überladen, ist folgende Methode zu implementieren:

CAny& operator = (const DTYP& param);

CAny ist die Klasse, für die der Zuweisungsoperator überladen wird.

Nach dem Returntyp folgt das Schlüsselwort operator und dann der zu überladende Operator, hier '='. Innerhalb der Parameterklammer steht der rechte Operand des Operators. Somit ist folgende Zuweisung definiert:

AnyObject = DTYP(ausdruck);

Im nachfolgenden Beispiel wird der Zuweisungsoperator für die Klasse Window definiert, um Objekte dieser Klasse einander zuzuweisen.

1: class Window
2: {
3:    char *pTitle; // Fenstertitel
4: public:
5:    ... // ctor & dtor
6:    // Zuweisungsoperator
7:    Window& operator = (const Window& src);
8: };
9: ...
10: // Zuweisungsoperator
11: Window& Window::operator = (const Window& src)
12: {
13:    // Bisherigen Titel freigeben
14:    delete [] pTitle;
15:    // Neuen Titel übernehmen
16:    pTitle = new char[strlen(src.pTitle)+1];
17:    strcpy_s(pTitle, strlen(src.pTitle)+1, src.pTitle);
18:    return *this;
19: }

Der überladene Zuweisungsoperator '=' muss eine Referenz auf das aktuelle Objekt zurückliefern. Dies erfolgt durch Dereferenzierung des Zeigers this. Warum hier eine Referenz zurückgeliefert werden muss, soll anhand eines Beispiels demonstriert werden.

Angenommen, wnd1, wnd2 und wnd3 sind Objekte vom Typ Window. Nach dem Überladen des Zuweisungsoperators ist dann folgende Anweisung möglich (Mehrfachzuweisung!):

wnd3 = wnd2 = wnd1;

Dieser Ausdruck wird vom Compiler in zwei Teilausdrücke zerlegt. Zuerst wird der Teilausdruck wnd2=wnd1 berechnet. Dies führt zum Aufruf des überladenen Zuweisungsoperators für das Objekt wnd2 (linker Operand), wobei wnd1 (rechter Operand) als Parameter an die Methode übergeben wird. Das Ergebnis dieses Ausdrucks wird nun als neuer rechter Operand für den zweiten Teilausdruck wnd3=Ergebnis aus (wnd2=wnd1) eingesetzt. Und dieses 'Ergebnis' ist der Inhalt des Objekts wnd2 nach der Auswertung des ersten Teilausdrucks.

Regel der großen 5

Im Zusammenhang mit dem Überladen des Operators = soll die sogenannte "Regel der großen 5" nicht unerwähnt bleiben:

Ist eine der Methoden

  • Kopierkonstruktor
  • Kopier-Zuweisungsoperator
  • Move-Konstruktor
  • Move-Zuweisungsoperator
  • Destruktor

notwendig, sind in der Regel ebenfalls die anderen Methoden erforderlich!

Sehen wir uns dazu das nachfolgende vereinfachte Beispiel an, ohne die Move-Methoden. Die dort definierte Klasse Window enthält die dynamische Eigenschaft pTitle zum Abspeichern eines Fenstertitels. Der für den Fenstertitel benötigte Platz wird im Konstruktor reserviert. Damit wird automatisch der Destruktor notwendig, der den reservierten Speicherplatz wieder freigibt. Nach der obigen Regel sind nun auch der Kopierkonstruktor sowie der überladene (Kopier-)Zuweisungsoperator notwendig!

1: // Klassendefinition
2: class Window
3: {
4:    char *pTitle; // dynamische Eigenschaft!
5: public:
6:    // ctor, reserviert Platz für Titel
7:    Window(const char* const pT)
8:    {
9:       pTitle = new char[strlen(pT)+1];
10:      strcpy_s(pTitle,strlen(pT)+1,pT);
11:   }
12:   // dtor, gibt Platz für Titel frei
13:   ~Window()
14:   {
15:      delete [] pTitle;
16:   }
17:   // Kopierkonstruktor, ruft ctor auf
18:   Window(const Window& src): Window(src.pTitle)
19:   {
20:   }
21:   // Überladener Zuweisungsoperator
22:   Window& operator= (const Window& src)
23:   {
24:      // Zuweisung auf sich selbst abprüfen!
25:      if (this == &src)
26:         return *this;
27:      delete [] pTitle;
28:      pTitle = new char[strlen(src.pTitle)+1];
29:      strcpy_s(pTitle,strlen(src.pTitle)+1,src.pTitle);
30:      return *this;
31:   }
32:};

Enthält die Klasse, für die der Operator = überladen wird, dynamische Daten, so sollte eine Zuweisung auf sich selbst immer abgefangen werden. Wird eine solche Zuweisung nicht abgefangen, kann dies unter Umständen zum fehlerhaften Verhalten des Operators führen. Überlegen Sie einmal was passiert, wenn im obigen Beispiel die Abfrage nicht vorhanden wäre.

Move-Zuweisungsoperator (Move-Semantik, Teil 2)

Im vorherigen Kapitel haben wir den Move-Konstruktor kennengelernt, der es erlaubt, den Besitz einer Ressource beim Erstellen eines Objekts vom Quell-Objekt auf das neue Objekt zu übertragen.

Und unter gewissen Bedingungen, die nicht so selten sind wie wir gleichen sehen werden, kann es auch bei einer Zuweisung sinnvoll sein die Ressourcen vom Quell-Objekt an das Ziel-Objekt zu übertragen. Sehen wir uns einmal folgende Anweisungen an:

1: // Klassendefinition
2: class Window
3: {
4:    char *pTitle; // dynamische Eigenschaft!
5: public:
6:    ...
7:    // Überladener Zuweisungsoperator
8:    Window& operator= (const Window& src)
9:    {
10:      ...
11:   }
12: };
13:
14: int main()
15: {
16:    // Erstelle 3 Objekte
17:    Window wnd1(...);
18:    Window wnd2, wnd3;
19:    // Objekt zuweisen
20:    wnd2 = wnd1;          // 1. Zuweisung
21:    wnd3 = wnd1 + wnd2;   // 2. Zuweisung
22: }

Unter der Annahme, dass dem Compiler bekannt ist wie zwei Window-Objekte zu addieren sind, passiert hier folgendes: In der ersten Zuweisung (Zeile 20) wird der definierte Zuweisungsoperator im Kontext von wnd2 aufgerufen, der als Parameter wnd1 erhält. Doch was erhält der Zuweisungsoperator bei der zweiten Zuweisung (Zeile 21) als Parameter übergeben? Bevor die Zuweisung erfolgen kann, ist zunächst die Summe aus wnd1 + wnd2 zu berechnen. Und diese Summe wird in einem temporären Window-Objekt abgelegt, welches dann als Parameter an den Zuweisungsoperator übergeben wird. Doch wird dieses temporäre Window-Objekt nach der Zuweisung noch benötigt? Eher nicht, da darauf nicht zugegriffen werden kann. Und in einem solchen Fall hilft der Move-Zuweisungsoperator den Ressourcenbedarf und auch die Laufzeit zu optimieren, denn er überträgt den Besitz der Ressource aus diesem temporären Objekt an das Zielobjekt.

Wie sieht nun der Move-Zuweisungsoperator aus? Er erhält als Parameter eine rvalue-Referenz. Innerhalb des Operators werden dann die Ressourcen vom Quell-Objekt auf das Ziel-Objekt übertragen. Und zum Schluss ist das Quell-Objekt in einen gültigen Zustand zu versetzen, sodass bei dessen Löschung nicht versucht wird, die an das Ziel-Objekt übertragene Ressource freizugeben.

Für unsere Klasse Window aus dem obigen Beispiel sieht der Move-Zuweisungsoperator damit wie folgt aus:

1: // Klassendefinition
2: class Window
3: {
4:    char *pData; // dynamische Eigenschaft!
5: public:
6:    ...
7:    // Überladener Zuweisungsoperator
8:    Window& operator= (const Window& src)
9:    {
10:      ...
11:   }
12:   // Move-Operator =
13:   Window& operator=(Window&& src)
14:   {
15:      // Zuweisung auf sich selbst abpruefen
16:      if (this == &src)
17:         return *this;
18:      pData = src.pData; // Datenbesitz transferieren
19:      // Quell-Objekt besitzt keine Daten mehr!
20:      src.pData = nullptr;
21:      return *this;
22:   }
23: };
24:
25: int main()
26: {
27:    // Erstelle 3 Objekte
28:    Window wnd1(...);
29:    Window wnd2, wnd3;
30:    // Objekt zuweisen
31:    wnd2 = wnd1;          // 1. Zuweisung
32:    wnd3 = wnd1 + wnd2;   // 2. Zuweisung
33: }

Bei der ersten Zuweisung wird der 'normale' Zuweisungsoperator aufgerufen und bei der zweiten Zuweisung automatisch durch den Compiler der Move-Zuweisungsoperator

Ebenso kann der Move-Zuweisungsoperator zur Steigerung der Effizienz eingesetzt werden. Sehen wir uns nochmals die Klasse Window und die Funktion Swap() aus den vorherigen Beispielen an, jetzt um den Move-Zuweisungsoperator erweitert.

1: class Window
2: {
3:    char *pTitle; // Fenstertitel
4: public:
5:    ... // ctor & dtor
6:    // Zuweisungsoperator
7:    Window& operator = (const Window& src);
8:    // Move-Zuweisungsoperator
9:    Window& operator = (Window&& src);
10:   void Print();
11: };
12: ...
13: // Move-Zuweisungsoperator
14: Window& Window::operator = (Window&& src)
15: {
16:    if (this == &src)
17:       return;
18:    pTitle = src.pTitle;
19:    src.pTitle = nullptr;
20:    return *this;
21: }
22: // Swap Methode
23: void Window::Swap(Window& obj)
24: {
25:    Window tmp(std::move(*this));
26:    *this = std::move(obj);

27:    obj = std::move(tmp);
28: }

Die Swap() Methode überträgt in Zeile 25 den Titel aus dem aktuellen Objekt zunächst in das temporäre Objekt tmp mithilfe des Move-Konstruktors. Die Zeile 26 überträgt dann den Fenstertitel aus dem Objekt obj ins aktuelle Objekt durch Aufruf des Move-Zuweisungsoperators. Und zum Schluss wird der Fenstertitel aus dem tmp-Objekt ins Objekt obj übertragen. D.h., der gesamte Tauschvorgang verläuft ohne zusätzliches Allokieren von Speicher für den Fenstertitel.

Aufruf des überladenen Operators =

Der überladene Zuweisungsoperator kann entweder direkt, d.h., durch Angabe des Namens der Methode operator=, oder indirekt durch einfaches Anwenden des Zuweisungsoperators aufgerufen werden. In der Regel wird der letzte Fall verwendet, da er eingängiger ist.

1: // Klassendefinition
2: class Window
3: {
4:    ...
5: public:
6:    // Überladener Zuweisungsoperator
7:    Window& operator= (const Window& src)
8:    {...}
9:    ...
10: };
11: // main() Funktion
12: int main()
13: {
14:    Window win1, win2;
15:    ...
16:    // direkter Aufruf
17:    win1.operator=(win2);
18:    // indirekter Aufruf
19:    win1 = win2;
20:    ...
21: }

Mehrfaches Überladen

Da das Überladen des Zuweisungsoperators durch eine Methode erfolgt, kann der Operator auch mehrfach überladen werden. Dies ermöglicht Zuweisungen mit unterschiedlichen Datentypen (rechter Operand des Operators =).

Ausgangsbasis für das Beispiel ist die folgende Klasse Complex zur Bearbeitung von komplexen Zahlen, bestehend aus einem Real- und Imaginäranteil.

1: // Klassendefinition
2: class Complex
3: {
4:    double real;
5:    double imag;
6: public:
7:    Complex(double r, double i): real(r), imag(i)
8:    {}
9:    ...
10: };

Wird z.B. für die Klasse Complex eine Methode für den überladenen Zuweisungsoperator definiert, die als Parameter einen double-Wert erhält, kann folgende Zuweisung ausgeführt werden:

Complex comp;
comp = 1.0;

D.h., durch Überladen des Zuweisungsoperators können auf diese Weise für eine Klasse verschiedene Konvertierungsfunktionen zur Verfügung gestellt werden.

1: // Klassendefinition
2: class Complex
3: {
4:    double real;
5:    double imag;
6: public:
7:    Complex(...);
8:    // 1. Operator =
9:    Complex& operator=(const Complex& src) = default;
10:   // 2. Operator =
11:   Complex& operator=(double val);
12:   ...
13: };
14: Complex& Complex::operator = (double val)
15: {
16:    real = val;
17:    imag = 0;
18:    return *this;
19: }

Verhindern von Zuweisungen bei Objekten

Standardmäßig kann einem Objekt ein anderes Objekt der gleichen Klasse zugewiesen werden. Wie bekannt, definiert der Compiler unter bestimmten Bedinungen automatisch die Methode operator =, welche die Eigenschaften einfach kopiert. Diese automatische Generierung der Operator-Methode kann unterbunden werden, indem bei der Deklaration des Operators nach der Parameterklammer = delete angegeben wird.

1: // Klassendefinition
2: class Complex
3: {
4:    double real;
5:    double imag;
6: public:
7:    Complex(...);
8:    // Verhindert die Zuweisung von Complex-Objekten
9:    Complex& operator=(const Complex& src) = delete;
10:   ...
11: };

Copyright 2024 © Wolfgang Schröder
E-Mail mit Fragen oder Kommentaren zu dieser Website an: info@cpp-tutor.de
Impressum & Datenschutz