11. Überladen von Operatoren

Mit Hilfe des Überladen von Operatoren können auf Objekte selbstdefinierter Klassen (fast) alle Operatoren von C++ mit einer neuen Bedeutung versehen werden. Diese Bedeutung wird durch spezielle Funktionen bzw. Methoden festgelegt. Der Gedanke ist dabei der gleiche wie beim Überladen von Funktionen und Methoden. Das Überladen von Operatoren wird allgemein auch als Operator-Overloading bezeichnet.

Sie haben das Überladen von Operatoren bei der Daten-Ein- und Ausgabe bereits indirekt kennengelernt. Mittels des Ausgabeoperators << können Objekte verschiedener Typen auf einem Ausgabe-Datenstrom ausgegeben werden. Der Eingabeoperator >> dient dem Einlesen von Objekten verschiedener Typen von einem Eingabe-Datenstrom.

Um den Mechanismus genau zu verstehen, ist etwas Vorarbeit notwendig. Prinzipiell existiert für jeden Operator auch eine entsprechende Funktionsnotation (deren explizite Verwendung allerdings bei den meisten C++-Compilern für Objekte nicht selbstdefinierter Klassen untersagt ist). Das heißt, für die Standarddatentypen wie int, float, double usw. sind z.B. die Operatoren +, -, *, /, = usw. definiert. Für z.B. Zeichenketten wie char[100] (C-Strings) ist dagegen kein Operator definiert. Sie könnten sich nun eine Klasse CString definieren, die die Zuweisung von Zeichenketten über den Operator = erlaubt.

Genaugenommen ist in C++ die Operator-Notation nur eine andere Schreibweise für bestimmte Funktionsaufrufe. Die vordefinierten Datentypen kann man auch als "vordefinierte Klassen" verstehen, für die bereits Operator-Funktionen definiert sind.

Beispiele:
 Operator-Schreibweise  Funktionsaufruf-Schreibweise
 a + b  operator+ (a, b)
 cout << a  operator<< (cout, a)
 !x  operator! (x)
 a += b  operator+= (a, b)
 f[3]  operator[] (f, 3)
 func (a, b, c)  operator() (func, a, b, c)
 cout << a << b  operator<< (operator<< (cout, a), b))
 -(a + b * 3)  operator- (operator+ (a, operator* (b, 3)))

Das Prinzip des Überladens von Operatoren besteht nun darin, dass diese Operator-Funktionen wie normale Funktionen überladen werden können. Die einzige Bedingung dabei ist, dass zumindest ein Argument den Typ einer (selbstdefinierten) Klasse oder eine Referenz darauf besitzt.

Auf diese Weise können grundsätzlich fast alle Operatoren überladen werden. Folgende Operatoren können nicht überladen werden:
.
.*
::
sizeof
?:

Die Ausführungsreihenfolgen und Prioritäten der Operatoren können nicht geändert werden. Außerdem können keine eigenen Operatoren definiert werden, sondern Sie müssen auf die vorhandenen Operatoren zurückgreifen. Allerdings werden Ihnen keine Vorschriften gemacht, wie ein Operator einzusetzen ist (der Operator + kann z.B. durch eine eigene Definition eine völlig andere Bedeutung - z.B. die der Multiplikation - erlangen).

Es gibt verschiedene Möglichkeiten, einen Operator zu überladen: Einerseits über friend-Funktionen der betreffenden Klasse oder über direkte Methoden der Klasse. Im folgenden Abschnitt wird die erste Möglichkeit betrachtet, die zweite Möglichkeit wird im darauf folgenden Abschnitt behandelt.

11.1. Überladen von Operatoren durch friend-Funktionen

Im folgenden Beispiel soll für eine Klasse complex, die zur Darstellung komplexer Zahlen dient, der Operator + so überladen werden, dass zwei komplexe Zahlen auf die bei Standardtypen wie int oder double übliche Weise addiert werden können.

Die Operator-Funktion für den Operator + hat grundsätzlich folgendes Aussehen:

complex operator+ (complex a, complex b);

Das folgende Programm demonstriert den Einsatz von friend-Funktionen zum Überladen von Operatoren. Die Operator-Funktion muss auf private Elemente der Klasse complex zugreifen. Daher ist es notwendig, sie in der Definition der Klasse als Freund zu deklarieren.

Beispiel:

kap11_01.cpp

#include <iostream>

using namespace std;

