Ü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
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 } |
|
|
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.
|
|
Im Zusammenhang mit dem Überladen des Operators = soll auch die so genannte "Regel der großen 3" erwähnt werden:
|
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.
|
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. |
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; .... } |
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; } |
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(...); ... }; |
|
|
Prüffall: Testen der Programmfunktionen Prüffall: Testen der Programmfunktionen Prüffall: Stesstest |
// 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(); } |
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
Nach Zuweisung |