C++: Konstruktoren und Destruktoren

Ein Konstruktor legt fest, welche Aktionen beim Erzeugen eines Objektes ausgef├╝hrt werden und sind daher ein zentraler Bestandteil einer Klasse. Wie Methoden k├Ânnen sie ├╝berladen werden. Die Eigenschaften von Konstruktoren (und des Destruktors, der beim L├Âschen eines Objektes aufgerufen wird), einige spezielle Konstruktoren (Default-Konstruktor, Kopier-Konstruktor) sowie das Delegations-Prinzip werden hier vorgestellt.
Noch keine Stimmen abgegeben
Noch keine Kommentare

Einordnung des Artikels

In den bisherigen Kapiteln ├╝ber C++: Fortgeschrittene Syntax wurde ein kleines Statistik-Projekt entwickelt, insbesondere wurde in C++: Grundlagen der objektorientierten Programmierung (OOP) eine objekt-orientierte Variante entworfen. Daran werden nun die mit Konstruktoren und Destruktoren verbundenen Konzepte erl├Ąutert.

Einf├╝hrung

F├╝r die Klasse RandomGenerator wurde in dem Statistik-Projekt bereits ein Konstruktor angeboten, n├Ąmlich der Konstruktor, der in einer Initialisierungs-Liste die beiden Datenelemente min und max der Klasse RandomGenerator setzt:

RandomGenerator::RandomGenerator(int minimum, int maximum) : min {minimum}, max {maximum}
{
}

Es wurde auch diskutiert, dass dieser Konstruktor gleichwertig ist zu dem ÔÇö leichter verst├Ąndlichen ÔÇö Konstruktor, der die Zuweisungen nutzt:

RandomGenerator::RandomGenerator(int minimum, int maximum)
{
    min = minimum;
    max = maximum;
}

Beachtenswert ist insbesondere, dass der Konstruktor auf die privaten Datenelemente min und max zugreifen darf; es ist nicht n├Âtig, dazu die ├Âffentlichen Setter- und Getter-Methoden zu verwenden (wie bei einem Zugriff von au├čerhalb der Klasse).

Dieses Beispiel soll in den folgenden Unterabschnitten weiterentwickelt werden.

├ťberladen von Konstruktoren und das Delegations-Prinzip

├ťberladen von Konstruktoren

Konstruktoren ├Ąhneln in vielerlei Hinsicht Funktionen, insbesondere k├Ânnen sie auch ├╝berladen werden. Wie bei Funktionen k├Ânnen weitere Konstruktoren angeboten werden, die sich in der Signatur (also der Parameterliste) unterscheiden m├╝ssen.

Beispiel:

Ein weiterer Konstruktor soll f├╝r die Klasse RandomGenerator zus├Ątzlich zum bereits bekannten Konstruktor

RandomGenerator(int minimum, int maximum);

testen, ob die beiden Zahlen min und max tats├Ąchlich min < max erf├╝llen. Genauer:

1. Man kann im neuen Konstruktor eine Boolsche Variable validate setzen.

  • Falls validate == true , wird die unten beschriebene ├ťberpr├╝fung ausgef├╝hrt.
  • Falls validate == false , wird nichts anderes gemacht als beim bereits bekannten Konstruktor.

2. Die ├ťberpr├╝fung der Eingabewerte beinhaltet:

  • Falls min < max, wird RandomGenerator(int minimum, int maximum) ausgef├╝hrt.
  • Falls min = max, wird der Nutzer darauf hingewiesen, aber es wird dennoch RandomGenerator(int minimum, int maximum) ausgef├╝hrt.
  • Falls min > max, wird der Nutzer darauf hingewiesen, aber min und max werden mit den eingegebenen Werte so gesetzt, dass min < max.

Die Deklaration des neuen Konstruktors erfolgt wieder in RandomGenerator.h:

// Konstruktor zum Setzen von min und max und zus├Ątzlicher ├ťberpr├╝fung der Eingabe
RandomGenerator(int minimum, int maximum, bool validate);

Die Implementierung befindet sich in RandomGenerator.cpp. Der folgende Quelltext erf├╝llt zwar die oben beschriebenen Anforderungen, ist aber in sehr schlechtem Stil geschrieben:

// Nicht nachahmen!! Der Quelltext enth├Ąlt leicht vermeidbare Wiederholungen!

// Konstruktor zum Setzen von min und max und zus├Ątzlicher ├ťberpr├╝fung der Eingabe
RandomGenerator::RandomGenerator(int minimum, int maximum, bool validate)
{
    if (!validate)          // keine ├ťberpr├╝fung von minimum und maximum
    {
        min = minimum;
        max = maximum;
    }
    else                // ├ťberpr├╝fung von minimum und maximum
    {
        if (minimum < maximum)
        {
            min = minimum;
            max = maximum;
        }
        else
            if (minimum == maximum)
            {
                cout << "Achtung: minimum == maximum!" << endl;
                min = minimum;
                max = maximum;
            }
            else
            {
                cout << "Achtung: Sie haben minimum > maximum eingegeben!\nDie Zahlen werden richtig sortiert!" << endl;
                max = minimum;
                min = maximum;
            }
    }
}

Werden beide Konstruktoren gleichzeitig angeboten, spricht man davon, dass der Konstruktor ├╝berladen ist. Damit gewinnt das Programm an Flexibilit├Ąt, da man die Konstruktoren so vorbereiten kann, wie sp├Ąter die Objekte der Klasse erzeugt werden sollen.

Dennoch ist das Beispiel nicht nachahmenswert. Warum soll der Konstruktor nicht so aufgebaut werden? Aus zwei Gr├╝nden:

  • Der Konstruktor wiederholt an mehreren Stellen, den eigenen Quelltext (immer wenn min und max gesetzt werden).
  • Der Konstruktor wiederholt den Quelltext, der schon im Konstruktor RandomGenerator::RandomGenerator(int minimum, int maximum) enthalten ist.

Dass man Quelltext-Wiederholungen vermeiden soll, wurde schon f├╝r Funktionen erkl├Ąrt, dies gilt nat├╝rlich auch f├╝r Konstruktoren. Besser w├Ąre es daher, die sich wiederholenden Quelltexte in eine private Funktion auszulagern und diese Funktion aufzurufen.

Es gibt sogar eine noch bessere L├Âsung, die dar├╝ber hinausgeht: In der Initialisierungs-Liste eines Konstruktors kann ein anderer Konstruktor aufgerufen werden; man nennt dieses Prinzip Delegation, da Aufgaben weitergereicht oder delegiert werden.

Die Delegation

Eine verbesserte Version der obigen Konstruktoren, die die Delegation nutzt, k├Ânnte dann so aussehen:

In der Datei mit den Deklarationen stehen die beiden Konstruktoren und eine private Methode init(), die den sich wiederholenden Quelltext beschreibt; die Pr├Ąprozessor-Direktiven und die restlichen Datenelemente und Methoden sind nicht gezeigt:

// RandomGenerator.h

// includes usw.

class RandomGenerator
{
public:
    
    // Konstruktor zum Setzen von min und max
    RandomGenerator(int minimum, int maximum);

    // Konstruktor zum Setzen von min und max und zus├Ątzlicher ├ťberpr├╝fung der Eingabe
    RandomGenerator(int minimum, int maximum, bool validate);
    
    // weitere Methoden
    
private:

    void init(int minimum, int maximum);
    
    // ...
};

Wie sp├Ąter die Delegation genutzt wird, ist an der Deklaration noch nicht erkennbar.

Die Implementierung zeigt:

  • den neuen Konstruktor mit Validierung der Eingaben (Zeile 8 bis 33),
  • den einfacheren Konstruktor ohne Validierung (Zeile 3 bis 5), der lediglich den komplizierteren Konstruktor aufruft,
  • sowie die private Methode init(), die die Datenelemente min und max setzt:
// RandomGenerator.cpp

RandomGenerator::RandomGenerator(int minimum, int maximum) : RandomGenerator(minimum, maximum, false)           // Delegation
{
}