class complex
{
   private:
      double re,im; // Real- und Imaginärteil
   public:
      complex(): re(0), im(0) {}
      complex(double r, double i): re(r), im(i) {}
      friend complex operator+ (const complex &a, const complex &b);
      void Ausgabe()
      {
         cout << "re = " << re << endl;
         cout << "im = " << im << endl;
      }
};

complex operator+ (const complex &a, const complex &b)
{
   complex c;

   c.re = a.re + b.re;
   c.im = a.im + b.im;
   return c;
}

int main()
{
   complex a(1,1), b(2,2), c;

   c = a + b; // ist identisch mit
   c = operator+ (a, b);
   c.Ausgabe();

   return 0;
}

Da Objekte der Klasse complex immerhin eine Größe von 16 Byte haben, ist es aus Zeit- und Speicherplatzgründen günstiger, die Parameter der Operator-Funktion als Referenzparameter zu deklarieren.

Dieses Programm enthält eine Methode Ausgabe für die Ausgabe einer komplexen Zahl. Besser wäre es jedoch, die Operatoren << und >> zu überladen und damit die Ausgabe über die üblichen Operatoren durchzuführen. Hier stellt sich die Frage, welche Typen die Parameter der Operator-Funktionen für << und >> haben.

Die Operator-Notation des Ausdrucks "cout << a" ist folgende:
operator<< (cout, a)

Der erste Parameter ist somit der Ausgabe-Datenstrom (class ostream), der zweite das auszugebende Objekt. Um den Operator << auch mehrfach hintereinander verwenden zu können, ist es notwendig, als Funktionsergebnis wieder den Ausgabe-Datenstrom selbst zurückzuliefern. Aus cout << a << b; wird dann operator<< (operator<< (cout, a), b);.

Da Datenstromobjekte meist aus vielen Daten bestehen und diese ferner bei einer Ausgabe auch verändert werden müssen, ist es hier notwendig, das Datenstromobjekt als Referenzparameter zu deklarieren. (Deshalb sind die Referenzen überhaupt in C++ eingeführt worden!) Dass das Datemstromobjekt nicht kopiert wird, wenn es als Funktionsergebnis bei einem geschachtelten Aufruf zurückgeliefert wird, kann dadurch erreicht werden, dass ebenfalls nur eine Referenz auf das Objekt und nicht das Objekt selbst zurückgeliefert wird.

Der Operator << würde also für die Klasse complex am besten wie folgt überladen werden. Der Aufruf der Methode Ausgabe kann nun durch cout << c; ausgetauscht werden.

Beispiel:

kap11_02.cpp

#include <iostream>

using namespace std;

class complex
{
   private:
      double re,im; // Real- und Imaginärteil
   public:
      complex(): re(0), im(0) {}
      complex(double r, double i): re(r), im(i) {}
      friend complex operator+ (const complex &a, const complex &b);
      friend ostream &operator<< (ostream &ostr, const complex &a);
      friend istream &operator>> (istream &istr, complex &a);
};

complex operator+ (const complex &a, const complex &b)
{
   complex c;

   c.re = a.re + b.re;
   c.im = a.im + b.im;
   return c;
}

ostream &operator<< (ostream &ostr, const complex &a)
{
   ostr << '(' << a.re << " + i * " << a.im << ')';
   return ostr;
}

istream &operator>> (istream &istr, complex &a)
{
   istr >> a.re >> a.im;
   return istr;
}

int main()
{
   complex a(1,1), b(2,2), c;

   c = a + b;
   cout << c << endl;

   return 0;
}

11.2. Überladen von Operatoren durch Methoden

Eine Operator-Funktion kann auch als Methode einer Klasse implementiert werden, wenn das erste Argument ein Objekt dieser Klasse ist. Dieses erste Argument wird dann nicht mehr angegeben, da es für die Operator-Methode das aktuelle Objekt darstellt.

Zu der oben vorgestellten Klasse complex soll der binäre Operator - (Subtraktion zweier komplexer Zahlen) in Form einer Operator-Methode hinzugefügt werden. Folgendes muss hinzugefügt werden:

kap11_03.cpp   (komplettes Beispielprogramm)

class complex
{
   ... // siehe oben
   public:
      complex operator- (const complex &a);
};

complex complex::operator- (const complex &a)
{
   complex c;

   c.re = re - a.re;
   c.im = im - a.im;
   return c;
}

Im folgenden werden noch der unäre Operator - für die Vorzeichenumkehr und der Operator += für die Klasse complex definiert. Bei einem unären Operator wird kein Parameter benötigt.

