Shallow und Deep Copying
-
Huhu
Ich lese gerade das Kapitel "Classes and dynamic memory allocation".
Da lernt man, wie man mit Pointern umgeht, die innerhalb von Klassen erzeugt werden. (Soweit ich das verstanden habe, gehören die, wenns ums löschen geht, nicht zu einem Objekt).
Das Hauptproblem ist ja, dass C++ z.B. beim Zuweisen (assignment) nur ein shallow-copying macht, wodurch einfach nur die Adresse eines Pointers übergeben wird, der Pointer des neuen Objekts also nicht mit new erzeugt wird und somit eigenständig ist.
Wenn man dies nicht mit den entsprechenen Konstruktoren handhabt, gibt das grosse Probleme, wie im Buch hier sehr schön demonstriert wird.
Daher meine Frage:
Wiso macht C++ nicht immer ein deep copying? Wiso macht es ein shallow copying?PS:
Nein, ich will keine Diskussion starten, in der gestritten wird, ob C++ schlecht ist und was besser ist oder so ^^
Ich kann mir nur gerade nicht vorstellen, wiso dass es so geregelt ist und ich möchte das gerne verstehen.
-
icarus2 schrieb:
Daher meine Frage:
Wiso macht C++ nicht immer ein deep copying? Wiso macht es ein shallow copying?Weil es ziemlicher Mist wäre, wenn du tatsächlich bei ner Kopie deines Objekts den Pointer haben willst, und kein neues Objekt.
Für eigene Wünsche gibt es eben assignment operator (operator=) und Copy-Konstruktor.
-
Hehe, ich hatte mir noch gedacht, dass es damit zu tun haben könnte.
Aber wäre es theoretisch(soll nicht heissen, dass es sinnvoll ist) möglich gewesen, das ganze umzukehren? Sprich man macht immer eine deep-copy und wenn man das nicht will, muss man es selber anspassen?
-
Nö, wäre nicht möglich gewesen. Ein Pointer ist auch ein Typ. Sein Wert ist eine Adresse, und zwar die des Objektes auf das er zeigt. Beim Kopieren aller Member wird auch ein Pointer kopiert. Für einen Pointer bedeutet dies, dass die Adresse kopiert wird. Der Pointer selber ist dann ein neues Objekt!
Wenn du das Verhalten nicht wünschst, sondern beim Kopieren eines Objektes sämtliche Member kopiert bekommen willst, nimm einfach keinen Pointer als Member

-
Nur ein Programmierer kann wissen, ob ein Zeiger bzw. eine Referenz intern ein Deep-Copy erfordert.
Die Definition 'char *s' sagt dem Compiler nichts über den Inhalt bzw. die Speicherallokation...
-
Achso, jetzt ist alles klar wiso das nicht geht

Thx für die Antworten.
-
C++ macht gewissermassen immer ein deep copy. Es kopiert immer die Klasse, so wie sie ist. Ein int wird kopiert, ein double wird kopiert oder ein Zeiger wird kopiert. Damit hat die neue Klasse in seinem Zeigermember den selben Wert, wie die ursprüngliche Klasse, das heisst, sie Zeigt wie die ursprüngliche Klasse auf das selbe Objekt.
Ein Zeiger ist nun mal nicht das Objekt, auf den verwiesen wird, sondern der Zeiger ist eine Speicheradresse. Wenn es semantisch anders gemeint ist, also das verzeigerte Objekt eigentlich der Klasse gehören soll, dann muss man das halt so programmieren. Im grunde verhält sich C++ hier wie viele andere Sprachen.
In C++ gibt es halt einen Unterschied zwischen Zeiger und Objekt. In anderen Programmiersprachen wird das nicht so unterschieden. Ich sehe es als Feature. Aber ich bin halt sicher durch die vielen Jahre C++-Erfahrung auch ein wenig voreingenommen
.
-
Ja, es ist ein Feature, das man wahrscheinlich in einigen Situationen sehr gut gebrauchen kann.
Zum lernen ist es am Anfang etwas kompliziert und verwirrend. Vor allem kann man sehr leicht Fehler machen wenn man sich nicht wirklich gut auskennt ^^
-
icarus2 schrieb:
Vor allem kann man sehr leicht Fehler machen wenn man sich nicht wirklich gut auskennt ^^
Das ist sowieso eine Eigenschaft von C++. Dafür hast du enorm mächtige Möglichkeiten, wenn du dich auskennst.