// Konstruktor zum Setzen von min und max und zus├Ątzlicher ├ťberpr├╝fung der Eingabe
RandomGenerator::RandomGenerator(int minimum, int maximum, bool validate)
{
    if (!validate)          // keine ├ťberpr├╝fung von minimum und maximum
    {
        init(minimum, maximum);
    }
    else                    // ├ťberpr├╝fung von minimum und maximum
    {
        cout << "├ťberpr├╝fung" << endl;
        if (minimum < maximum)
        {
            init(minimum, maximum);
        }
        else
            if (minimum == maximum)
            {
                cout << "Achtung: minimum == maximum!" << endl;
                init(minimum, maximum);
            }
            else
            {
                cout << "Achtung: Sie haben minimum > maximum eingegeben!\nDie Zahlen werden richtig sortiert!" << endl;
                init(maximum, minimum);          // Achtung: Vertauschung der Reihenfolge
            }
    }
}

void RandomGenerator::init(int minimum, int maximum)
{
    min = minimum;
    max = maximum;
}

Zur Erkl├Ąrung:

  1. Die Delegation ist in Zeile 3 erkennbar: wird der einfache Konstruktor RandomGenerator(int minimum, int maximum) aufgerufen, wird validate gleich false gesetzt und damit der Konstruktor RandomGenerator(int minimum, int maximum, false) aufgerufen.
  2. Der K├Ârper des einfachen Konstruktors kann leer bleiben, da alle Aktionen nach dem Weiterreichen erledigt werden (Zeile 4 und 5).
  3. Im Konstruktor RandomGenerator(int minimum, int maximum, bool validate) werden die sich wiederholenden Quelltexte in die private Methode init() ausgelagert, die in Zeile 12, 19, 25, 30) aufgerufen wird.

Der Vorteil des Delegations-Prinzips liegt weniger darin, dass man weniger Schreibarbeit hat. Viel wichtiger ist, dass der Quelltext leichter wartbar wird: Die Aktionen, die in der alten Version im Konstruktor RandomGenerator(minimum, maximum) enthalten waren, sind jetzt in der privaten Methode init() enthalten. Sollte sich die Anforderung an den Konstruktor RandomGenerator(minimum, maximum) einmal ├Ąndern, so muss nur der Quelltext von init() angepasst werden. In der nicht nachahmenswerten Version des Konstruktors RandomGenerator(minimum, maximum, bool validate) m├╝sste man alle Stellen aufsuchen, die den Aktionen von init() entsprechen und diese ab├Ąndern. Vergisst man dabei eine Stelle, werden andere Aktionen ausgef├╝hrt, obwohl dies nicht gew├╝nscht ist.

Tip: Wenn Sie Konstruktoren ├╝berladen m├╝ssen, versuchen Sie die Delegation und private Methoden zu nutzen, um Quelltext-Wiederholungen zu vermeiden. Bei nachtr├Ąglichen ├änderungen des Quelltextes sollten dann die abzu├Ąndernden Stellen leicht auffindbar sein.

Gegenbeispiel: Falsche Anwendung der Delegation

Die nicht nachahmenswerte Version des Konstruktors von oben

// Konstruktor zum Setzen von min und max und zus├Ątzlicher ├ťberpr├╝fung der Eingabe
RandomGenerator::RandomGenerator(int minimum, int maximum, bool validate)

verf├╝hrt dazu, das Delegations-Prinzip falsch einzusetzen.

Das folgende Beispiel zeigt eine Version der beiden Konstruktoren, die auf den ersten Blick identisch ist zur Version, die die Delegation nutzt. Der Konstruktor erf├╝llt aber nicht die Aufgabe, die man von ihm erwartet.

Gegenbeispiel: Falscher Einsatz des Delegations-Prinzips

Deklaration der Konstruktoren:

// Konstruktor zum Setzen von min und max
// Es wird versucht, mit Hilfe der Delegation Aufgaben an diesen Konstruktor weiterzuleiten
RandomGenerator(int minimum, int maximum);

// Konstruktor zum Setzen von min und max und zus├Ątzlicher ├ťberpr├╝fung der Eingabe
RandomGenerator(int minimum, int maximum, bool validate);

Implementierung der Konstruktoren:

// NICHT NACHAHMEN: Die Konstruktoren arbeiten nicht wie erwartet!!
// Konstruktor mit Initialisierungs-Liste
// Es wird versucht, mit Hilfe der Delegation Aufgaben an diesen Konstruktor weiterzuleiten
RandomGenerator::RandomGenerator(int minimum, int maximum) : min {minimum}, max {maximum}
{
}

// Konstruktor zum Setzen von min und max und zus├Ątzlicher ├ťberpr├╝fung der Eingabe
RandomGenerator::RandomGenerator(int minimum, int maximum, bool validate)
{
    if (!validate)          // keine ├ťberpr├╝fung von minimum und maximum
    {
        RandomGenerator(minimum, maximum);                  // Versuch der Weiterleitung! Warum geht das nicht??
    }
    else                    // ├ťberpr├╝fung von minimum und maximum
    {
        cout << "├ťberpr├╝fung" << endl;
        if (minimum < maximum)
        {
            RandomGenerator(minimum, maximum);              // Versuch der Weiterleitung (siehe oben)
        }
        else
            if (minimum == maximum)
            {
                cout << "Achtung: minimum == maximum!" << endl;
                RandomGenerator(minimum, maximum);          // Versuch der Weiterleitung (siehe oben)
            }
            else
            {
                cout << "Achtung: Sie haben minimum > maximum eingegeben!\nDie Zahlen werden richtig sortiert!" << endl;
                RandomGenerator(maximum, minimum);          // Achtung: Vertauschung der Reihenfolge
            }
    }
}

In obigem Gegenbeispiel wird versucht, vom Konstruktor mit drei Argumenten die Aufgaben an den Konstruktor mit zwei Argumenten weiterzureichen. Behauptet wird, dass dies nicht der Fall ist ÔÇö aber warum?

Man muss sich dazu klarmachen, dass der Konstruktor

RandomGenerator(int minimum, int maximum, bool validate)

etwa folgenderma├čen (aus der main()-Methode) aufgerufen wird:

RandomGenerator rg(1, 6, true);         // Erzeugen eines W├╝rfels; mit ├ťberpr├╝fung, ob min < max

Diese Programmzeile soll also daf├╝r sorgen, dass das Objekt rg mit Hilfe des Konstruktors (mit drei Argumenten) erzeugt wird. Man kann jetzt leicht nachvollziehen, welcher Fall vorliegt (n├Ąmlich validate == true und minimum < maximum) und daher wird Zeile 20 ausgef├╝hrt (in der Implementierung der Konstruktoren):

RandomGenerator(minimum, maximum);

Aber was geschieht in Zeile 20? (Und genauso in den Zeilen 13, 26, 31 mit dem Aufruf dieses Konstruktors.) Es wird ein neues Objekt vom Typ RandomGenerator mit Hilfe des Konstruktors mit zwei Argumenten erzeugt. Das hei├čt die Erzeugung des Objektes rg l├Âst die Erzeugung eines weiteren Objektes aus; dieses liegt zwar irgendwo im Speicher, kann aber nicht durch rg angesprochen werden.

Obige Implementierung der Konstruktoren ist syntaktisch v├Âllig korrekt, die Delegation arbeitet hier aber nicht so wie gew├╝nscht.

Dieses Beispiel zeigt somit auch, warum die Delegation nur in der Initialisierungs-Liste verwendet werden kann.

Der Standard-Konstruktor (Default-Konstruktor)

Die Klasse RandomGenerator besitzt jetzt zwei Konstruktoren, die das Delegations-Prinzip nutzen; ihre Deklarationen lauten:

// Konstruktor zum Setzen von min und max
// Delegation: der Konstruktor ruft den anderen Konstruktor mit validate == false auf
RandomGenerator(int minimum, int maximum);

// Konstruktor zum Setzen von min und max und zus├Ątzlicher ├ťberpr├╝fung der Eingabe
RandomGenerator(int minimum, int maximum, bool validate);

Nach obigen Diskussionen sollte klar sein, warum gerade diese Konstruktoren angeboten werden.