kap11_04.cpp   (komplettes Beispielprogramm)

class complex
{
   ... // siehe oben
   public:
      complex operator- ();
      complex operator+= (const complex &a);
};

complex complex::operator- ()
{
   complex c;

   c.re = -re;
   c.im = -im;
   return c;
}

complex complex::operator+= (const complex &a)
{
   re += a.re;
   im += a.im;
   return *this;
}

Da das Ergebnis des Operators += das Objekt selber ist, wird mit Hilfe des this-Zeigers das Objekt selber zurückgeliefert.Hier sehen Sie auch eine notwendige Verwendung des this-Zeigers.

Hinweis:
Die Operatoren << und >> können nicht als Operator-Methoden einer Klasse geschrieben werden, da das erste Argument dieser Operatoren kein Objekt der Klasse, sondern das entsprechende Datenstromobjekt ist.

11.3. Allgemeines

Werden die Operatoren ++ oder -- überladen, so kann oft nicht zwischen der Prefix- und Postfix-Notation unterschieden werden. Für Objekte eigener Klassen sind somit folgende Ausdrücke äquivalent (und bedeuten Prefix-Notation):

b = a++;
b = ++a;

Einige Compiler - so auch der GNU-C++-Compiler - ermöglichen allerdings die Definition eigener Postfix-Operatoren für das Inkrementieren und Dekrementieren. Dabei wird ein normalerweise mit 0 belegter und nicht verwendeter Parameter mit dem Typ int definiert. (Ob dies ein so geschickter Kunstgriff ist, sei dahin gestellt.) Im folgenden werden Prefix- und Postfix-Operatoren für die Klasse complex vorgestellt.

kap11_05.cpp   (komplettes Beispielprogramm)

class complex
{
   ... // siehe oben
   public:
      complex operator++ ();    // Prefix-Operator
      complex operator++ (int); // Postfix-Operator
};

complex complex::operator++ ()
{
   ++re;
   cout << "Prefix" << endl;
   return *this;
}

complex complex::operator++ (int a)
{
   re++;
   cout << "Postfix" << endl;
   return *this;
}

Da die Operatoren aber immer in der gleichen Reihenfolge abgearbeitet werden, machen die beiden Operatoren keinen Unterschied. So kommt bei den beiden folgenden Berechnungen immer das gleiche Ergebnis:

complex a(1,1), b(2,2), c, d;

d = a;
c = d++ + b;
cout << c << endl; // ergibt 4 + 3i
d = a;
c = ++d + b;
cout << c << endl; // ergibt ebenfalls 4 + 3i

Das Überladen von den Operatoren (), ->, new und delete stellen Spezialfälle dar, die in den nachfolgenden Abschnitten erläutert werden. Zuvor noch ein Abschnitt zum Thema Typumwandlung.

11.4. Typumwandlungs-Operatoren

In einem der vorigen Abschnitte wurde der Operator + für die Klasse complex überladen, so dass er auch zur Addition zweier komplexer Zahlen verwendet werden kann. Nach wie vor nicht besprochen ist, wie z.B. eine Addition einer komplexen Zahl mit einer int- oder double-Zahl vorgenommen wird.

complex a(1,1), b(2,2), c;

c = a + b;      // OK!
c = a + 17;     // Fehler! Operation ist nicht definiert!
c = a + 3.1415; // Fehler! Operation ist nicht definiert!

Um auch diese Formen der Addition zu ermöglichen, können grundsätzlich entsprechende Operator-Funktionen definiert werden. Die Klassendefinition von complex bräuchte dann 5 Operator-Funktionen allein für die Addition. Entsprechend kämen dann noch eine ganze Menge Operator-Funktionen für die anderen Rechenarten wie Subraktion, Multiplikation, Division, usw.

class complex
{  // siehe oben
   public:
      // für die Addition:
      friend complex operator+ (complex &a, complex &b);
      friend complex operator+ (complex &a, int b);
      friend complex operator+ (int a, complex &b);
      friend complex operator+ (complex &a, double b);
      friend complex operator+ (double a, complex &b);
};

// Die deklarierten Funktionen müssen natürlich
// auch noch definiert werden!

Wie Sie sehen, wären sehr viele Einzelfunktionen notwendig, um das Problem auf diese Art zu lösen. Wenn Sie sich jetzt vorstellen, dass auf einen abstrakten Datentyp wie der Klasse der komplexen Zahlen noch sehr viel mehr Operatoren anwendbar sein müssten und evtl. auch andere Typen akzeptiert werden sollten, so erscheint der Aufwand dann zu hoch zu werden.

