C++ Kurs

Überladen des Zuweisungsoperators

Die Themen:

Überladen des Operators =
Returnwert des Operators =
Regel der großen 3
Aufruf des überladenen Operators =
Mehrfaches Überladen
Verhindern von Zuweisungen bei Objekten
Beispiel und Übung

Ü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. Als Beispiel mag hier die inzwischen aus den Übungen bekannte Klasse CString dienen. Im weiteren Verlauf des Kurses werden wir diese Klasse so erweitern, dass z.B. zwei CString-Objekte mit dem Plus-Operator '+' zusammengefügt werden können. Als Einstieg in das Überladen von Operatoren soll in dieser Lektion zunächst das Überladen des Zuweisungsoperators '=' betrachtet werden.

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 '='. Sie können die Kombination operator = sozusagen als Name der Memberfunktion betrachten. 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:

CAnyObject = DTYP(Ausdruck);

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


// 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
}

Beachten Sie bitte, dass Sie Objekte in der Regel als Referenzparameter übergeben sollten. Hier können Sie das 'Warum?' nochmals nachlesen.

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 dem so ist, soll anhand eines Beispiels demonstriert werden.

Angenommen, comp1, comp2 und comp3 sind Objekte vom Typ Complex. Nach dem Sie den Zuweisungsoperator überladen haben, können Sie dann unter anderem folgende Anweisung schreiben (Mehrfachzuweisung!):

comp3 = comp2 = comp1;

Dieser Ausdruck wird vom Compiler in zwei Teilausdrücke aufgeteilt. 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.

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 so genannte "Regel der großen 3" erwähnt werden:

Ist eine der Memberfunktionen

  • Kopierkonstruktor
  • Destruktor
  • Überladener Zuweisungsoperator

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

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


