C++ Kurs

Überladen des Zuweisungsoperators

Die Themen:

Überladen des Operators =

Für Objekte lassen sich fast alle Operatoren so umdefinieren, dass sie im Zusammenhang mit diesen eine frei definierbare Funktion ausführen. So können z.B. zwei std::string Objekte mit dem Plus-Operator '+' zusammengefügt werden. Als Einstieg in das Überladen von Operatoren wird in diesem Kapitel zunächst das Überladen des Zuweisungsoperators '=' betrachtet.

Um den Zuweisungsoperator '=' für eine Klasse zu überladen, ist in der Regel folgende Memberfunktion einzusetzen:

CAny& CAny::operator = (const DTYP& Param)

CAny ist die Klasse, für die der Zuweisungsoperator überladen werden soll. Danach folgt das Schlüsselwort operator und dann der zu überladende Operator, hier also '='. Die Kombination operator = kann sozusagen als Name der Memberfunktion betrachtet werden. Innerhalb der Parameterklammer folgt der Datentyp DTYP des rechten Operanden des Operators, d.h. der Datentyp des rechts vom Operator '=' stehenden Ausdrucks. Damit ist dann folgende Zuweisung definiert:

AnyObject = DTYP(AUSDRUCK);

Im nachfolgenden Beispiel wird der Zuweisungsoperator für eine Klasse Complex definiert, um Objekte dieser Klasse einander zuweisen zu können.

PgmHeader// Klassendefinition für komplexe Zahlen
class Complex
{
    double real;
    double imag;
  public:
    Complex(...);
    Complex& operator = (const Complex& src);  // Zuweisungsoperator
    ...
};
// Memberfunktion des überladenen Zuweisungsoperator definieren
Complex& Complex::operator = (const Complex& src)
{
    real = src.real;   // Beide Anteile entsprechend dem
    imag = src.imag;   // Zielobjekt zuweisen
    return *this;      // Referenz zurückgeben
}
HinweisBeachten Sie bitte, dass Sie Objekte in der Regel als Referenzparameter übergeben sollten.

Returnwert des Operators =

Die Memberfunktion des überladenen Zuweisungsoperators '=' sollte (ja fast muss) immer 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, comp1, comp2 und comp3 sind Objekte vom Typ Complex. Nach dem Überladen des Zuweisungsoperators kann dann zum Beispiel folgende Anweisung geschrieben werden (Mehrfachzuweisung!):

comp3 = comp2 = comp1;

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

AchtungUnd nochmals: Sie müssen im Regelfall immer den Zuweisungsoperator überladen, wenn eine Klasse dynamische Eigenschaften enthält (Zeiger auf Speicherbereiche!). Tun Sie dies nicht und es wird eine Zuweisung eines Objekts an ein anderes durchgeführt, so enthalten danach beide Objekte Zeiger auf den gleichen Speicherbereich. Und dies kann dann bis zum Programmabsturz führen.

Regel der großen 3

Im Zusammenhang mit dem Überladen des Operators = soll auch die sogenannte "Regel der großen 3" erwähnt werden:

Hinweis

Ist eine der Memberfunktionen

  • Kopierkonstruktor
  • Destruktor
  • Überladener Zuweisungsoperator

notwendig, so sind in der Regel auch die beiden anderen Memberfunktionen erforderlich!

Sehen wir uns dazu das nachfolgende Beispiel an. Die dort definierte Klasse Window enthält eine dynamische Eigenschaft pTitle für die Aufnahme eines Fenstertitels. Der für den Fenstertitel benötigte Platz wird im Konstruktor reserviert. Damit wird automatisch der Destruktor notwendig, da der reservierte Speicherplatz beim Löschen des Objekts auch wieder freigegeben werden muss. Nach der obigen Regel sind nun aber auch der Kopierkonstruktor als auch der überladene Zuweisungsoperator notwendig!

PgmHeader// Klassendefinition
class Window
{
    char *pTitle;       // dynamische Eigenschaft!
    ...
  public:
    // ctor, reserviert Platz für Titel
    Window(const char* const pT)
    {
        pTitle = new char[strlen(pT)+1];
        strcpy(pTitle,pT);
    }
    // dtor, gibt Platz für Titel frei
    ~Window()
    {
        delete [] pTitle;
    }
    // Kopierkonstruktor
    Window(const Window& src): Window(src.pTitle)
    {
    }
    // Überladener Zuweisungsoperator
    Window& operator= (const Window& src)
    {
        // Zuweisung auf sich selbst abprüfen!
        if (this == &src)
            return *this;
        delete [] pTitle;
        pTitle = new char[strlen(src.pTitle)+1];
        strcpy(pTitle,src.pTitle);
        return *this;
    }
};