In C werden grundsätzlich Ausdrücke, bei denen verschiedene Typen gemischt vorkommen, mittels automatischer Typumwandlung übersetzt.

Beispiel:
Der folgende Ausdruck wird mittels automatischer (impliziter) Typumwandlung folgendermaßen ausgewertet:

10 + 3L - 123.456
(10 - int-Konstante, 3 - long-Konstante, 123.456 - double-Konstante)

1. Schritt: Umwandlung der int-Konstante 10 in einen long-Wert: 10L
2. Schritt: long-Addition: 10L + 3L = 13L
3. Schritt: Umwandlung des long-Werts 13L in einen double-Wert: 13.0
4. Schritt: double-Subtraktion: 13.0 - 123.456 = -110.456

Es findet also eine automatische Typumwandlung vor der Durchführung der Operationen statt. Die Typumwandlung kann auch mittels expliziter Typumwandlung geschrieben werden:

(double) ((long) 10 + 3L) - 123.456

Während bei dem obigen Ausdruck die explizite Typumwandlung nicht notwendig ist, da die Typumwandlungen auch automatisch entsprechend richtig ausgeführt werden, sind im folgenden Beispiel explizite Typumwandlungen für die richtige Ausführung notwendig, da sonst ein Integer-Überlauf ein falsches Ergebnis liefern würde:

cout << "1000000 * 1000000 = ";
cout << (double) 1000000 * (double) 1000000 << endl;


In C++ können Typumwandlungen auch in einer anderen, den Funktionsaufrufen ähnlichen Schreibweise geschrieben werden, die aus Gründen der Einheitlichkeit und besseren Übersichtlichkeit vorzuziehen ist.

C: (int) d C++: int (d)
  (long) (a + b)   long (a + b)
  (char *) p   (char *) (p)

Implizite Typumwandlungen sollten möglichst vermieden und stattdessen explizit vorgenommen werden, da hierdurch Möglichkeiten für viele schwer zu findende Fehler ausgeschaltet werden können.

Auf das Beispiel mit der Klasse complex angewendet heißt das, dass es sinnvoller wäre, anstatt vieler verschiedener Operator-Funktionen mit allen möglichen Kombinationen von Datentypen nur grundsätzlich eine Operator-Funktion pro Operator für alle Datentypen zu schreiben. Trotzdem soll auf den Mechanismus der automatischen oder expliziten Typumwandlung nicht verzichtet werden.

Doch wie können Variablen anderer Typen automatisch oder explizit in Objekte der Klasse complex umgewandelt werden?

Konstruktoren als Typumwandlungs-Operatoren

Grundsätzlich stellt jeder Konstruktor mit genau einem Argument eine Beschreibung dar, wie aus einer Variablen mit dem Typ des Arguments ein Objekt der entsprechenden Klasse erzeugt werden kann. Diese Beschreibung wird von C++ auch für die Typumwandlung verwendet.

Schließlich wird ja bei der Auswertung eines Teilausdrucks wie complex(3) ein temporäres Objekt erzeugt. Und beim Erzeugen eines Objekts wird der Konstruktor ausgeführt.

Für die Klasse complex bedeutet dies, dass nun der folgende Programmcode hinzugefügt werden kann.

kap11_06.cpp   (komplettes Beispielprogramm)

class complex
{
   ... // siehe oben
   public:
      // Konstruktoren:
      complex();                   // wie gehabt
      complex(double r, double i); // wie gehabt
      complex(int i);              // int -> complex
      complex(double d);           // double -> complex
};

complex::complex(int i)
{
   re = i;
   im = 0;
}

complex::complex(double d)
{
   re = d;
   im = 0;
}

Nun lassen sich implizite und explizite Typumwandlungen durchführen:

complex a(1,1), b(2,2), c;

c = a + a;                     // -> complex + complex
c = complex(3);                // -> complex(int)
c = 3;                         // -> complex(int)
c = complex(4.6);              // -> complex(double)
c = 4.6;                       // -> complex(double)
c = a + 3;                     // -> complex + complex(int)
c = a + 4.6;                   // -> complex + complex(double)
c = 3 - b;                     // -> complex(int) - complex
c = complex(3) + complex(4.6); // -> complex(int) + complex(double)
c = complex(3 + 4.6);          // -> complex(double(int) + double)

Hinweis:
Die Zeile c = 3 - b funktioniert nur, wenn der --Operator als friend-Funktion überladen wurde, da sonst die impliziete Typumwandlung nicht funktioniert.

