Was ist die Dreierregel?

2148
  • 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?
Fredoverflow
quelle
52
Bitte lies diesen ganzen Thread und das c++-faqTag-Wiki, bevor du abstimmst, um zu schließen .
sbi
13
@Binary: Nehmen Sie sich mindestens die Zeit, um die Kommentardiskussion zu lesen, bevor Sie eine Stimme abgeben. Der Text war früher viel einfacher, aber Fred wurde gebeten, ihn zu erweitern. Auch wenn das grammatikalisch vier Fragen sind , ist es wirklich nur eine Frage mit mehreren Aspekten. (Wenn Sie damit nicht einverstanden sind, beweisen Sie Ihre POV, indem Sie jede dieser Fragen
einzeln
1
Fred, hier ist eine interessante Ergänzung zu deiner Antwort zu C ++ 1x: stackoverflow.com/questions/4782757/… . Wie gehen wir damit um?
sbi
6
Verwandte: Das Gesetz der großen Zwei
Nemanja Trifunovic
4
Denken Sie daran, dass dies ab C ++ 11 auf die Regel fünf oder ähnliches aktualisiert wurde.
Paxdiablo

Antworten:

1794

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:

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 .)

Spezielle Mitgliedsfunktionen

Was bedeutet es, ein personObjekt zu kopieren ? Die mainFunktion 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 = awird 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]

Implizite Definitionen

Die implizit definierten speziellen Elementfunktionen für personsehen 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: nameund agewerden kopiert, sodass wir ein in sich geschlossenes, unabhängiges personObjekt 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 personDestruktor 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].

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::stringund Programmierer waren in Zeiger verliebt. Die personKlasse 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 nameMitglieds kopiert einen Zeiger, nicht das Zeichenarray, auf das er zeigt! Dies hat mehrere unangenehme Auswirkungen:

  1. Änderungen über akönnen über beobachtet werden b.
  2. Sobald bzerstört a.nameist, baumelt ein Zeiger.
  3. Wenn aes zerstört wird, führt das Löschen des baumelnden Zeigers zu undefiniertem Verhalten .
  4. Da bei der Zuweisung nicht berücksichtigt wird, worauf namevor 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:

// 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[] namewürde das Array mit der Quellzeichenfolge gelöscht , da beim Schreiben x = xbeide this->nameund that.namedenselben 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:

// 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.

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 privateohne Definition:

private:

    person(const person& that);
    person& operator=(const person& that);

Alternativ können Sie sie erben boost::noncopyableoder als gelöscht deklarieren (in C ++ 11 und höher):

person(const person& that) = delete;
person& operator=(const person& that) = delete;

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 :

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.)

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:

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 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::stringbereits für Sie erledigt. Vergleichen Sie einfach den einfachen Code mit einem std::stringMitglied 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.

Fredoverflow
quelle
4
Fred, ich würde mich bei meiner Abstimmung besser fühlen, wenn (A) Sie die schlecht implementierte Zuweisung nicht in kopierbarem Code formulieren und eine Notiz hinzufügen würden, die besagt, dass sie falsch ist, und an anderer Stelle im Kleingedruckten nachsehen würden; Verwenden Sie entweder c & s im Code oder überspringen Sie einfach die Implementierung all dieser Mitglieder (B). Sie würden die erste Hälfte verkürzen, was wenig mit dem RoT zu tun hat. (C) Sie würden die Einführung der Bewegungssemantik diskutieren und was dies für die RoT bedeutet.
sbi
7
Aber dann sollte der Beitrag C / W gemacht werden, denke ich. Ich finde es gut, dass Sie die Begriffe größtenteils korrekt halten (dh dass Sie " Kopierzuweisungsoperator " sagen und nicht in die allgemeine Falle tippen, dass die Zuweisung keine Kopie implizieren kann).
Johannes Schaub - Litb
4
@Prasoon: Ich denke nicht, dass das Ausschneiden der Hälfte der Antwort als "faire Bearbeitung" einer Nicht-CW-Antwort angesehen werden würde.
sbi
69
Es wäre großartig, wenn Sie Ihren Beitrag für C ++ 11 aktualisieren (dh Konstruktor / Zuweisung verschieben)
Alexander Malakhov
5
@solalito Alles, was Sie nach der Verwendung freigeben müssen: Parallelitätssperren, Dateihandles, Datenbankverbindungen, Netzwerk-Sockets, Heap-Speicher ...
Fredoverflow
509

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.)

