- Was bedeutet das Kopieren eines Objekts ?
- Was sind der Kopierkonstruktor und der Kopierzuweisungsoperator ?
- Wann muss ich sie selbst deklarieren?
- Wie kann ich verhindern, dass meine Objekte kopiert werden?
Antworten:
C ++ behandelt Variablen benutzerdefinierter Typen mit Wertsemantik . Dies bedeutet, dass Objekte implizit in verschiedenen Kontexten kopiert werden, und wir sollten verstehen, was "Kopieren eines Objekts" tatsächlich bedeutet.
Betrachten wir ein einfaches Beispiel:
class person
{
std::string name;
int age;
public:
person(const std::string& name, int age) : name(name), age(age)
{
}
};
int main()
{
person a("Bjarne Stroustrup", 60);
person b(a); // What happens here?
b = a; // And here?
}
(Wenn Sie von dem name(name), age(age)
Teil verwirrt sind , wird dies als Mitgliederinitialisierungsliste bezeichnet .)
Was bedeutet es, ein person
Objekt zu kopieren ? Die main
Funktion zeigt zwei unterschiedliche Kopierszenarien. Die Initialisierung person b(a);
wird vom Kopierkonstruktor durchgeführt . Seine Aufgabe ist es, ein neues Objekt basierend auf dem Status eines vorhandenen Objekts zu erstellen. Die Zuordnung b = a
wird vom Kopierzuweisungsoperator ausgeführt . Die Aufgabe ist im Allgemeinen etwas komplizierter, da sich das Zielobjekt bereits in einem gültigen Zustand befindet, der behandelt werden muss.
Da wir weder den Kopierkonstruktor noch den Zuweisungsoperator (noch den Destruktor) selbst deklariert haben, sind diese implizit für uns definiert. Zitat aus dem Standard:
Der [...] Kopierkonstruktor und der Kopierzuweisungsoperator, [...] und der Destruktor sind spezielle Elementfunktionen. [ Hinweis : Die Implementierung deklariert diese Elementfunktionen implizit für einige Klassentypen, wenn das Programm sie nicht explizit deklariert. Die Implementierung definiert sie implizit, wenn sie verwendet werden. [...] Endnote ] [n3126.pdf Abschnitt 12 § 1]
Standardmäßig bedeutet das Kopieren eines Objekts das Kopieren seiner Mitglieder:
Der implizit definierte Kopierkonstruktor für eine Nicht-Union-Klasse X führt eine Mitgliedskopie ihrer Unterobjekte durch. [n3126.pdf Abschnitt 12.8 §16]
Der implizit definierte Kopierzuweisungsoperator für eine Nicht-Gewerkschaftsklasse X führt eine kopierweise Zuweisung ihrer Unterobjekte durch. [n3126.pdf Abschnitt 12.8 §30]
Die implizit definierten speziellen Elementfunktionen für person
sehen folgendermaßen aus:
// 1. copy constructor
person(const person& that) : name(that.name), age(that.age)
{
}
// 2. copy assignment operator
person& operator=(const person& that)
{
name = that.name;
age = that.age;
return *this;
}
// 3. destructor
~person()
{
}
Das kopieren von Mitgliedern ist genau das, was wir in diesem Fall wollen:
name
und age
werden kopiert, sodass wir ein in sich geschlossenes, unabhängiges person
Objekt erhalten. Der implizit definierte Destruktor ist immer leer. Dies ist auch in diesem Fall in Ordnung, da wir im Konstruktor keine Ressourcen erworben haben. Die Destruktoren der Mitglieder werden implizit aufgerufen, nachdem der person
Destruktor fertig ist:
Nach dem Ausführen des Körpers des Destruktors und dem Zerstören aller im Körper zugewiesenen automatischen Objekte ruft ein Destruktor für Klasse X die Destruktoren für die direkten [...] Mitglieder von X auf [n3126.pdf 12.4 §6].
Wann sollten wir diese speziellen Mitgliedsfunktionen explizit deklarieren? Wenn unsere Klasse verwaltet eine Ressource , das heißt, wenn ein Objekt der Klasse ist verantwortlich für diese Ressource. Das bedeutet in der Regel die Ressource erworben im Konstruktor (oder in den Konstruktor übergibt) und freigegeben im destructor.
Lassen Sie uns in der Zeit zurück zu C ++ gehen. Es gab keine std::string
und Programmierer waren in Zeiger verliebt. Die person
Klasse könnte so ausgesehen haben:
class person
{
char* name;
int age;
public:
// the constructor acquires a resource:
// in this case, dynamic memory obtained via new[]
person(const char* the_name, int the_age)
{
name = new char[strlen(the_name) + 1];
strcpy(name, the_name);
age = the_age;
}
// the destructor must release this resource via delete[]
~person()
{
delete[] name;
}
};
Noch heute schreiben die Leute Klassen in diesem Stil und geraten in Schwierigkeiten: „ Ich habe eine Person in einen Vektor gestoßen und jetzt bekomme ich verrückte Speicherfehler! “ Denken Sie daran, dass das Kopieren eines Objekts standardmäßig das Kopieren seiner Mitglieder bedeutet, aber nur das Kopieren des name
Mitglieds kopiert einen Zeiger, nicht das Zeichenarray, auf das er zeigt! Dies hat mehrere unangenehme Auswirkungen:
a
können über beobachtet werden b
.b
zerstört a.name
ist, baumelt ein Zeiger.a
es zerstört wird, führt das Löschen des baumelnden Zeigers zu undefiniertem Verhalten .name
vor der Zuweisung hingewiesen wurde, treten früher oder später überall Speicherlecks auf.Da das kopieren nach Elementen nicht den gewünschten Effekt hat, müssen wir den Kopierkonstruktor und den Kopierzuweisungsoperator explizit definieren, um tiefe Kopien des Zeichenarrays zu erstellen:
// 1. copy constructor
person(const person& that)
{
name = new char[strlen(that.name) + 1];
strcpy(name, that.name);
age = that.age;
}
// 2. copy assignment operator
person& operator=(const person& that)
{
if (this != &that)
{
delete[] name;
// This is a dangerous point in the flow of execution!
// We have temporarily invalidated the class invariants,
// and the next statement might throw an exception,
// leaving the object in an invalid state :(
name = new char[strlen(that.name) + 1];
strcpy(name, that.name);
age = that.age;
}
return *this;
}
Beachten Sie den Unterschied zwischen Initialisierung und Zuweisung: Wir müssen den alten Status vor der Zuweisung aufheben name
, um Speicherverluste zu vermeiden. Wir müssen uns auch vor Selbstzuweisung des Formulars schützen x = x
. Ohne diese Prüfung delete[] name
würde das Array mit der Quellzeichenfolge gelöscht , da beim Schreiben x = x
beide this->name
und that.name
denselben Zeiger enthalten.
Leider new char[...]
schlägt diese Lösung fehl, wenn aufgrund von Speicherauslastung eine Ausnahme ausgelöst wird. Eine mögliche Lösung besteht darin, eine lokale Variable einzuführen und die Anweisungen neu zu ordnen:
// 2. copy assignment operator
person& operator=(const person& that)
{
char* local_name = new char[strlen(that.name) + 1];
// If the above statement throws,
// the object is still in the same state as before.
// None of the following statements will throw an exception :)
strcpy(local_name, that.name);
delete[] name;
name = local_name;
age = that.age;
return *this;
}
Dies sorgt auch für eine Selbstzuweisung ohne explizite Prüfung. Eine noch robustere Lösung für dieses Problem ist das Copy-and-Swap-Idiom , aber ich werde hier nicht auf die Details der Ausnahmesicherheit eingehen. Ich habe nur Ausnahmen erwähnt, um den folgenden Punkt hervorzuheben : Das Schreiben von Klassen, die Ressourcen verwalten, ist schwierig.
Einige Ressourcen können oder sollten nicht kopiert werden, z. B. Dateihandles oder Mutexe. In diesem Fall deklarieren Sie einfach den Kopierkonstruktor und den Kopierzuweisungsoperator als private
ohne Definition:
private:
person(const person& that);
person& operator=(const person& that);
Alternativ können Sie sie erben boost::noncopyable
oder als gelöscht deklarieren (in C ++ 11 und höher):
person(const person& that) = delete;
person& operator=(const person& that) = delete;
Manchmal müssen Sie eine Klasse implementieren, die eine Ressource verwaltet. (Verwalten Sie niemals mehrere Ressourcen in einer Klasse. Dies führt nur zu Schmerzen.) Denken Sie in diesem Fall an die Dreierregel :
Wenn Sie entweder den Destruktor, den Kopierkonstruktor oder den Kopierzuweisungsoperator selbst explizit deklarieren müssen, müssen Sie wahrscheinlich alle drei explizit deklarieren.
(Leider wird diese "Regel" weder vom C ++ - Standard noch von einem mir bekannten Compiler durchgesetzt.)
Ab C ++ 11 verfügt ein Objekt über zwei spezielle Elementfunktionen: den Verschiebungskonstruktor und die Verschiebungszuweisung. Die Regel von fünf Staaten, um diese Funktionen ebenfalls zu implementieren.
Ein Beispiel mit den Signaturen:
class person
{
std::string name;
int age;
public:
person(const std::string& name, int age); // Ctor
person(const person &) = default; // Copy Ctor
person(person &&) noexcept = default; // Move Ctor
person& operator=(const person &) = default; // Copy Assignment
person& operator=(person &&) noexcept = default; // Move Assignment
~person() noexcept = default; // Dtor
};
Die Regel von 3/5 wird auch als Regel von 0/3/5 bezeichnet. Der Nullteil der Regel besagt, dass Sie beim Erstellen Ihrer Klasse keine der speziellen Elementfunktionen schreiben dürfen.
In den meisten Fällen müssen Sie eine Ressource nicht selbst verwalten, da eine vorhandene Klasse dies std::string
bereits für Sie erledigt. Vergleichen Sie einfach den einfachen Code mit einem std::string
Mitglied mit der verschlungenen und fehleranfälligen Alternative mit a, char*
und Sie sollten überzeugt sein. Solange Sie sich von rohen Zeigerelementen fernhalten, ist es unwahrscheinlich, dass die Dreierregel Ihren eigenen Code betrifft.
Die Dreierregel ist eine Faustregel für C ++
Wenn Ihre Klasse etwas braucht
- ein Kopierkonstruktor ,
- ein Zuweisungsoperator ,
- oder ein Zerstörer ,
explizit definiert, dann werden wahrscheinlich alle drei benötigt .
Der Grund dafür ist, dass alle drei normalerweise zum Verwalten einer Ressource verwendet werden. Wenn Ihre Klasse eine Ressource verwaltet, muss sie normalerweise sowohl das Kopieren als auch das Freigeben verwalten.
Wenn es keine gute Semantik zum Kopieren der von Ihrer Klasse verwalteten Ressource gibt, sollten Sie das Kopieren verbieten, indem Sie den Kopierkonstruktor und den Zuweisungsoperator als deklarieren (nicht definieren ) private
.
(Beachten Sie, dass die bevorstehende neue Version des C ++ - Standards (C ++ 11) C ++ eine Verschiebungssemantik hinzufügt, wodurch sich wahrscheinlich die Dreierregel ändert. Ich weiß jedoch zu wenig darüber, um einen C ++ 11-Abschnitt zu schreiben über die Dreierregel.)
boost::noncopyable
). Es kann auch viel klarer sein. Ich denke, dass C ++ 0x und die Möglichkeit, Funktionen zu "löschen", hier helfen könnten, vergaß aber die Syntax: /
noncopyable
es nicht Teil der Standardbibliothek ist, halte ich es nicht für eine große Verbesserung. (Oh, und wenn Sie die Löschsyntax vergessen haben, haben Sie mehr Ethan vergessen, das ich jemals kannte. :)
)
Das Gesetz der großen Drei ist wie oben angegeben.
Ein einfaches Beispiel für die Art des Problems, das es löst:
Nicht standardmäßiger Destruktor
Sie haben Speicher in Ihrem Konstruktor zugewiesen und müssen daher einen Destruktor schreiben, um ihn zu löschen. Andernfalls kommt es zu einem Speicherverlust.
Sie könnten denken, dass dies erledigt ist.
Das Problem besteht darin, dass, wenn eine Kopie Ihres Objekts erstellt wird, die Kopie auf denselben Speicher wie das ursprüngliche Objekt verweist.
Sobald einer von diesen den Speicher in seinem Destruktor löscht, hat der andere einen Zeiger auf einen ungültigen Speicher (dies wird als baumelnder Zeiger bezeichnet), wenn er versucht, ihn zu verwenden, werden die Dinge haarig.
Daher schreiben Sie einen Kopierkonstruktor, damit er neuen Objekten ihre eigenen Speicherelemente zum Zerstören zuweist.
Zuweisungsoperator und Kopierkonstruktor
Sie haben einem Elementzeiger Ihrer Klasse Speicher in Ihrem Konstruktor zugewiesen. Wenn Sie ein Objekt dieser Klasse kopieren, kopieren der Standardzuweisungsoperator und der Kopierkonstruktor den Wert dieses Elementzeigers auf das neue Objekt.
Dies bedeutet, dass das neue Objekt und das alte Objekt auf dasselbe Speicherelement zeigen. Wenn Sie es also in einem Objekt ändern, wird es auch für das andere Objekt geändert. Wenn ein Objekt diesen Speicher löscht, versucht das andere weiterhin, ihn zu verwenden - eek.
Um dies zu beheben, schreiben Sie Ihre eigene Version des Kopierkonstruktors und des Zuweisungsoperators. Ihre Versionen weisen den neuen Objekten separaten Speicher zu und kopieren die Werte, auf die der erste Zeiger zeigt, und nicht die Adresse.
Wenn Sie einen Destruktor haben (nicht den Standard-Destruktor), bedeutet dies im Grunde, dass die von Ihnen definierte Klasse eine gewisse Speicherzuordnung hat. Angenommen, die Klasse wird außerhalb von einem Clientcode oder von Ihnen verwendet.
MyClass x(a, b);
MyClass y(c, d);
x = y; // This is a shallow copy if assignment operator is not provided
Wenn MyClass nur einige primitiv typisierte Elemente hat, würde ein Standardzuweisungsoperator funktionieren, aber wenn es einige Zeigerelemente und Objekte hat, die keine Zuweisungsoperatoren haben, wäre das Ergebnis unvorhersehbar. Daher können wir sagen, dass wir, wenn im Destruktor einer Klasse etwas zu löschen ist, möglicherweise einen Deep-Copy-Operator benötigen, was bedeutet, dass wir einen Copy-Konstruktor und einen Zuweisungsoperator bereitstellen sollten.
Was bedeutet das Kopieren eines Objekts? Es gibt einige Möglichkeiten, wie Sie Objekte kopieren können. Lassen Sie uns über die beiden Arten sprechen, auf die Sie sich am wahrscheinlichsten beziehen - Deep Copy und Flat Copy.
Da wir in einer objektorientierten Sprache sind (oder dies zumindest annehmen), nehmen wir an, Sie haben ein Stück Speicher zugewiesen. Da es sich um eine OO-Sprache handelt, können wir leicht auf von uns zugewiesene Speicherblöcke verweisen, da es sich normalerweise um primitive Variablen (Ints, Zeichen, Bytes) oder von uns definierte Klassen handelt, die aus unseren eigenen Typen und Grundelementen bestehen. Nehmen wir also an, wir haben eine Klasse von Autos wie folgt:
class Car //A very simple class just to demonstrate what these definitions mean.
//It's pseudocode C++/Javaish, I assume strings do not need to be allocated.
{
private String sPrintColor;
private String sModel;
private String sMake;
public changePaint(String newColor)
{
this.sPrintColor = newColor;
}
public Car(String model, String make, String color) //Constructor
{
this.sPrintColor = color;
this.sModel = model;
this.sMake = make;
}
public ~Car() //Destructor
{
//Because we did not create any custom types, we aren't adding more code.
//Anytime your object goes out of scope / program collects garbage / etc. this guy gets called + all other related destructors.
//Since we did not use anything but strings, we have nothing additional to handle.
//The assumption is being made that the 3 strings will be handled by string's destructor and that it is being called automatically--if this were not the case you would need to do it here.
}
public Car(const Car &other) // Copy Constructor
{
this.sPrintColor = other.sPrintColor;
this.sModel = other.sModel;
this.sMake = other.sMake;
}
public Car &operator =(const Car &other) // Assignment Operator
{
if(this != &other)
{
this.sPrintColor = other.sPrintColor;
this.sModel = other.sModel;
this.sMake = other.sMake;
}
return *this;
}
}
Eine tiefe Kopie ist, wenn wir ein Objekt deklarieren und dann eine vollständig separate Kopie des Objekts erstellen ... wir erhalten 2 Objekte in 2 vollständig gespeicherten Sätzen.
Car car1 = new Car("mustang", "ford", "red");
Car car2 = car1; //Call the copy constructor
car2.changePaint("green");
//car2 is now green but car1 is still red.
Jetzt machen wir etwas Seltsames. Angenommen, car2 ist entweder falsch programmiert oder absichtlich dazu gedacht, den tatsächlichen Speicher zu teilen, aus dem car1 besteht. (Es ist normalerweise ein Fehler, dies zu tun, und in Klassen ist es normalerweise die Decke, unter der es besprochen wird.) Stellen Sie sich vor, dass Sie jedes Mal, wenn Sie nach car2 fragen, wirklich einen Zeiger auf den Speicherplatz von car1 auflösen ... das ist mehr oder weniger eine flache Kopie ist.
//Shallow copy example
//Assume we're in C++ because it's standard behavior is to shallow copy objects if you do not have a constructor written for an operation.
//Now let's assume I do not have any code for the assignment or copy operations like I do above...with those now gone, C++ will use the default.
Car car1 = new Car("ford", "mustang", "red");
Car car2 = car1;
car2.changePaint("green");//car1 is also now green
delete car2;/*I get rid of my car which is also really your car...I told C++ to resolve
the address of where car2 exists and delete the memory...which is also
the memory associated with your car.*/
car1.changePaint("red");/*program will likely crash because this area is
no longer allocated to the program.*/
Unabhängig davon, in welcher Sprache Sie schreiben, sollten Sie beim Kopieren von Objekten sehr vorsichtig sein, da Sie die meiste Zeit eine tiefe Kopie wünschen.
Was sind der Kopierkonstruktor und der Kopierzuweisungsoperator? Ich habe sie oben bereits verwendet. Der Kopierkonstruktor wird aufgerufen, wenn Sie Code eingeben, z. B. Car car2 = car1;
Im Wesentlichen, wenn Sie eine Variable deklarieren und in einer Zeile zuweisen. In diesem Fall wird der Kopierkonstruktor aufgerufen. Der Zuweisungsoperator ist das, was passiert, wenn Sie ein Gleichheitszeichen verwenden car2 = car1;
. Hinweis car2
wird nicht in derselben Anweisung deklariert. Die beiden Codestücke, die Sie für diese Vorgänge schreiben, sind wahrscheinlich sehr ähnlich. Tatsächlich hat das typische Entwurfsmuster eine andere Funktion, die Sie aufrufen, um alles festzulegen, sobald Sie zufrieden sind. Die anfängliche Kopie / Zuweisung ist legitim. Wenn Sie sich den von mir geschriebenen Langhandcode ansehen, sind die Funktionen nahezu identisch.
Wann muss ich sie selbst deklarieren? Wenn Sie keinen Code schreiben, der gemeinsam genutzt oder auf irgendeine Weise produziert werden soll, müssen Sie ihn nur dann deklarieren, wenn Sie ihn benötigen. Sie müssen sich darüber im Klaren sein, was Ihre Programmiersprache tut, wenn Sie sie "versehentlich" verwenden und keine erstellt haben - dh Sie erhalten den Compiler-Standard. Ich verwende zum Beispiel selten Kopierkonstruktoren, aber Überschreibungen von Zuweisungsoperatoren sind sehr häufig. Wussten Sie, dass Sie überschreiben können, was Addition, Subtraktion usw. ebenfalls bedeuten?
Wie kann ich verhindern, dass meine Objekte kopiert werden? Das Überschreiben aller Möglichkeiten, wie Sie Speicher für Ihr Objekt mit einer privaten Funktion zuweisen dürfen, ist ein vernünftiger Anfang. Wenn Sie wirklich nicht möchten, dass Personen sie kopieren, können Sie sie öffentlich machen und den Programmierer benachrichtigen, indem Sie eine Ausnahme auslösen und das Objekt auch nicht kopieren.
Wann muss ich sie selbst deklarieren?
Die Dreierregel besagt, dass, wenn Sie eine von a deklarieren
dann sollten Sie alle drei deklarieren. Es entstand aus der Beobachtung, dass die Notwendigkeit, die Bedeutung eines Kopiervorgangs zu übernehmen, fast immer von der Klasse herrührte, die eine Art Ressourcenmanagement durchführte, und das implizierte dies fast immer
Was auch immer die Ressourcenverwaltung in einem Kopiervorgang durchgeführt wurde, musste wahrscheinlich in dem anderen Kopiervorgang durchgeführt werden und
Der Klassendestruktor würde auch an der Verwaltung der Ressource teilnehmen (normalerweise wird sie freigegeben). Die klassische zu verwaltende Ressource war Speicher. Aus diesem Grund deklarieren alle Standardbibliotheksklassen, die Speicher verwalten (z. B. die STL-Container, die eine dynamische Speicherverwaltung durchführen), alle die „großen Drei“: sowohl Kopiervorgänge als auch einen Destruktor.
Eine Konsequenz der Dreierregel ist, dass das Vorhandensein eines vom Benutzer deklarierten Destruktors darauf hinweist, dass eine einfache Kopie in Bezug auf die Mitglieder für die Kopiervorgänge in der Klasse wahrscheinlich nicht geeignet ist. Dies legt wiederum nahe, dass die Kopiervorgänge, wenn eine Klasse einen Destruktor deklariert, wahrscheinlich nicht automatisch generiert werden sollten, da sie nicht das Richtige tun würden. Zum Zeitpunkt der Einführung von C ++ 98 wurde die Bedeutung dieser Argumentation nicht vollständig erkannt, sodass in C ++ 98 die Existenz eines vom Benutzer deklarierten Destruktors keinen Einfluss auf die Bereitschaft der Compiler hatte, Kopiervorgänge zu generieren. Dies ist in C ++ 11 weiterhin der Fall, jedoch nur, weil eine Einschränkung der Bedingungen, unter denen die Kopiervorgänge generiert werden, zu viel Legacy-Code beschädigen würde.
Wie kann ich verhindern, dass meine Objekte kopiert werden?
Deklarieren Sie den Kopierkonstruktor und den Kopierzuweisungsoperator als privaten Zugriffsspezifizierer.
class MemoryBlock
{
public:
//code here
private:
MemoryBlock(const MemoryBlock& other)
{
cout<<"copy constructor"<<endl;
}
// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other)
{
return *this;
}
};
int main()
{
MemoryBlock a;
MemoryBlock b(a);
}
Ab C ++ 11 können Sie auch den Kopierkonstruktor und den Zuweisungsoperator für gelöscht erklären
class MemoryBlock
{
public:
MemoryBlock(const MemoryBlock& other) = delete
// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other) =delete
};
int main()
{
MemoryBlock a;
MemoryBlock b(a);
}
Viele der vorhandenen Antworten berühren bereits den Kopierkonstruktor, den Zuweisungsoperator und den Destruktor. In Post C ++ 11 kann die Einführung der Bewegungssemantik dies jedoch über 3 hinaus erweitern.
Kürzlich hielt Michael Claisse einen Vortrag zu diesem Thema: http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class
Die Dreierregel in C ++ ist ein Grundprinzip des Entwurfs und der Entwicklung von drei Anforderungen: Wenn eine der folgenden Elementfunktionen klar definiert ist, sollte der Programmierer die beiden anderen Elementfunktionen zusammen definieren. Die folgenden drei Elementfunktionen sind nämlich unverzichtbar: Destruktor, Kopierkonstruktor, Kopierzuweisungsoperator.
Der Kopierkonstruktor in C ++ ist ein spezieller Konstruktor. Es wird verwendet, um ein neues Objekt zu erstellen. Dies ist das neue Objekt, das einer Kopie eines vorhandenen Objekts entspricht.
Der Zuweisungsoperator kopieren ist ein spezieller Zuweisungsoperator, mit dem normalerweise ein vorhandenes Objekt für andere Objekte desselben Objekttyps angegeben wird.
Es gibt kurze Beispiele:
// default constructor
My_Class a;
// copy constructor
My_Class b(a);
// copy constructor
My_Class c = a;
// copy assignment operator
b = a;
c++-faq
Tag-Wiki, bevor du abstimmst, um zu schließen .