Typumwandlungsoperator-Methoden

Mittels Konstruktoren mit nur einem Parameter ist es möglich, beliebige andere Variablen in Objekte der eigenen Klasse umzuwandeln. Oft ist es aber auch wünschenswert, Objekte der eigenen Klasse in Variablen anderer Typen (z.B. int, double, usw.) umzuwandeln. Sogenannte Typumwandlungsoperator-Methoden erfüllen genau diese Aufgabe.

Eine Typumwandlungsoperator-Methode ist eine Methode, welche keine Argumente und keinen explizit angegebenen Ergebnistyp besitzt und deren Name aus dem Schlüsselwort operator und dem Namen des Zieltyps besteht. Der Ergebnistyp solcher Methoden ist bereits durch den Namen vorgegeben. Der Code dieser Methode muss die Umwandlung des aktuellen Objekts in den geforderten Zieltyp beinhalten. Typumwandlungsoperator-Methoden sollten wie Konstruktoren immer public deklariert werden.

Im folgenden Beispiel wird die Klasse complex um eine Typumwandlungsoperator-Methode erweitert, die eine komplexe Zahl in eine double-Zahl umwandelt. Dabei soll der double-Wert gleich dem Realteil sein.

kap11_07.cpp   (komplettes Beispielprogramm)

class complex
{
   ... // siehe oben
   public:
      operator double() { return re; }
};

complex a(1, 1), b(2, 2);
double d;

d = double(a);             // -> double(complex)
d = a;                     // -> double(complex)
d = a + b;                 // -> double(complex + complex)
cout << double(a) << endl; // -> double(complex)

Eine implizite Typumwandlung erfolgt immer bei der Parameterübergabe an Funktionen. Da Operationen in Ausdrücken eigentlich auch Funktionen sind, werden diese Typumwandlungen auch dort durchgeführt. Dabei versucht der Compiler, die Parametertypen immer so umzuwandeln, dass es eine Funktion dafür gibt. Allerdings kann es dabei auch zu Mehrdeutigkeiten kommen, wie das folgende Beispiel zeigt.

kap11_08.cpp

#include <iostream>

using namespace std;

class Test
{  public:
      Test(int i);
      operator int();
      Test operator+ (const Test &a);
};

int main()
{
   Test a(2);

   a = a + 3; // 2 Möglichkeiten:
              // Test::operator+ (a, Test(3))   oder
              // int::operator+ (int(a), 3)

   return 0;
}

Der Compiler muss an der Stelle a = a + 3 eine Mehrdeutigkeit feststellen, da es verschiedene Möglichkeiten des Einsatzes von Operatoren gibt. Der Programmierer muss durch eine explizite Typumwandlung einen Parameter umwandeln, z.B. a = a + Test(3).

11.5. Kopieren von Objekten

Zum Kopieren von Objekten wird grundsätzlich der Operator = verwendet. Dabei werden normalerweise alle Datenkomponenten des Quellobjekts im Verhältnis 1:1 in das (bereits existierende) Zielobjekt übertragen.

Verwendet ein Objekt auch dynamische Daten, welche z.B. im Konstruktor erzeugt und im Destruktor gelöscht werden, so werden die Daten selbst nicht kopiert, sondern nur die Zeiger bzw. Referenzen auf diese. Die alten, dynamischen Daten des Zielobjekts werden ferner auch nicht freigegeben.

Beispiel:
Die Klasse Mitarbeiter dient zur Verwaltung verschiedener Daten eines Angestellten. Dabei sollen auch evtl. Zweitwohnsitze mit ihrer genauen Adresse gespeichert werden können. Da nur wenige Mitarbeiter einen zweiten Wohnsitz besitzen, ist es sinnvoll, diese Daten dynamisch zu speichern, um nur bei vorhandenem zweiten Wohnsitz auch Speicherplatz zu belegen.

struct Wohnsitz
{  char PLZ[6];
   char Ort[46];
   char Strasse[50];
   int Nr;
};

class Mitarbeiter
{  char Name[50];
   Wohnsitz HauptWS;
   Wohnsitz *ZweitWS;
   // ...
};

Wird nun ein Mitarbeiter auf einen anderen Mitarbeiter kopiert (also zugewiesen), passiert folgendes:

Mitarbeiter M1, M2;

M2 = M1; // bei der Zuweisung werden die (statischen) Daten kopiert