// 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)
    {
        pTitle = new char[strlen(src.pTitle)+1];
        strcpy(pTitle,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 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.

Enthä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 zu fehlerhaftem Verhalten des Operators führen. Überlegen Sie sich einmal was passiert, wenn im obigen Beispiel die entsprechende Abfrage nicht vorhanden wäre.

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.


// 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 Sie verschiedene Zuweisungen definieren, die sich jeweils im Datentyp des rechten Operanden unterscheiden. Wenn Sie z.B. für die Klasse Complex eine zweite Memberfunktion für den überladenen Zuweisungsoperator definieren, der als Parameter einen double Wert erhält, so können Sie danach folgende Zuweisung durchführen:

Complex comp;
comp = 1.0;

Hier wird einem Objekt der Klasse Complex eine double-Zahl zugewiesen. Dies führt zum Aufruf des überladenen Operators operator= (double val).


// Klassendefinition
class Complex
{
    double real;
    double imag;
  public:
    Complex(...);
    Complex& operator=(const Complex& src);  // 1. überladener Operator =
    Complex& operator=(double val);          // 2. überladener 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

Und zum Schluss noch ein Hinweis: Standardmäßig ist es immer erlaubt, einem Objekt einer Klasse ein anderes Objekt der gleichen Klasse zuzuweisen. Sie müssen hierfür nicht explizit einen überladenen Zuweisungsoperator definieren, dies macht der Compiler automatisch. In diesem Fall werden die Daten einfach Element für Element umkopiert. Wollen Sie dieses Verhalten unterbinden, so überladen Sie den Zuweisungsoperator durch eine entsprechende leere Memberfunktion, die Sie jetzt aber innerhalb der private Sektion der Klasse definieren. Da private Member nicht von außerhalb der Klasse zugänglich sind, können auch keine direkten Zuweisungen mehr durchgeführt werden.


// Klassendefinition
class Complex
{
    double real;
    double imag;
    // private Memberfunktion des Operators =
    Complex& operator=(const Complex& src)
    { }
  public:
    Complex(...);
    ...
};

Beispiel und Übung

Beispiel:

Für die Bearbeitung von Prüffällen wird eine Klasse CTestCase definiert. Ein Prüffall enthält außer einer Beschreibung des Prüffalls noch eine eindeutige Prüffallnummer, eine Priorität und einen Status. Die Priorität und der Status eines Prüffalls sind durch entsprechende enums vorgegeben. Die Prüffallnummer wird automatisch vergeben.

Bei der Definition eines Prüffalls wird dessen Beschreibung so wie eine Priorität angegeben. Der Status des Prüffalls wird initial auf 'defined' gesetzt.

Die Priorität und der Status eines Prüffalls sollen durch Zuweisungen verändert werden können. Dazu wird der Zuweisungsoperator durch entsprechende Memberfunktionen überladen.

Die Ausgabe des Prüffalls erfolgt durch die Memberfunktion PrintIt().

In der main() Funktion wird zunächst ein Prüffall definiert und ausgegeben. Anschließend wird dessen Priorität und Status verändert und der Prüffall dann erneut ausgegeben.

Zum Testen der Prüffall-Nummerierung wird dann ein weiterer Prüffall definiert, dessen Status auf failed gesetzt wird.

Prüffall: Testen der Programmfunktionen
Nummer: 1, Priorität: 0, Status: definiert

Prüffall: Testen der Programmfunktionen
Nummer: 1, Priorität: 2, Status: erfolgreich

Prüffall: Stesstest
Nummer: 2, Priorität: 3, Status: fehlerhaft


// Beispiel zum überladenen Operator =

// Dateien einbinden
#include <iostream>
#include <string>

using std::cout;
using std::endl;

// Klassendefinition
class CTestCase
{
public:
    // enum der Prioritäten
    enum priority {low, medium, high, critical};
    // enum des Prüffallstatus
    enum state {defined, failed, ok};
private:
    std::string description;        // Beschreibung
    unsigned int tcNumber;          // Nummer
    priority prio;                  // Priorität
    state tcState;                  // Status
    static unsigned int number;     // Prüffallindex
  public:
    // ctor
    CTestCase(const std::string& desc, priority p);
    // Zuweisung der Priorität
    CTestCase& operator = (priority p);
    // Zuweisung des Status
    CTestCase& operator = (state s);
    // Ausgabe des Prüffalls
    void PrintIt () const;
};
// Definition der statischen Eigenschaft
unsigned int CTestCase::number = 1;
// Definition der Memberfunktion
// ctor
CTestCase::CTestCase(const std::string& desc, priority p) :
    description(desc)
{
    tcNumber = number;          // Nummber ablegen
    number++;                   // Pruffallindex erhöhen
    tcState = defined;          // Status setzen
    prio = p;                   // Priorität ablegen

}
// Zuweisung des Status
inline CTestCase& CTestCase::operator = (state s)
{
    tcState = s;
    return *this;
}
// Zuweisung der Priorität
inline CTestCase& CTestCase::operator = (priority p)
{
    prio = p;
    return *this;
}
// Ausgabe des Prüffalls
void CTestCase::PrintIt () const
{
    cout << "Prüffall: " << description << endl;
    cout << "Nummer: " << tcNumber << ", Priorität: " << prio << ", Status: ";
    switch (tcState)
    {
    case defined:
        cout << "definiert";
        break;
    case failed:
        cout << "fehlerhaft";
        break;
    case ok:
        cout << "erfolgreich";
        break;
    default:
        cout << "unbekannt";
    }
    cout << endl << endl;
}

// main() Funktion
int main()
{
    // Prüffall definieren
    CTestCase test1("Testen der Programmfunktionen", CTestCase::low);
    // und ausgeben
    test1.PrintIt();
    // Priotät umsetzen
    test1 = CTestCase::high;
    // Status auf ok setzen
    test1 = CTestCase::ok;
    // Prüffall erneut ausgeben
    test1.PrintIt();
    // Weiteren Prüffall definieren
    CTestCase test2("Stesstest", CTestCase::critical);
    // Prüffall auf  failed setzen
    test2 = CTestCase::failed;
    // Prüffall ausgeben
    test2.PrintIt();
}

Übung:

Erweitern Sie die in vorherigen Übung erstellte Klasse CString um zwei überladene Zuweisungsoperatoren. Zum einen soll es nun möglich sein, zwei CString-Objekte einander zuzuweisen und zum anderen einem CString-Objekt einen C-String.

Alle anderen Memberfunktionen bleiben vorerst unverändert.

Erstellen Sie in main() zwei CString-Objekte, denen Sie bei ihrer Definition einen beliebigen Text zuweisen. Geben Sie die CString-Objekte zur Kontrolle aus.

Weisen Sie dann dem ersten CString-Objekt einen beliebigen C-String zu. Dieses erste CString-Objekt ist im Anschluss daran dem zweiten CString-Objekt zuzuweisen. Geben Sie beide CString-Objekte erneut aus. 

Ausgangs-Strings
1.String: bla bla bla...
2.String: und sonstiger Nonsens

Nach Zuweisung
1.String: Dieser Text wird dupliziert
2.String: Dieser Text wird dupliziert

Lösung ansehen!