Dass diese Regel hier gilt, lässt sich an den beiden folgenden Anweisungen demonstrieren:

Window myWin(yourWin);
newWin = yourWin;

Mit der ersten Anweisung wird ein neues Objekt myWin definiert, welches mit den Eigenschaften des bereits bestehenden Objekts yourWin initialisiert wird, d.h. es wird der Kopierkonstruktor des Objekts myWin aufgerufen. Um nun den Titel des übergebenen Objekts yourWin übernehmen zu können, muss zuerst entsprechend Speicher reserviert werden und erst dann kann der Kopiervorgang für den Fenstertitel erfolgen.

Die zweite Anweisung weist (dem vorher zu definierenden) Objekt newWin ebenfalls die Eigenschaften des Objekt yourWin zu, was zum Aufruf der überladenen Zuweisungsoperators führt. Bei dieser Zuweisung muss zunächst der Speicher für den bisherigen Titel von newWin freigegeben werden und danach Speicher für den zu übernehmenden Titel reserviert werden.

AchtungEnthält die Klasse, für die der Operator = überladen wird, dynamische Daten, so sollte eine Zuweisung auf sich selbst immer abgefangen werden:

myWin = myWin;

Wird eine solche Zuweisung nicht abgefangen, so kann dies unter Umständen zum fehlerhaften Verhalten des Operators führen. Überlegen Sie einmal was passiert, wenn im obigen Beispiel die entsprechende Abfrage nicht vorhanden wäre.