Hierbei werden die statischen Daten kopiert, d.h. für den Zweitwohnsitz wird nur der Zeiger kopiert, nicht aber die dynamische Datenstruktur selbst. Es zeigen nun die Zeiger beider Mitarbeiter auf den gleichen Speicherbereich. Wird nun der Zweitwohnsitz von dem einen Mitarbeiter verändert, so verändert sich automatisch auch der vom anderen Mitarbeiter. Da der Zeiger vom zweiten Mitarbeiter überschrieben wurde, existiert nun kein Zeiger mehr auf den vorigen Zweitwohnsitz, also auf die dynamischen Daten, auf die er vorher zeigte. Daher können diese auch nicht mehr freigegeben werden (memory leak).

Um dieses im allgemeinen unerwünschte Verhalten zu umgehen und trotzdem Objekte mit dem Operator = auf einfache Weise kopieren zu können, ist es notwendig, diesen zu überladen und die notwendigen Schritte zum korrekten Kopieren explizit durchzuführen. Für die Klasse Mitarbeiter könnte das so aussehen:

class Mitarbeiter
{     // ... (siehe oben)
   public:
      Mitarbeiter &operator= (const Mitarbeiter &m);
};

Mitarbeiter &Mitarbeiter::operator= (const Mitarbeiter &m)
{  strcpy(Name, m.Name);
   HauptWS = m.HauptWS;
   if (ZweitWS) // ZweitWS != NULL ?
      delete ZweitWS;
   if (m.ZweitWS) // m.ZweitWS != NULL ?
   {  ZweitWS = new Wohnsitz;
      *ZweitWS = *m.ZweitWS;
   }
   else
      ZweitWS = NULL;
   return *this;
}

Zuerst (in den ersten beiden Programmzeilen der Operator-Methode) werden die statischen Daten kopiert. Als nächstes wird geprüft, ob im Ziel-Objekt bereits ein Zweitwohnsitz vorhanden ist, wenn ja, wird dieser dynamische Speicherbereich freigegeben. Dann wird geprüft, ob im Quell-Objekt ein Zweitwohnsitz eingetragen ist, wenn ja, wird entsprechend Speicherplatz reserviert und der Zweiwohnsitz kopiert (hier reicht eine einfache Zuweisung, da die Struktur selber keinen Zeiger enthält), wenn nein, wird ein NULL-Zeiger zugewiesen.

Mit Hilfe dieses zusätzlichen Operators können nun Mitarbeiter-Objekte korrekt kopiert werden. Es gibt aber immer noch einen Fall, in dem das Kopieren nicht wie zunächst erwartet funktioniert: Beim Aufruf einer Funktion bzw. Methode, die als Parameter ein Mitarbeiter-Objekt als Wert erhält, wird nicht der Zuweisungsoperator verwendet, sondern ein Kopier-Konstruktor (schließlich wird dabei auch ein neues Objekt erzeugt). Und der Standard-Kopier-Konstruktor kopiert wieder nur die statischen Daten. Also muss auch noch ein eigener Kopier-Konstruktor geschrieben werden.

class Mitarbeiter
{     // ... (siehe oben)
   public:
      // Kopier-Konstruktor:
      Mitarbeiter (const Mitarbeiter &m);
};

// Standard-Kopier-Konstruktor:
Mitarbeiter::Mitarbeiter (const Mitarbeiter &m)
{  *this = m; // Kopieren der statischen Daten
};

// eigener Kopier-Konstruktor:
Mitarbeiter::Mitarbeiter (const Mitarbeiter &m)
{  // fast gleicher Inhalt wie bei Mitarbeiter::operator=
   strcpy(Name, m.Name);
   HauptWS = m.HauptWS;
   if (m.ZweitWS) // m.ZweitWS != NULL ?
   {  ZweitWS = new Wohnsitz;
      *ZweitWS = *m.ZweitWS;
   }
   else
      ZweitWS = NULL;
}

Hinweise:

• Das Kopieren von Objekten kann auch gänzlich unterbunden werden, indem ein Kopier-Konstruktor und ein entsprechender Zuweisungsoperator zwar deklariert, aber nicht definiert werden.

• Wird der Operator = überladen, so ist es nicht unbedingt notwendig, das Argument als Referenzparameter zu schreiben, jedoch meistens empfehlenswert. Beim Kopier-Konstruktor ist es aber Pflicht, das Argument als Referenzparameter zu schreiben. Sonst wäre eine Endlosrekursion die Folge, da bei der Übergabe des Parameters ein neues Objekt erzeugt und damit der Kopier-Konstruktor selber wieder aufgerufen werden würde.