sbi
quelle
3
Eine andere Lösung, um das Kopieren zu verhindern, besteht darin, (privat) von einer Klasse zu erben, die nicht kopiert werden kann (wie 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: /
Matthieu M.
2
@Matthieu: Ja, das funktioniert auch. Aber wenn noncopyablees 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. :))
sbi
3
@ Daan: Siehe diese Antwort . Ich würde jedoch empfehlen bleiben Martinho ‚s Rule of Zero . Für mich ist dies eine der wichtigsten Faustregeln für C ++, die im letzten Jahrzehnt geprägt wurden.
sbi
3
Martinhos Null-Regel jetzt besser (ohne offensichtliche Adware-Übernahme) auf archive.org
Nathan Kidd
161

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.

Stefan
quelle
4
Wenn wir also einen Kopierkonstruktor verwenden, wird die Kopie erstellt, jedoch an einem anderen Speicherort. Wenn wir keinen Kopierkonstruktor verwenden, wird eine Kopie erstellt, die jedoch auf denselben Speicherort verweist. ist es das, was du sagen willst? Eine Kopie ohne Kopierkonstruktor bedeutet also, dass ein neuer Zeiger vorhanden ist, der jedoch auf denselben Speicherort zeigt. Wenn jedoch der Kopierkonstruktor explizit vom Benutzer definiert wurde, haben wir einen separaten Zeiger, der auf einen anderen Speicherort zeigt, aber die Daten enthält.
Unbreakable
4
Entschuldigung, ich habe vor Ewigkeiten darauf geantwortet, aber meine Antwort scheint noch nicht hier zu sein :-( Grundsätzlich ja - du verstehst es :-)
Stefan
1
Wie wirkt sich das Prinzip auf den Kopierzuweisungsoperator aus? Diese Antwort wäre nützlicher, wenn die dritte in der Dreierregel erwähnt würde.
DBedrenko
1
@DBedrenko, "Sie schreiben einen Kopierkonstruktor, damit er neuen Objekten ihre eigenen Speicherstücke zuweist ..." Dies ist das gleiche Prinzip, das sich auch auf den Kopierzuweisungsoperator erstreckt. Glaubst du nicht, ich habe das klargestellt?
Stefan
2
@DBedrenko, ich habe einige weitere Informationen hinzugefügt. Macht das das klarer?
Stefan
44

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.

fatma.ekici
quelle
36

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 car2wird 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.

user1701047
quelle
5
Die Frage wurde mit C ++ markiert. Diese Pseudocode-Darstellung klärt bestenfalls wenig über die gut definierte "Dreierregel" und verbreitet im schlimmsten Fall nur Verwirrung.
sehe
26

Wann muss ich sie selbst deklarieren?

Die Dreierregel besagt, dass, wenn Sie eine von a deklarieren

  1. Konstruktor kopieren
  2. Kopierzuweisungsoperator
  3. Zerstörer

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);
}
Ajay yadav
quelle
10

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;
Marcus Thornton
quelle
7
Hallo, deine Antwort fügt nichts Neues hinzu. Die anderen behandeln das Thema viel ausführlicher und genauer - Ihre Antwort ist ungefähr und an einigen Stellen sogar falsch (nämlich, dass es hier kein "Muss" gibt; es ist "sehr wahrscheinlich sollte"). Es lohnt sich wirklich nicht, diese Art von Antwort auf Fragen zu veröffentlichen, die bereits gründlich beantwortet wurden. Es sei denn, Sie müssen neue Dinge hinzufügen.
Mat
1
Es gibt auch vier kurze Beispiele, die irgendwie mit zwei der drei zusammenhängen , von denen die Dreierregel spricht. Zu viel Verwirrung.
Anatolyg