Move-Operator = (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 gar nicht so selten sind wie wir gleichen sehen werden, kann es auch bei einer Zuweisung sinnvoll sein, die Ressourcen vom Quell-Objekt (rvalue) an das Ziel-Objekt (lvalue) zu übertragen. Sehen wir uns dazu einmal folgende Anweisungen an:

PgmHeader// Klassendefinition
class Any
{
    char *pData;       // dynamische Eigenschaft!
    int  noOfData;     // Laenge des Datenfeldes
    ...
  public:
    ...
    // Überladener Zuweisungsoperator
    Any& operator= (const Any& src)
    {
        // Zuweisung auf sich selbst abprüfen!
        if (this == &src)
            return *this;
        delete [] pData;                  // Alte Daten entfernen
        pData = new char[src.noOfData];   // Feld fuer neue Daten allokieren
        memcpy(pData,src.pData);          // Daten umkopieren
        noOfData = src.noOfData;     
        return *this;
    }
};

int main()
{
    // Erstelle 3 Objekte
    Any obj1(...);
    Any obj2, obj3;
    ...
    // Objekt zuweisen
    obj2 = obj1;               // 1. Zuweisung
    obj3 = obj1 + obj2;        // 2. Zuweisung
    ...
}

Unter der Annahme, dass dem Compiler bekannt ist, wie zwei Any-Objekte zu addieren sind, passiert hier nun folgendes. In der ersten Zuweisung wird der explizit definierte Zuweisungsoperator der Klasse Any aufgerufen, der als Parameter das Quell-Objekt obj1 erhält. Doch was erhält der Zuweisungsoperator bei der zweiten Zuweisung als Parameter übergeben? Bevor die Zuweisung erfolgen kann, muss zunächst die Summe aus obj1 + obj2 berechnet werden. Und diese Summe wird in einem temporären Any-Objekt abgelegt, welches dann als Parameter an den Zuweisungsoperator übergeben wird. Doch brauchen wir dieses temporäre Any-Objekt nach der Zuweisung noch? Wohl eher nicht, da wir es auch nicht direkt ansprechen können. Und in einem solchen Fall hilft uns der Move-Operator = den Ressourcenbedarf und auch die Laufzeit zu optimieren. Denn der Move-Operator = überträgt den Besitz der Ressource aus diesem temporären Objekt an das Zielobjekt.

Wie sieht nun dieser Move-Operator aus? Auch er erhält als Parameter wiederum eine rvalue-Referenz. Innerhalb des Move-Operators werden dann die Ressourcen vom Quell-Objekt auf das Ziel-Objekt übertragen. Zum Schluss muss das Quell-Objekt noch in einen gültigen Zustand versetzt werden, sodass bei dessen Zerstörung nicht versucht wird, die jetzt an das Ziel-Objekt übertragene Ressource freizugeben.

Für unsere Klasse Any aus dem obigen Beispiel würde der Move-Operator = damit wie folgt aussehen:

PgmHeader// Klassendefinition
class Any
{
    char *pData;       // dynamische Eigenschaft!
    int  noOfData;     // Laenge des Datenfeldes
    ...
  public:
    ...
    // Überladener Zuweisungsoperator
    Any& operator= (const Any& src)
    {
        // Zuweisung auf sich selbst abprüfen!
        if (this == &src)
            return *this;
        delete [] pData;                  // Alte Daten entfernen
        pData = new char[src.noOfData];   // Feld fuer neue Daten allokieren
        memcpy(pData,src.pData);          // Daten umkopieren
        noOfData = src.noOfData;     
        return *this;
    }
    // Move-Operator =
    Any& operator=(Any&& src)
    {
       if (this == &src)                  // Zuweisung auf sich selbst abpruefen
          return *this;
       pData = src.pData;                 // Datenbesitz transferieren
       noOfData = src.noOfData;           // Datenlaenge transferieren
       src.pData = nullptr;               // Quell-Objekt besitzt keine Daten mehr!
       src.noOfData = 0;
       return *this;
   }
};

int main()
{
    // Erstelle 3 Objekte
    Any obj1(...);
    Any obj2, obj3;
    ...
    // Objekt zuweisen
    obj2 = obj1;               // 1. Zuweisung
    obj3 = obj1 + obj2;        // 2. Zuweisung
    ...
}

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

Aufruf des überladenen Operators =

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

PgmHeader// Klassendefinition
class Window
{
    ...
  public:
    // Überladener Zuweisungsoperator
    Window& operator= (const Window& src)
    {...}
    ...
};
// main() Funktion
int main()
{
    Window win1, win2;
    ....
    // direkter Aufruf
    win1.operator=(win2);
    // indirekter Aufruf
    win1 = win2;
    ....
}

Mehrfaches Überladen

Da das Überladen des Zuweisungsoperators durch eine Memberfunktion erfolgt, kann der Operator auch durch mehrere unterschiedliche Memberfunktionen überladen werden. Dadurch können verschiedene Zuweisungen definiert werden, die sich jeweils im Datentyp des rechten Operanden unterscheiden. Wenn z.B. für die Klasse Complex eine zweite Memberfunktion für den überladenen Zuweisungsoperator definiert ist, die als Parameter einen double Wert erhält, so könnte danach folgende Zuweisung durchführt werden:

Complex comp;
comp = 1.0;

Hier wird einem Objekt der Klasse Complex eine double-Zahl zugewiesen, was zum Aufruf des überladenen Operators operator= (double val) führt. D.h. durch überladen des Zuweisungsoperators können so zum Beispiel für eine Klasse verschiedene Konvertierungsfunktionen zur Verfügung gestellt werden.

PgmHeader// Klassendefinition
class Complex
{
    double real;
    double imag;
  public:
    Complex(...);
    Complex& operator=(const Complex& src);  // 1. Operator =
    Complex& operator=(double val);          // 2. Operator =
    ...
};
// Überladene Operatoren definieren
Complex& Complex::operator = (const Complex& src)
{
    ....
}
Complex& Complex::operator = (double val)
{
    real = val;
    imag = 0;
    return *this;
}

Verhindern von Zuweisungen bei Objekten

Standardmäßig ist es stets erlaubt, einem Objekt einer Klasse ein anderes Objekt der gleichen Klasse zuzuweisen. Wie ja bereits bekannt, definiert der Compiler standardmäßig eine entsprechende Memberfunktion operator =, die die Eigenschaften einfach kopiert. Diese automatische Generierung der Operator-Memberfunktion kann jedoch unterbunden werden, indem bei der Deklaration des Operators nach der Parameterklammer = delete angegeben wird.

PgmHeader// Klassendefinition
class Complex
{
    double real;
    double imag;
  public:
    Complex(...);
    // Verhindert die Zuweisung von Complex-Objekten
    Complex& operator=(const Complex& src) = delete;
    ...
};

Beispiel und ÜbungUnd hier geht's zum Beispiel und zur Übung.