Kopieren eines Objekts
Content |
Es gibt zwei Wege, wie ein Objekt kopiert werden kann, die vom Zielobjekt abhängen:
-
Wenn das Zielobjekt noch nicht existiert, kann es kopier-konstruiert werden.
-
Wenn hingegen das Zielobjekt bereits konstruiert ist, kann es mit der Kopie komponentenweise überschrieben werden.
Beide Varianten werden durch den Übersetzer für jede Klasse zur Verfügung gestellt, wenn dem keine Hindernisse in Wege stehen. Zu den Hindernissen gehören
-
das explizite Löschen der entsprechenden Methode,
-
Komponenten, die die entsprechende Methode nicht unterstützen, oder
-
die eigene Definition der entsprechenden Methode.
Wir werfen hintereinander einen Blick auf beide Varianten:
Kopierkonstruktor
Ein Kopierkonstruktor erhält eine const-Referenz auf ein anderes Objekt des gleichen Typs. Bei allen elementaren Datentypen und Zeigern ist die Operation vordefiniert, wenn dem kein Hindernis entgegensteht.
Hier ist ein Beispiel eines expliziten Kopierkonstruktors, der genau das tut, was auch der vom Übersetzer automatisch erzeugte Kopierkonstruktor getan hätte:
class Vector2D { public: Vector2D(const Vector2D& other) : x(other.x), y(other.y) { } double x, y; };
Das explizite Hinzufügen des Kopierkonstruktors hat jetzt aber eine möglicherweise unerwartete Nebenwirkung. Wissen Sie welche?
Wenn wir möchten, können wir den Kopierkonstruktor explizit verbieten:
class Vector2D { public: Vector2D() : x(0), y(0) { } Vector2D(const Vector2D& other) = delete; double x, y; }; int main() { Vector2D a; Vector2D b = a; }
theon$ g++ -Wall -c deleted-copy-constructor.cpp deleted-copy-constructor.cpp: In function 'int main()': deleted-copy-constructor.cpp:11:17: error: use of deleted function 'Vector2D::Vector2D(const Vector2D&)' 11 | Vector2D b = a; | ^ deleted-copy-constructor.cpp:5:7: note: declared here 5 | Vector2D(const Vector2D& other) = delete; | ^~~~~~~~ theon$
Der Übersetzer verwendet den Kopierkonstruktor gerne implizit beim Anlegen eines Objekts und bei der Parameterübergabe:
#include <iostream> class Vector2D { public: Vector2D() : x(0), y(0) { std::cout << "X: default constructor" << std::endl; } Vector2D(const Vector2D& other) : x(other.x), y(other.y) { std::cout << "X: copy constructor" << std::endl; } double x, y; }; void foo(Vector2D v) { } int main() { Vector2D a; Vector2D b = a; // equivalent to: Vector2D b{a} foo(b); }
theon$ g++ -Wall -o copy-constructor copy-constructor.cpp theon$ ./copy-constructor X: default constructor X: copy constructor X: copy constructor theon$
Zuweisung
Ein expliziter Zuweisungsoperator für Vector2D, der ansonsten implizit durch den Übersetzer erzeugt worden wäre, sieht so aus:
class Vector2D { public: Vector2D& operator=(const Vector2D& other) { x = other.x; y = other.y; return *this; } double x, y; };
Überladene Operatoren sind ganz normale Funktionen oder Methoden in C++, die den Namen operator gefolgt von dem jeweiligen Operator haben. Wie beim Kopierkonstruktor wird das Objekt, von dem kopiert werden soll, normalerweise als const-Referenz erwartet. Die vom Übersetzer erzeugte Fassung kopiert dann komponentenweise.
Im Vergleich zum Kopierkonstruktor ist hier noch der Rückgabetyp interessant. Das Schlüsselwort this liefert einen Zeiger zum eigenen Objekt, das ist hier vom Datentyp Vector2D*. (Idealerweise würde this eine Referenz liefern, aber historisch gesehen kam in C++ zuerst das Schlüsselwort this, bevor Referenzen eingeführt worden sind.) Wenn ein Zeiger dereferenziert wird, haben wir noch eine sogenannte lvalue, d.h. ein Wert, der links von einer Zuweisung stehen kann. Im Typsystem von C++ sind alle lvalues Referenzen, d.h. *this hat den Datentyp Vector2D&. Wenn der Rückgabetyp diesen Wert hat, können wir dementsprechend auch den Funktionsaufruf als lvalue behandeln, d.h. der Aufruf darf auf der linken Seite einer Zuweisung stehen. Das erlaubt beim rechts-assoziativen Zuweisungsoperator eine Kaskadierung der Zuweisungsoperatoren, die von C++ auch für die elementaren Datentypen unterstützt wird:
#include <iostream> class Vector2D { public: Vector2D() : x(0), y(0) { std::cout << "X: default constructor" << std::endl; } Vector2D(const Vector2D& other) : x(other.x), y(other.y) { std::cout << "X: copy constructor" << std::endl; } Vector2D& operator=(const Vector2D& other) { std::cout << "X: assignment operator" << std::endl; x = other.x; y = other.y; return *this; } double x, y; }; int main() { Vector2D a, b, c; c = b = a; }
theon$ g++ -Wall -o assignment-operator assignment-operator.cpp theon$ ./assignment-operator X: default constructor X: default constructor X: default constructor X: assignment operator X: assignment operator theon$
Aufgaben
-
Im folgenden Beispiel wurde der Kopierkonstruktor explizit gelöscht. Gibt das einen Fehler? Wenn ja, wie kann dieser behoben werden, ohne die Klasse Vector2 zu verändern?
#include <iostream> class Vector2D { public: Vector2D() : x(0), y(0) { std::cout << "X: default constructor" << std::endl; } Vector2D(const Vector2D& other) = delete; Vector2D& operator=(const Vector2D& other) { std::cout << "X: assignment operator" << std::endl; x = other.x; y = other.y; return *this; } double x, y; }; int main() { Vector2D a; Vector2D b = a; }
-
Die Klasse X im folgenden Beispiel weder einen expliziten Kopierkonstruktor noch einen Zuweisungsoperator. Beides wird vom Übersetzer gestellt. Leider führt die Ausführung zu mehrfachen Freigaben der gleichen Speicherfläche, was nicht zulässig ist. Wie genau kommt es dazu? Und wie lässt sich das verhindern, indem die Klasse X geeignet ergänzt wird und main unverändert bleibt?
#include <iostream> class X { public: X() : data(new int{0}) { std::cout << "X: default constructor" << std::endl; } X(int i) : data(new int{i}) { std::cout << "X: constructor with i = " << i << std::endl; } ~X() { std::cout << "X: destructor with *data = " << *data << std::endl; delete data; } private: int* data; }; int main() { X x1{42}; X x2 = x1; X x3; x3 = x1; }
heim$ g++-8.3 -Wall -g -o data-on-heap data-on-heap.cpp /usr/local/libexec/gcc/x86_64-pc-linux-gnu/8.3.0/cc1plus: error while loading shared libraries: libmpfr.so.4: cannot open shared object file: No such file or directory heim$ ./data-on-heap sh: 1: ./data-on-heap: not found heim$ valgrind ./data-on-heap valgrind: ./data-on-heap: No such file or directory heim$
Die Standard-Bibliothek für die Speicherverwaltung unter Linux entdeckt gelegentlich Fehler wie die mehrfache Freigabe der gleichen Speicherfläche. valgrind untersucht solche Fehler systematisch und stellt auch Speicherlecks fest.