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&)' Vector2D b = a; ^ deleted-copy-constructor.cpp:5:7: note: declared here 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++-7.2 -Wall -o data-on-heap data-on-heap.cpp heim$ ./data-on-heap X: constructor with i = 42 X: default constructor X: destructor with *data = 42 X: destructor with *data = 0 *** Error in `./data-on-heap': double free or corruption (fasttop): 0x0000000000961c20 *** Aborted heim$ valgrind ./data-on-heap ==14895== Memcheck, a memory error detector ==14895== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al. ==14895== Using Valgrind-3.10.0 and LibVEX; rerun with -h for copyright info ==14895== Command: ./data-on-heap ==14895== X: constructor with i = 42 X: default constructor X: destructor with *data = 42 ==14895== Invalid read of size 4 ==14895== at 0x400B93: X::~X() (in /home/numerik/cpp/ss18/sessions/session02/data-on-heap) ==14895== by 0x400A44: main (in /home/numerik/cpp/ss18/sessions/session02/data-on-heap) ==14895== Address 0x5a89c80 is 0 bytes inside a block of size 4 free'd ==14895== at 0x4C2A360: operator delete(void*) (vg_replace_malloc.c:507) X: destructor with *data = 42 X: destructor with *data = 42 ==14895== by 0x400BBF: X::~X() (in /home/numerik/cpp/ss18/sessions/session02/data-on-heap) ==14895== by 0x400A38: main (in /home/numerik/cpp/ss18/sessions/session02/data-on-heap) ==14895== ==14895== Invalid free() / delete / delete[] / realloc() ==14895== at 0x4C2A360: operator delete(void*) (vg_replace_malloc.c:507) ==14895== by 0x400BBF: X::~X() (in /home/numerik/cpp/ss18/sessions/session02/data-on-heap) ==14895== by 0x400A44: main (in /home/numerik/cpp/ss18/sessions/session02/data-on-heap) ==14895== Address 0x5a89c80 is 0 bytes inside a block of size 4 free'd ==14895== at 0x4C2A360: operator delete(void*) (vg_replace_malloc.c:507) ==14895== by 0x400BBF: X::~X() (in /home/numerik/cpp/ss18/sessions/session02/data-on-heap) ==14895== by 0x400A38: main (in /home/numerik/cpp/ss18/sessions/session02/data-on-heap) ==14895== ==14895== Invalid read of size 4 ==14895== at 0x400B93: X::~X() (in /home/numerik/cpp/ss18/sessions/session02/data-on-heap) ==14895== by 0x400A50: main (in /home/numerik/cpp/ss18/sessions/session02/data-on-heap) ==14895== Address 0x5a89c80 is 0 bytes inside a block of size 4 free'd ==14895== at 0x4C2A360: operator delete(void*) (vg_replace_malloc.c:507) ==14895== by 0x400BBF: X::~X() (in /home/numerik/cpp/ss18/sessions/session02/data-on-heap) ==14895== by 0x400A38: main (in /home/numerik/cpp/ss18/sessions/session02/data-on-heap) ==14895== ==14895== Invalid free() / delete / delete[] / realloc() ==14895== at 0x4C2A360: operator delete(void*) (vg_replace_malloc.c:507) ==14895== by 0x400BBF: X::~X() (in /home/numerik/cpp/ss18/sessions/session02/data-on-heap) ==14895== by 0x400A50: main (in /home/numerik/cpp/ss18/sessions/session02/data-on-heap) ==14895== Address 0x5a89c80 is 0 bytes inside a block of size 4 free'd ==14895== at 0x4C2A360: operator delete(void*) (vg_replace_malloc.c:507) ==14895== by 0x400BBF: X::~X() (in /home/numerik/cpp/ss18/sessions/session02/data-on-heap) ==14895== by 0x400A38: main (in /home/numerik/cpp/ss18/sessions/session02/data-on-heap) ==14895== ==14895== ==14895== HEAP SUMMARY: ==14895== in use at exit: 72,708 bytes in 2 blocks ==14895== total heap usage: 3 allocs, 3 frees, 72,712 bytes allocated ==14895== ==14895== LEAK SUMMARY: ==14895== definitely lost: 4 bytes in 1 blocks ==14895== indirectly lost: 0 bytes in 0 blocks ==14895== possibly lost: 0 bytes in 0 blocks ==14895== still reachable: 72,704 bytes in 1 blocks ==14895== suppressed: 0 bytes in 0 blocks ==14895== Rerun with --leak-check=full to see details of leaked memory ==14895== ==14895== For counts of detected and suppressed errors, rerun with: -v ==14895== ERROR SUMMARY: 4 errors from 4 contexts (suppressed: 0 from 0) 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. (Momentan steht valgrind noch nicht auf der Theon zur Verfügung, sondern nur auf den Linux-Maschinen in E.44.)