Oftmals wird auch ein zus├Ątzlicher Konstruktor angeboten, der weder Eingabewerte noch irgendwelche auszuf├╝hrenden Aktionen besitzt. Seine Implementierung w├╝rde hier also lauten:

// Default-Konstruktor
RandomGenerator::RandomGenerator()
{
}

Dieser Konstruktor wird meist als der Standard-Konstruktor oder Default-Konstruktor einer Klasse bezeichnet. Er ist so einfach, das es schon wieder schwer zu verstehen ist, warum dieser Konstruktor ben├Âtigt werden soll.

Der Default-Konstruktor hat mehrere besondere Eigenschaften:

1. Besitzt eine Klasse keinen Konstruktor, wird der Default-Konstruktor automatisch vom Compiler erzeugt und kann aufgerufen werden.

2. Sobald eine Klasse (mindestens) einen selbtgeschriebenen Konstruktor hat, wird der Default-Konstruktor nicht vom Compiler erzeugt. Wenn man ihn ben├Âtigt, muss man ihn selbst implementieren.

3. Der Aufruf des Default-Konstruktors erfolgt ohne runde Klammern (anschlie├čend werden f├╝r das neu erzeugte Objekt die Datenelemente min und max gesetzt):

RandomGenerator defaultGenerator;           // Aufruf des Default-Konstruktors
defaultGenerator.setMin(0);                 // Verwendung des erzeugten Objektes
defaultGenerator.setMax(1);                 // min und max werden selbst gesetzt (nicht im Konstruktor)

Da Konstruktoren ├Ąhnlich wie Funktionen aufgerufen werden, h├Ątte man vermutlich erwartet:

// Falscher Gebrauch des Default-Konstruktors
RandomGenerator defaultGenerator();         // Compiler-Fehler

Hier stellt sich sofort die Frage: Wozu soll ein Default-Konstruktor eigentlich gut sein, wenn er keine Aktion ausf├╝hrt? Ein Konstruktor ist doch dazu da ein Objekt zu erzeugen und wie kann dies ohne irgendwelche Aktionen geschehen?

Wird die Frage so gestellt, offenbart sie ein tiefes Missverst├Ąndnis ├╝ber die Bedeutung des Konstruktors, die allerdings durch seinen Namen suggeriert wird. Der Aufruf eines Konstruktors (egal ob der Default-Konstruktor oder ein anderer selbstdefinierter Konstruktor aufgerufen wird) besteht immer aus zwei Phasen:

1. Es wird Speicherplatz reserviert f├╝r das zu erzeugende Objekt. Da der Compiler die Klasse kennt, wei├č er, welche Datenelemente (insbesondere deren Datentypen) die Klasse besitzt und kann diese Aufgabe erledigen.

2. Anschlie├čend wird der Konstruktor aufgerufen. Welcher dies ist, erkennt der Compiler an der Parameterliste des Aufrufs; falls kein Konstruktor vorhanden ist, wird der Default-Konstruktor erzeugt und ausgef├╝hrt. Im folgenden Beispiel wird einmal der Konstruktor mit drei Argumenten und dann der Default-Konstruktor (sofern er implementiert ist) aufgerufen:

RandomGenerator rg(1, 6, true);
RandomGenerator defaultGenerator;

Die erste Phase kann nicht von au├čen beeinflusst werden; nur die Aktionen der zweiten Phase k├Ânnen vorgeschrieben werden.

Mit dem Aufruf des Default-Konstruktors wird also ein Objekt im Speicher vorbereitet, das man ├╝ber seinen Namen ansprechen kann und das sp├Ąter alle Eigenschaften der Klasse tragen kann, es muss aber erst durch weitere Befehle konfiguriert werden.

Der Zuweisungs-Operator und der Kopier-Konstruktor

Zur Wiederholung: Eigenschaften von fundamentalen Datentypen

Werden zwei Variablen a und b eines fundamentalen Datentyps angelegt, eine initialisiert und der anderen zugewiesen, so sollte klar sein, was passiert. Ebenso wenn eine Referenz auf eine Variable erzeugt wird, etwa wie im folgenden Beispiel:

int a {1};
int b {a};
int & c {a};            // Referenz:    int & c {1};    w├Ąre ein Compiler-Fehler    

cout << "Werte: " << endl;
cout << b << endl;          // 1
cout << c << endl;          // 1

cout << "Gleichheit: " << endl;
cout << (a == b) << endl;       // true
cout << (a == c) << endl;       // true

a = 2;

cout << "Werte: " << endl;
cout << b << endl;          // 1
cout << c << endl;          // 2


cout << "Gleichheit: " << endl;
cout << (a == b) << endl;           // false
cout << (a == c) << endl;           // true

c = 1;

cout << "a = " << a << endl;        // a = 1
cout << "c = " << c << endl;        // c = 1
cout << (a == c) << endl;           // true

Zur Erkl├Ąrung:

  1. Da b eine Variable ist und keine Referenz auf eine int-Variable, hat die Ver├Ąnderung von a keinen Einfluss auf b (siehe Zeile 6, 13 und 16).
  2. Werden zwei Variablen a und b auf Gleichheit gepr├╝ft, werden die Inhalte des Speichers verglichen (siehe Zeile 10 und 21).
  3. Die Variable c ist als Referenz auf die Variable a definiert (siehe Zeile 3). Man beachte, dass c (als Referenz) mit einer Variable initialisiert werden muss; die Initialisierung mit einem Zahlenwert erzeugt einen Compiler-Fehler.
  4. Die Variable c stimmt zun├Ąchst mit a ├╝berein (Zeile 7). Wird a ver├Ąndert (Zeile 13), ├Ąndert auch c seinen Wert (Zeile 17).
  5. Gleichheit zwischen a und c besteht immer (Zeile 11 und 22).
  6. Die Beziehung zwischen a und c ist symmetrisch: ├ändert man c, wird auch a ver├Ąndert (Zeile 24 bis 28).

Der Zuweisungs-Operator bei selbstdefinierten Datentypen

Das zuletzt untersuchte Verhalten von fundamentalen Datentypen sollte klar sein. Aber wie verhalten sich selbstdefinierte Datentypen? Wie Variablen von fundamentalen Datentypen oder wie Referenzen? Was passiert bei einer Zuweisung mit Objekten eines selbstdefinierten Datentyps?

Dass Antworten auf diese Fragen nicht so selbstverst├Ąndlich sind, zeigt folgendes Beispiel:

Beispiel: Kopieren eines Ordners

Die Ordner eines Dateisystems sind baumartig angeordnet:

  • Jeder Ordner kann Dateien oder Unterordner enthalten.
  • Ein Unterordner hat wieder die Eigenschaften eines Ordners.

Wird nun die Kopie eines Ordners erzeugt, muss man zwischen einer flachen und einer tiefen Kopie unterscheiden:

  • Bei der flachen Kopie wird nur der Ordner kopiert, nicht aber seine Inhalte.
  • Bei der tiefen Kopie werden auch alle Inhalte kopiert.

Und warum soll der Nutzer nicht die M├Âglichkeit haben, selber zu definieren, was bei einer Kopie ├╝bertragen wird, zum Beispiel alle Unterordner bis zu einer gew├╝nschten Ordnung.

Um zu kl├Ąren, wie sich selbstdefinierte Datentypen bei Zuweisungen verhalten, wird obiges Beispiel mit int-Variablen auf selbstdefinierte Datentypen ├╝bertragen. Im Statistik-Projekt wurde zum Beispiel die Klasse RandomGenerator definiert, die als Datenelemente min und max besitzt. Was passiert jetzt, wenn man ein Objekt dieser Klasse erzeugt, die beiden Datenelemente setzt und dann dieses Objekt einem anderen zuweist, wie in:

RandomGenerator rg(1, 6);
RandomGenerator dice = rg;

Sind damit auch die Datenelemente min und max f├╝r das Objekt dice gesetzt? Ist dice tats├Ąchlich ein neues Objekt oder eine Referenz auf rg? Was passiert also bei einer Ver├Ąnderung der Datenelemente in rg mit denen in dice?

RandomGenerator rg(1, 6);
RandomGenerator dice = rg;

