- 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?
c++
copy-constructor
assignment-operator
c++-faq
rule-of-three
Fredoverflow
quelle
quelle
c++-faq
Tag-Wiki, bevor du abstimmst, um zu schließen .Antworten:
Einführung
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:
(Wenn Sie von dem
name(name), age(age)
Teil verwirrt sind , wird dies als Mitgliederinitialisierungsliste bezeichnet .)Spezielle Mitgliedsfunktionen
Was bedeutet es, ein
person
Objekt zu kopieren ? Diemain
Funktion zeigt zwei unterschiedliche Kopierszenarien. Die Initialisierungperson b(a);
wird vom Kopierkonstruktor durchgeführt . Seine Aufgabe ist es, ein neues Objekt basierend auf dem Status eines vorhandenen Objekts zu erstellen. Die Zuordnungb = 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:
Standardmäßig bedeutet das Kopieren eines Objekts das Kopieren seiner Mitglieder:
Implizite Definitionen
Die implizit definierten speziellen Elementfunktionen für
person
sehen folgendermaßen aus:Das kopieren von Mitgliedern ist genau das, was wir in diesem Fall wollen:
name
undage
werden kopiert, sodass wir ein in sich geschlossenes, unabhängigesperson
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 derperson
Destruktor fertig ist:Ressourcen verwalten
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. Dieperson
Klasse könnte so ausgesehen haben: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 werdenb
.b
zerstörta.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.Explizite Definitionen
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:
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ützenx = x
. Ohne diese Prüfungdelete[] name
würde das Array mit der Quellzeichenfolge gelöscht , da beim Schreibenx = x
beidethis->name
undthat.name
denselben Zeiger enthalten.Ausnahmesicherheit
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: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.
Nicht kopierbare Ressourcen
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:Alternativ können Sie sie erben
boost::noncopyable
oder als gelöscht deklarieren (in C ++ 11 und höher):Die Dreierregel
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 :
(Leider wird diese "Regel" weder vom C ++ - Standard noch von einem mir bekannten Compiler durchgesetzt.)
Die Regel von fünf
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:
Die Regel von Null
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.
Rat
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 einemstd::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.quelle
Die Dreierregel ist eine Faustregel für C ++
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.)
quelle
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.
quelle
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.
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.
quelle
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:
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.
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.
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 verwendencar2 = car1;
. Hinweiscar2
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.
quelle
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.
Deklarieren Sie den Kopierkonstruktor und den Kopierzuweisungsoperator als privaten Zugriffsspezifizierer.
Ab C ++ 11 können Sie auch den Kopierkonstruktor und den Zuweisungsoperator für gelöscht erklären
quelle
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
quelle
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:
quelle