icarus2 schrieb:
Aber wäre es theoretisch(soll nicht heissen, dass es sinnvoll ist) möglich gewesen, das ganze umzukehren? Sprich man macht immer eine deep-copy und wenn man das nicht will, muss man es selber anspassen?
Technisch wäre es vielleicht möglich gewesen, jeden Zeiger tief zu kopieren. Aber so eine Regelung wäre jenseits von Gut und Böse. Wie will man da kompatibel zu PODs aus C bleiben? In sehr vielen Fällen sind Zeiger nur Verweise ohne Besitz. Was sollte passieren, wenn das Objekt, auf das verwiesen wird, nicht kopierbar ist?
Andererseits stimmt es auch nicht, dass du jedes Mal die Grossen Drei selbst implementieren musst, wenn du Zeiger tief kopieren willst. Für solche Anwendungsfälle gibt es Smart-Pointer mit den unterschiedlichsten Kopiersemantiken.
Und ja, ich kann mir immer noch nicht erklären, wieso weder die Standardbibliothek noch Boost einen kopierenden Smart-Pointer anbieten. Als wäre der sowieso überbewertete
shared_ptrder Weisheit letzter Schluss.
(Wenigstens Loki machts richtig.)
-
Jo, wenn man sich auskennt (ist bei mir irgendwie nicht so der Fall ^^ sollte aber irgendwann noch kommen).
Smart pointers kenne ich noch nicht, aber vielleicht wird das hier im Buch ja noch behandelt.
Thx
-
Nexus schrieb:
Und ja, ich kann mir immer noch nicht erklären, wieso weder die Standardbibliothek noch Boost einen kopierenden Smart-Pointer anbieten. Als wäre der sowieso überbewertete
shared_ptrder Weisheit letzter Schluss.
(Wenigstens Loki machts richtig.)Wozu sollte der gut sein? Wenn ich eine Klasse in alleinigen Besitz haben möchte, brauche ich keinen Zeiger mehr - auch keinen smarten. Ich bevorzuge einfach auf Zeiger soweit wie möglich zu verzichten. Smart-Pointer sind nur die zweitbeste Alternative.
Die Frage ist übrigens nicht rhetorisch gedacht, sondern mich würden wirklich Anwendungsfälle interessieren, wo Du so einen kopierenden Smart-Pointer gebrauchen könntest.
Auch würde mich interessieren, wie so einer aussehen könnte. Ein Zeiger - auch ein smarter Zeiger - kann auf eine abgeleitete Klasse zeigen und die vollständig zu kopieren ist meiner Meinung nach nicht allgemeingültig lösbar.
-
tntnet schrieb:
Wozu sollte der gut sein? Wenn ich eine Klasse in alleinigen Besitz haben möchte, brauche ich keinen Zeiger mehr - auch keinen smarten. Ich bevorzuge einfach auf Zeiger soweit wie möglich zu verzichten. Smart-Pointer sind nur die zweitbeste Alternative.
Er hat zwei nicht zu unterschätzende Vorteile.
- Gegenüber Zeigern: Der kopierende Smart-Pointer besitzt Value-Semantik, wie man sie von normalen Objekten her kennt (Kopierbarkeit, Zuweisbarkeit und solche Sachen). Kein geteilter Besitz oder so ein Spezialfall. Implementierung der Big Three ist nicht nötig.
- Gegenüber "normalen" Value-Objekten: Der gehaltene Typ muss bei der Deklaration nicht bekannt sein. Wahnsinnig praktisch, wenns ums Verringern von Abhängigkeiten geht.
- Natürlich gibt es auch Nachteile, wie die zusätzliche Heap-Allokation. Aber bei rohen Zeigern hat man dieses Problem auch.
tntnet schrieb:
Auch würde mich interessieren, wie so einer aussehen könnte. Ein Zeiger - auch ein smarter Zeiger - kann auf eine abgeleitete Klasse zeigen und die vollständig zu kopieren ist meiner Meinung nach nicht allgemeingültig lösbar.
Doch, sowas ist möglich – sogar ohne
Clone().
Der Trick dabei ist, dass der Konstruktor ein Funktionstemplate ist, das den statischen Typen des erstellten Objekts erkennt. Nur wenn dieser bekannt ist, kann eine Kopie vom selben Typ erstellt werden.
template <typename T> // Templateparameter für Klasse template <typename Derived> // Templateparameter für Funktion clone_ptr<T>::clone_ptr(Derived* new_pointer);Eine Initialisierung kann also folgendermassen aussehen:
// Aufruf mit folgenden Templateargumenten: // T == BaseClass // Derived == DerivedClass clone_ptr<BaseClass> ptr(new DerivedClass);Der Konstruktor initialisiert ein Helper-Objekt
helper<T,Derived>, das von einer polymorphen Basisklassehelper_base<T>abgeleitet ist. Die Basisklasse bietet abstrakte Funktionen fürs Kopieren an, die abgeleitete implementiert diese Funktionen für den konkreten TypDerived(das abgeleitete Helper-Objekt ist wiederum ein Template).template <typename T> template <typename Derived> clone_ptr<T>::clone_ptr(Derived* new_pointer) : m_pointer(new_pointer) // m_pointer ist Member vom Typ T* , m_helper(new helper<T,Derived>()) // m_helper ist Member vom Typ helper_base<T>* { }Die Funktionalität fürs polymorphe Kopieren ist also in
helper<T,Derived>ausgelagert und kann über eine virtuelle Funktion inhelper_base<T>erreicht werden. Die einzige Bedingung dafür ist, dass zur Initialisierungszeit der statische Typ bekannt ist, was bei einer Konstruktion mitnewnie ein Problem darstellt. Das selbe Prinzip kann für eine spätere Zuweisung überreset()angewandt werden.Die ganze Klasse beinhaltet natürlich ein wenig Overhead, aber angesichts der Tatsache, dass
Tkeine virtuelleClone()-Methode anbieten muss (welche auch gewisse Gefahren wie Vergessen der Überschreibung mit sich brächte), und den anderen Vorzügen, ist es mir das völlig Wert. Davon abgesehen gibt es auch einige Anwendungsfälle, wo gar keine Polymorphie im Spiel ist, und ein entsprechender Smart-Pointer käme im Bezug auf Overhead einer manuellen Lösung recht nahe (er müsste nur die Big Three sinnvoll implementieren, was trivial ist).
-
icarus2 schrieb:
Das Hauptproblem ist ja, dass C++ z.B. beim Zuweisen (assignment) nur ein shallow-copying macht, ...
Das kann ich so natürlich nicht stehen lassen. Per Definition gibt es hier Element-weises Kopieren und Zuweisen. Ein "Problem" stellt das nur dar, wenn Du Dich bei der Wahl der Elementtypen "ungeschickt" anstellst. Und manchmal geht es nicht ohne Zeiger. Aber das sind kleine Spezialfälle, mit denen man sich eigentlich so gut wie gar nicht rumschlagen muss, wenn man Standard-Container als Elemente benutzt.
icarus2 schrieb:
Daher meine Frage:
Wiso macht C++ nicht immer ein deep copying? Wiso macht es ein shallow copying?Was Du "shallow copying" nennst, ist die einzig sinnvolle und C-abwärtskompatible Semantik für Zeiger. Wenn Dir das nicht gefällt, nimm etwas anderes als Zeiger (das meinte ich mit "ungeschickter Wahl der Elementtypen" oben).
Gruß,
SPPS: Dü könntest auch mal eine Rechtschreibprüfung über Deine Texte laufen lassen. :p
-
Danke für die Antworten.
Haben meine Texte so viele Fehler drin?
-
Um das Ganze zu üben habe ich mir nun mal eine kleine Klasse Triangle gebastelt. Die Klasse selber ist nicht wirklich brauchbar, aber als Übung reicht es aus.
Ich habe folgendes header-file:
// Triangle1.h -- Triangle class header file #ifndef CIRCLE_1_INCLUDED #define CIRCLE_1_INCLUDED #include <iostream> namespace my_math { class Triangle { private: char * name; int name_length; double width; double height; public: // constructors and destructor Triangle(const char * name, const int name_length, const int width = 0, const int height = 0); Triangle(const Triangle &); //copy constructor ~Triangle(); // overloaded operator functions Triangle & operator=(const Triangle & t); //assignment operator overloading friend std::ostream & operator<<(std::ostream & os, const Triangle & t); // << operator // member functions double area(); // getters char * get_name(); double get_width(); double get_height(); }; } // end of namespace my_math #endifDazu das .cpp file, das die Triangle Methoden implementiert:
// Triangle1.cpp -- implementing Triangle methods #include "Triangle1.h" //includes <iostream> namespace my_math { // constructor Triangle::Triangle(const char * a_name, const int a_name_length, const int width, const int height) { std::cout << "Constructor" << std::endl; name = new char[a_name_length]; std::strcpy(name, a_name); this->name_length = a_name_length; this->width = width; this->height = height; } // copy constructor Triangle::Triangle(const Triangle & t) { std::cout << "Copy Constructor" << std::endl; name_length = t.name_length; name = new char[name_length + 1]; std::strcpy(name, t.name); } // destructor Triangle::~Triangle() { delete [] name; } // public class methods double Triangle::area() { return (width * height / 2); } //overloaded operator functions Triangle & Triangle::operator=(const Triangle & t) // assignment { std::cout << "Assignment function" << std::endl; if(this == &t) { return *this; } delete [] name; name_length = t.name_length; name = new char[name_length + 1]; std::strcpy(name, t.name); return *this; } std::ostream & operator<<(std::ostream & os, const Triangle & t) // << operator { os << "Name: " << t.name << ", Width: " << t.width << ", Height: " << t.height; return os; } // getter functions char * Triangle::get_name() { return name; } double Triangle::get_width() { return width; } double Triangle::get_height() { return height; } } // end of namespace my_mathMeiner Meinung nach müsste alles funktionieren.
Beim Testen gibt es jedoch Fehler. Hier ein kleines Beispiel:// TriangleTest.cpp -- used to test the Triangle class #include <iostream> #include "Triangle1.h" int main() { using std::cout; using std::endl; using my_math::Triangle; Triangle *t1 = new Triangle("One", 3, 100.0, 100.0); // works Triangle t2 = Triangle("Two", 2, 20.2, 40.2); // causes HEAP CORRUPTION DETECTED error delete t1; // causes HEAP CORRUPTION DETECTED error return 0; }Kann mir jemand sagen, was ich in der Triangle-Klasse ändern muss, so dass der Code funktioniert? Aus meiner Sicht müsste alles korrekt ablaufen. Aber das tut es leider nicht.
PS:
Ich weiss, dass das char * nicht wirklich toll ist und ich wohl besser einfach string verwenden würde. Ich würde es normalerweise auch anders machen. Aber ich brauchte ja einen Pointer, der mit new erzeugt wird, in der Übung.
-
Der String "Two" hat 3 Zeichen. Somit überschreitet Dein strcpy den allokierten Puffer, da Du als Länge nur 2 übergibst.
-
Benutze doch strlen(..) um die Länge zu ermitteln.
Simon
-
Thx. Das hatte ich ganz übersehen

Aber auch mit dem hier:
Triangle t2 = Triangle("Two", 3, 20.2, 40.2); // causes HEAP CORRUPTION DETECTED errorfunktioniert es nicht.
-
tntnet schrieb:
Der String "Two" hat 3 Zeichen.
Irrtum, es sind vier. Und hast du meinen letzten Beitrag schon gelesen?

icarus2 schrieb:
Thx. Aber auch mit dem hier [..] funktioniert es nicht.
Wie sieht denn dein Konstruktor aus? Immer noch gleich? Schau, dass du die Nullterminierung bei der Speicheranforderung berücksichtigst. Aber warum überhaupt das ganze Rumgefrickel mit C-Strings? Kennst du
std::string?
-
Nexus, du bist der Beste

Ich hatte vergessen die Nullterminierung zu berücksichtigen. Jetzt klappt auch alles.
Ich kenne std::string. Allerdings habe ich das hier absichtlich mit char * gemacht, da ich einen pointer in der Klasse haben wollte, um das ganze mal ein bisschen zu üben. Steht irgendwo in meinem Post oben. Ich benutze sonst schon std::string.
Dangööö

*Edit
Ja, der Konstruktor ist immer noch gleich.