• Wenn die Notwendigkeit des Überladens besteht, sind der Kopier-Konstruktor und der Zuweisungsoperator = in der Regel verschieden zu programmieren. Beim Konstruktor liegt ein uninitialisierter Speicherbereich vor, beim Zuweisungsoperator muss meist vorher geprüft werden, ob nicht ein dynamischer Speicherbereich erst freigegeben werden muss.

• Beim Überladen des Zuweisungsoperators = sollte auch darauf geachtet werden, dass eine Zuweisung der Form a = a nicht zum Absturz führt. Im oben angegebenen Beispiel wurde darauf keine Rücksicht genommen, so dass in diesem Fall der Speicherplatz freigegeben wird, der anschließend noch zum Kopieren nötig ist. Um dieses Problem zu umgeben, muss beim Zuweisungsoperator als erstes der Parameter m auf Gleichheit mit this geprüft werden.
if (&m == this)
   return *this; // nichts kopieren

• Leider wird in C++ bei der Initialisierung auch die alte Form noch zugelassen, um mit C kompatibel zu bleiben. Die meisten C++-Compiler führen bei einer Initialisierung der Form "class complex a(2,3), b(a)" wie erwartet den Kopierkonstruktor durch, bei einer Initialisierung der Form "class complex a(2,3), b = a" allerdings den Zuweisungsoperator. Da in der Zuweisung nicht damit gerechnet werden kann, dass b eine noch uninitialisierte Größe ist, führt das oft zu schwerwiegenden Speicherzugriffsfehlern. Initialisieren Sie also immer mit der neuen Schreibweise (Initialwert in Klammern).

11.6. Überladen des Funktionsoperators()

Mit Hilfe des Funktionsoperators kann ein Objekt auch ohne Angabe einer Methode - so als wäre es eine Funktion - aufgerufen werden, z.B. Objekt(Par1, ...). Dadurch kann sich die Schreibarbeit für den Programmierer vereinfachen.

Im Gegensatz zu allen anderen Operatoren ist die Anzahl der Parameter beim Funktionsoperator sowie der Datentyp des Rückgabewertes bei der Definition frei wählbar. Dadurch entstehen hier noch mehr Möglichkeiten des Überladens als bei anderen Operatoren.

Neben dem Funktionsoperator () (also mit den runden Klammern) kann auch ein Funktionsoperator mit eckigen Klammern definiert werden (also []).

Beispiel:
#include <iostream>

using namespace std;

class FktOp
{     int Wert;
   public:
      FktOp(int W)       { Wert = W; }
      void Ausgabe()     { cout << Wert << endl; }
      void operator() () { Ausgabe(); }
      int operator[] (int W)
      {  int Temp = Wert; // alten Wert merken

         Wert = W;        // neuen Wert setzen
         Ausgabe();       // neuen Wert ausgeben
         return Temp;     // alten Wert zurückgeben
      }
};

int main()
{  FktOp F(5);

   cout << "Aufruf über Methode: ";
   F.Ausgabe();
   cout << "Aufruf über Funktionsoperator(): ";
   F();
   cout << "Aufruf über Funktionsoperator[]: ";
   cout << "Ergebnis des Funktionsoperators[]: " << F[7] << endl;
   return 0;
}

11.7. Überladen des Komponentenzugriffsoperator ->

Der Zugriff auf eine Komponente einer Klasse (oder einer Struktur) über einen Zeiger mit -> (z.B. Objekt->Komponente) ist eine Operation, die aus zwei Teilen besteht. Zuerst wird der linke Teil verarbeitet (Objekt->): Hier wird der Zeiger auf die Klasse - oder besser gesagt: auf die Struktur innerhalb der Klasse - ermittelt. Dann wird mit diesem Zeiger auf die Komponente der Struktur zugegriffen.

Wird der Operator -> überladen, so muss dieser Operator als Ergebnis den ersten Teil - also einen Zeiger auf die Struktur - zurückgeben. Wird also die normale Schreibweise   Objekt->Komponente   in die Operatorschreibweise umgesetzt, sieht es wie folgt aus:   (Objekt.operator->())->Komponente. Wichtig: Das Ergebnis des selbstdefinierten -> Operators muss als linke Seite des gesamten Operators verwendbar sein!