cout << rg.getMax() << endl;              // 6
cout << dice.getMax() << endl;            // 6

rg.setMax(12);

cout << rg.getMax() << endl;              // 12
cout << dice.getMax() << endl;            // 6

Man sieht an diesem Beispiel:

  1. Durch die Zuweisung in Zeile 2 wird tats├Ąchlich eine tiefe Kopie des Objektes rg erzeugt; denn sonst h├Ątte dice.getMax() einen unbestimmten Wert (Zeile 5).
  2. Die Ver├Ąnderung, die in Zeile 7 am Objekt rg vorgenommen wird, hat keine Auswirkung auf das Objekt dice; die Zuweisung hat also ein neues Objekt dice als Kopie des alten Objektes rg erzeugt; es wurde nicht nur eine Referenz auf rg erzeugt.

Die Erkl├Ąrung f├╝r dieses Verhalten erfordert etwas Hintergrundwissen: In C++ wird bei der Zuweisung (wie in Zeile 2) f├╝r selbstdefinierte Datentypen der sogenannte Kopier-Konstruktor aufgerufen; dieser erzeugt eine Kopie des Objektes auf der rechten Seite der Zuweisung nach folgenden Regeln:

  1. Es wird ein neues Objekt erzeugt.
  2. Die Datenelemente des urspr├╝nglichen Objektes werden kopiert, genauer:
    • haben die Datenelemente fundamentale Datentypen, werden sie kopiert (wie oben bei den fundamentalen Datentypen diskutiert),
    • haben sie selbstdefinierte Datentypen, wird wiederum deren Kopier-Konstruktor aufgerufen (Rekursion!).

Man kann dies leicht ├╝berpr├╝fen, indem man Zeile 2 oben ersetzt: anstelle der Zuweisung mit dem Zuweisungs-Operator wird der Kopier-Konstruktor aufgerufen. Alle anderen Anweisungen bleiben gleich:

RandomGenerator rg(1, 6);
RandomGenerator dice(rg);

cout << rg.getMax() << endl;              // 6
cout << dice.getMax() << endl;            // 6

rg.setMax(12);

cout << rg.getMax() << endl;              // 12
cout << dice.getMax() << endl;            // 6

Der Konstruktor mit einem Argument vom Datentyp RandomGenerator ist in der Klasse RandomGenerator nicht definiert; es ist der Kopier-Konstruktor, der automatisch erzeugt wird und der wahlweise mit dem Zuweisungs-Operator oder explizit als Konstruktor aufgerufen werden kann.

├ťberschreiben des Kopier-Konstruktors

Soll der Kopier-Konstruktor andere Aufgaben erf├╝llen als der automatisch erzeugte Kopier-Konstruktor, reicht es den geeigneten Kopier-Konstruktor selber zu definieren, man sagt der Kopier-Konstruktor wird ├╝berschrieben. Er muss dann nur die richtige Signatur besitzen, um in der Anweisung

RandomGenerator dice(rg);

aufgerufen zu werden.

Im vorliegenden Beispiel bietet sich folgender Kopier-Konstruktor an (zuerst die Deklaration, dann die Implementierung):

// Deklaration des Kopier-Konstruktors in RandomGenerator.h
RandomGenerator(const RandomGenerator & rgToCopy);
// Implementierung des Kopier-Konstruktors in RandomGenerator.cpp
RandomGenerator::RandomGenerator(const RandomGenerator & rgToCopy)
{
    cout << "Kopier-Konstruktor" << endl;
    min = rgToCopy.min;
    max = rgToCopy.max;
}

Zur Erkl├Ąrung:

  1. Generell muss der Eingabewert vom Datentyp RandomGenerator sein. Da dieses Objekt aber sehr gro├čen Speicherplatz beanspruchen kann, ist es g├╝nstiger eine Referenz auf das zu kopierende RandomGenerator-Objekt zu ├╝bergeben.
  2. Und da dieses Objekt nicht ver├Ąndert werden soll, wird es als const gekennzeichnet.
  3. In der Implementierung werden dann die beiden Datenelemente min und max von RandomGenerator gesetzt (siehe Zeile 5 und 6 der Implementierung).
  4. Um zu demonstrieren, dass tats├Ąchlich der selbstdefinierte (und nicht der automatisch erzeugte) Kopier-Konstruktor aufgerufen wird, wird die Konsolen-Ausgabe (Zeile 4) eingef├╝gt.

