Kopierkonstruktor

Willemers Informatik-Ecke

Wenn ein Objekt kopiert wird, wird der Speicher, den das Objekt einnimmt, Bit für Bit an die Zielstelle kopiert. In manchen Fällen führt das aber nicht zum gewünschten Ergebnis. Ein solcher Fall tritt immer dann ein, wenn die Klasse Zeiger enthält, die auf Daten außerhalb des Objektspeichers zeigen. Bei der Kopie würde der Zeiger mitkopiert. Aber die Daten, auf die der Zeiger verweist, werden nicht kopiert. Stattdessen weisen anschließend die Zeiger von Original und Kopie auf dieselbe Speicherstelle, da der Zeiger ja mitkopiert wurde. Das bedeutet, dass die Kopie des Objekts keine wirkliche Kopie ist, sondern über den Zeiger mit den Daten des Originals arbeitet. Abbildung (abbobjkopie) veranschaulicht diese Situation.

An dieser Stelle steht im Buch die Abbildung "Kopieren von Objekten" (abbobjkopie)

Destruktorschäden

Noch schwieriger wird die Situation, wenn die Kopie wieder zerstört wird. Bei Parametervariablen wird das am Ende der Funktion ganz sicher eintreten, da eine Parametervariable ja den Charakter einer lokalen Variablen hat. Dann wird der Destruktor der Klasse, sofern er nicht sehr schlampig geschrieben ist, sicher auch den externen Speicher freigeben, auf den der Zeiger verweist. Da der Zeiger aber auf den externen Speicher des Originals verweist, wird dieser auch freigegeben. Das heißt, dass jedes Objekt, das als Parameter an eine Funktion übergeben wird, seinen externen Speicher am Ende der Funktion verliert.

Wann muss ein Kopierkonstruktor erstellt werden?
Wenn die Klasse Verweise auf fremden Speicher hat, muss ein Kopierkonstruktor erstellt werden. Solche Verweise sind Zeiger und Referenzen. Aber auch fremde Klassen können solch einen Verweis enthalten, wie beispielsweise Strings.

Selbst ist die Klasse

Die Kopie externer Datenspeicher kann durch keinen Automatismus erledigt werden, sondern hier ist der Programmierer gefragt. Wenn Sie eine Klasse entwerfen, die Zeiger auf externe Daten enthält, können Sie einen Kopierkonstruktor (Copy-Konstruktor) schreiben, der immer dann aufgerufen wird, wenn ein Objekt der Klasse kopiert wird.

Aufruf

Der Kopierkonstruktor wird bei internen Kopieraktionen aufgerufen. Das sind Initialisierungen, Parameterübergaben und Funktionswertrückgaben als Wert. Bei Parameterübergaben per Zeiger oder Referenz wird er nicht aufgerufen, da ja das Objekt in solch einem Fall auch nicht kopiert wird. Die Zuweisung ist explizit ausgeschlossen, weil dafür explizit der Zuweisungsoperator überlagert wird. Wie man Operatoren selbst definiert, wird an anderer Stelle erklärt.

Funktion des Kopierkonstruktors

Der Kopierkonstruktor sollte zunächst die Datenelemente kopieren und anschließend für alle Zeigervariablen, die nicht 0 sind, neuen Speicher anfordern und den externen Speicher des Originals in den neuen Speicher kopieren. Die Hauptaufgabe des Kopierkonstruktors ist es also, auch die Daten zu kopieren, auf die Zeiger verweisen. Immer dann, wenn eine Klasse einen Zeiger auf externe Daten enthält, können Sie darauf wetten, dass es erforderlich ist, einen Kopierkonstruktor zu erstellen.

Syntax

Wie der Name schon sagt, ist ein Kopierkonstruktor ein Konstruktor, hat also keinen Rückgabewert und trägt den Klassennamen. Der Kopierkonstruktor hat als einzigen Parameter eine Referenz auf die eigene Klasse. Der Parameter muss eine Referenz sein, damit der Kopierkonstruktor sich nicht selbst aufruft.

Wenn Sie als Datenanteil in der Klasse tStack statt eines Integers einen Zeiger verwenden, dann würden Sie in der Klasse tStack einen Kopierkonstruktor benötigen. Um zu zeigen, wie dieser arbeiten müsste, erfinden wir hier eine sehr einfache Klasse mit einem externen Speicher.

[Externer Speicher und Kopierkonstruktor (copycons.cpp)]

// Programm zur Demonstration eines Kopierkonstruktors
#include <iostream>
using namespace std;

class tKlasse
{
public:
    tKlasse() // Konstruktor: erzeugt externe Daten
    {
        Zeiger = new int;
        *Zeiger = 5;
    }
    ~tKlasse() // Destruktor: gibt externe Daten frei
    {
        delete Zeiger;
        Zeiger = 0;
    }
    void SetData(int a) { *Zeiger = a; }
    int GetData() { return *Zeiger; }

    tKlasse(const tKlasse& k) // Kopierkonstruktor
    {
        // zur Demonstration meldet er sich
        cout << "Kopierkonstruktor" << endl;
        // Externe Daten erzeugen und kopieren
        Zeiger = new int;
        *Zeiger = k.GetData();
        // Normale Datenelemente auch kopieren
        sonstiges = k.sonstiges;
    }

    int sonstiges; // steht für die Nicht-Zeiger Datenelemente
private:
    int *Zeiger;   // Zeiger, also Kopierkonstruktor notwendig
};

// Die Funktion dient nur zur Demonstration. Weil der Parameter
// per Wert übergeben wird, wird beim Aufruf der
// Kopierkonstruktor aufgerufen
void Funktion(tKlasse para)
{
    cout << "Funktion:" << para.GetData() << endl;
}

// Hauptprogramm zum Testen
int main()
{
    tKlasse Objekt;
    Objekt.SetData(7);
    Funktion(Objekt); // Hier wird der Kopierkonstruktor aktiv
    cout << Objekt.GetData() << endl;
}

Der Parameter des Kopierkonstruktors hat das Attribut const, da der Parameter kopiert und nicht etwa ver&uaml;ndert werden soll. Verändert wird das Objekt selbst, also das, auf das this zeigt.

Um zu zeigen, wo der Kopierkonstruktor eingesetzt wird, gibt er eine kurze Meldung auf dem Bildschirm aus. Wenn Sie den Kopierkonstruktor auskommentieren, wird nach dem Aufruf der Funktion der Datenbereich, auf den der Zeiger des Objektes zeigt, freigegeben sein. Wenn Sie dies nachprüfen wollen, ändern Sie den Destruktor so, dass er den externen Integerwert auf 9 setzt, anstatt ihn freizugeben.

~tKlasse() // Destruktor: hier zum Test missbraucht
{
    *Zeiger = 9; // nur zum Test
}

Bei auskommentiertem Kopierkonstruktor meldet die letzte Zeile im Programm eine 9, als deutliches Zeichen, dass der Destruktor auf dem Bereich gearbeitet hat, auf den der Zeiger verweist. Wenn Sie den Kopierkonstruktor wieder aktivieren, erscheint wieder 7.


Informatik-Ecke Einstieg in C++ (C) Copyright 2008 Arnold Willemer