Durch das Überladen des -> Operators erhält man die Möglichkeit, vor dem Zugriff auf die Komponente noch einzugreifen. Beispielsweise könnten damit "intelligente Zeiger" programmiert werden, die vor dem Komponentenzugriff überprüfen, ob dieser überhaupt sinnvoll oder möglich ist. Außerdem kann im Bedarfsfall eine neue linke Seite durch den Funktionswert angegeben werden.

Beispiel:
#include <iostream>
#include <string>

using namespace std;

class IntelligenterZeiger
{     // die eigentliche Struktur (private)
      struct Struktur
      {  int Nummer;
         char Bezeichnung[100];
         // Konstruktoren für die Struktur anlegen:
         Struktur()
         {  Nummer = 0;
            strcpy(Bezeichnung, "Nicht vorhanden!");
         }
         Struktur(int Nr, char *Bez)
         {  Nummer = Nr;
            strcpy(Bezeichnung, Bez);
         }
      };

      Struktur *Element;
      bool Vorhanden;

   public:
      IntelligenterZeiger(): Vorhanden(false) {}
      IntelligenterZeiger(int Nr, char *Bez)
      {  Vorhanden = true;
         Element = new Struktur(Nr, Bez);
      }
      ~IntelligenterZeiger()
      {  if (Vorhanden)
            delete Element;
      }

      // eigener Komponentenzugriffsoperator:
      Struktur * operator->()
      {  if (!Vorhanden)
         {  // wenn nicht vorhanden, dann leere Struktur anlegen:
            Element = new Struktur;
            Vorhanden = true;
         }
         // private Struktur für Komponentenzugriff zurückgeben:
         return Element;
      }
};

int main()
{  IntelligenterZeiger a(5, "Objekt"), b;

   cout << "Nummer: " << a->Nummer << "; "
        << "Bezeichnung: " << a->Bezeichnung << endl;
   cout << "Nummer: " << b->Nummer << "; "
        << "Bezeichnung: " << b->Bezeichnung << endl;
   return 0;
}

Das Programm erzeugt folgende Ausgabe:
Nummer: 5; Bezeichnung: Objekt
Nummer: 0; Bezeichnung: Nicht vorhanden!

Hinweis:
Der Operator "." kann nicht überladen werden!

11.8. Überladen von new und delete

Auch die Operatoren new und delete können überladen werden. Allerdings sind dabei einige besondere Regeln zu beachten:

• Der erste Parameter von operator new muss immer den Typ size_t haben. Dieser Typ ist je nach Compiler in stddef.h oder types.h definiert. Dieser Parameter wird immer mit der Größe des bei new angegebenen Typs belegt.

• Der Operator new kann auch weitere Parameter haben und damit beliebig überladen werden. Die Parameter (außer dem ersten, der ja automatisch belegt wird) werden dann folgendermaßen übergeben:
new (<Par1> P1, ...) <classname> (<Par1> P1, ...)

• Da bei Aufruf von operator new noch kein Objekt existieren kann, ist new immer automatisch eine statische Funktion. new kennt also kein aktuelles Objekt.

• Hat eine Klasse einen Operator new, so wird dieser verwendet, wenn ein Objekt der Klasse dynamisch erzeugt wird. Ansonsten wird der globale Operator new verwendet. Wird allerdings ein Vektor dynamisch erzeugt, so wird immer der globale Operator new verwendet!

• Der Rückgabewert der Methode operator new muss immer void * sein.

• Wenn new überladen wird, kommt im Fehlerfall in der Regel der newhandler nicht zum Einsatz.

• Wird der Operator delete in einer Klasse überladen, so muss die Methode immer den Rückgabetyp void haben.

• Der Operator delete hat immer einen Parameter vom Typ void *. Er kann auch einen zweiten Parameter vom Typ size_t haben, der, falls vorhanden, mit der Größe des Objekts in Byte belegt ist.

• Pro Klasse kann es zwar mehrere new-Operatormethoden, aber immer nur eine delete-Operatormethode geben.

• Soll in einer Klasse, die einen überladenen Operator new oder delete besitzt, auf den globalen Operator zugegriffen werden, so muss der Bereichsoperator :: verwendet werden (also ::new bzw. ::delete).

Mit Hilfe des Überladens der Operatoren new und delete kann beispielsweise für eine eigene Klasse eine selbstgeschriebene dynamische Speicherverwaltung implementiert werden.

Leider funktioniert das Überladen von new und delete nicht bei allen C++-Compilern gleich. Oft werden viele weitere Ausnahmeregeln definiert. So ist es beispielsweise manchmal möglich, auch die globalen Operatoren new und delete zu überladen.