Der Test (also aufruf des Kopier-Konstruktors wie oben oder der Zuweisung RandomGenerator dice = rg; ) zeigt, dass tats├Ąchlich der selbstdefinierte Kopier-Konstruktor aufgerufen wird und dass das erzeugte Objekt identische Eigenschaften hat wie beim automatisch erzeugten Kopier-Konstruktor.

Damit hat man die M├Âglichkeit, selbst zu bestimmen, welche Datenelemente beim Kopieren eines Objektes ├╝bernommen und welche nicht gesetzt werden (tiefe oder flache Kopie).

Der Kopier-Konstruktor wird nicht nur bei Zuweisungen aufgerufen, er wird auch verwendet, wenn ein Funktions-Aufruf mit call by value realisiert wird.

Aufgabe:

Testen Sie, ob der Kopier-Konstruktor tats├Ąchlich bei einem Funktions-Aufruf mit call by value aufgerufen wird.

Und dass er bei call by reference nicht aufgerufen wird.

Der Destruktor

Der Destruktor ist das Gegenst├╝ck zum Konstruktor. Und so wie der Konstruktor nicht das Objekt erzeugt, sondern aufgerufen wird, nachdem das Objekt erzeugt wurde, ist auch der Destruktor nicht f├╝r die Zerst├Ârung eines Objektes zust├Ąndig. Vielmehr wird der Destruktor aufgerufen, wenn der G├╝ltigkeitsbereich eines Objektes verlassen wird. Der Compiler sorgt daf├╝r, dass der Aufruf des Destruktors zum richtigen Zeitpunkt geschieht ÔÇö n├Ąmlich unmittelbar bevor der Speicherplatz des Objektes freigegeben wird. Letzteres wird wieder von Compiler erledigt und kann nicht beeinflusst werden.

Deklaration und Implementierung (hier ohne Befehle) des Destruktors f├╝r die Klasse RandomGenerator sehen wie folgt aus:

// Deklaration in RandomGenerator.h
::~RandomGenerator();

// Implementierung in RandomGenerator.cpp
RandomGenerator::~RandomGenerator()
{
}

Das Symbol ~ soll an das bitweise Komplement erinnern, da der Destruktor das Komplement zum Konstruktor ist.

Ein expliziter Aufruf des Destruktors ist m├Âglich, f├╝hrt aber nicht dazu, dass das Objekt zerst├Ârt wird, es werden einfach die Befehle des Destruktors ausgef├╝hrt:

rg.~RandomGenerator();       // Aufruf des Destruktors

Es ist gar nicht so leicht, sich ein Szenario vorzustellen, in dem man den Destruktor mit sinnvollen Aktionen ausstatten kann ÔÇö wenn das Objekt sowieso zerst├Ârt wird, warum soll man dann noch etwas damit tun?

Bei den bisher behandelten Programmier-Techniken ben├Âtigt man den Destruktor nicht. Allerdings hat man in C++ auch die M├Âglichkeit, selber ├╝ber den Speicherplatz zu verf├╝gen ÔÇö und dann ist der Einsatz von Destruktoren geboten.

Was ist damit gemeint? Die Klasse std::vector ist ein Beispiel f├╝r eine Klasse, die in der Lage ist, den Speicherplatz dynamisch (also w├Ąhrend der Programm-Ausf├╝hrung) zu verwalten: werden an ein vector-Objekt Elemente angeh├Ąngt oder beseitigt, wird der Speicher automatisch angepasst. Man kann sich solche Klassen mit dynamischer Speicherplatz-Verwaltung auch selber programmieren. Umgekehrt ist man dann aber auch selber daf├╝r verantwortlich, den Speicherplatz wieder freizugeben, falls ein Objekt zerst├Ârt wird. Macht man dies nicht, kann leicht ein Speicher-Leck (memory leak) entstehen.