Frage Was ist die Regel der Drei?


  • Was macht ein Objekt kopieren bedeuten?
  • Was sind die Konstruktor kopieren und das Zuweisungsoperator kopieren?
  • Wann muss ich sie selbst erklären?
  • Wie kann ich verhindern, dass meine Objekte kopiert werden?

1841
2017-11-13 13:27


Ursprung


Antworten:


Einführung

C ++ behandelt Variablen von benutzerdefinierten Typen mit Wert Semantik. Dies bedeutet, dass Objekte implizit in verschiedenen Kontexten kopiert werden, und wir sollten verstehen, was "ein Objekt kopieren" eigentlich 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 der name(name), age(age) Teil, das nennt man a Mitgliedsinitialisierungsliste.)

Spezielle Mitgliederfunktionen

Was bedeutet es, a zu kopieren? person Objekt? Das main Funktion zeigt zwei verschiedene Kopierszenarien. Die Initialisierung person b(a); wird von der durchgeführt Konstruktor kopieren. Seine Aufgabe besteht darin, ein neues Objekt basierend auf dem Status eines vorhandenen Objekts zu konstruieren. Die Zuordnung b = a wird von der durchgeführt Zuweisungsoperator kopieren. Seine Aufgabe ist in der Regel ein wenig komplizierter, weil das Zielobjekt bereits in einem gültigen Zustand ist, der behandelt werden muss.

Da wir weder den Kopierkonstruktor noch den Zuweisungsoperator (noch den Destruktor) selbst deklariert haben, diese sind implizit für uns definiert. Zitat aus dem Standard:

Der Kopierkonstruktor und der Kopierzuweisungsoperator [...] und -destruktor sind spezielle Elementfunktionen.   [ Hinweis: Die Implementierung wird diese Memberfunktionen implizit deklarieren   für einige Klassenarten, wenn das Programm sie nicht explizit deklariert.   Die Implementierung definiert sie implizit, wenn sie verwendet werden. [...] Endnote ]   [n3126.pdf Abschnitt 12 §1]

Das Kopieren eines Objekts bedeutet standardmäßig das Kopieren seiner Mitglieder:

Der implizit definierte Kopierkonstruktor für eine Nicht-Vereinigungsklasse X führt eine elementweise Kopie seiner Unterobjekte durch.   [n3126.pdf Abschnitt 12.8 §16]

Der implizit definierte Kopierzuweisungsoperator für eine nicht gewerkschaftliche Klasse X führt eine elementweise Kopierzuordnung durch   seiner Unterobjekte.   [n3126.pdf Abschnitt 12.8 §30]

Implizite Definitionen

Die implizit definierten speziellen Member-Funktionen für person sieht aus wie das:

// 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()
{
}

Memberwise-Kopieren ist genau das, was wir in diesem Fall wollen: name und age kopiert werden, so erhalten wir eine in sich geschlossene, unabhängige person Objekt. Der implizit definierte Destruktor ist immer leer. Dies ist auch in diesem Fall in Ordnung, da wir im Konstruktor keine Ressourcen erhalten haben. Die Destruktoren der Mitglieder werden implizit nach der person Destruktor ist fertig:

Nachdem der Körper des Destruktors ausgeführt wurde und alle im Körper zugewiesenen automatischen Objekte zerstört wurden,   Ein Destruktor für die Klasse X ruft die Destruktoren für die direkten [...] Mitglieder von X auf   [n3126.pdf 12.4 §6]

Ressourcen verwalten

Wann sollten wir diese speziellen Memberfunktionen explizit deklarieren? Wenn unsere Klasse verwaltet eine Ressource, das ist, wenn ein Objekt der Klasse ist verantwortlich für diese Ressource. Das bedeutet normalerweise, dass die Ressource ist erworben im Konstruktor (oder in den Konstruktor übergeben) und freigegeben im Destruktor.

Lassen Sie uns zurück in die Zeit vor dem Standard C ++ gehen. So etwas gab es nicht std::stringund Programmierer waren in Zeiger verliebt. Das 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;
    }
};

Auch heute noch schreiben Menschen in diesem Stil und geraten in Schwierigkeiten: "Ich habe eine Person in einen Vektor gedrängt und jetzt bekomme ich verrückte Gedächtnisfehler!" Denken Sie daran, dass das Kopieren eines Objekts standardmäßig das Kopieren seiner Mitglieder bedeutet. aber das kopieren name Mitglied kopiert nur einen Zeiger, nicht das Zeichenarray, auf das es zeigt! Dies hat mehrere unangenehme Auswirkungen:

  1. Änderungen über a kann über beobachtet werden b.
  2. Einmal b ist zerstört, a.name ist ein fliegender Zeiger.
  3. Ob a zerstört wird, löscht der dangling Zeiger undefiniertes Verhalten.
  4. Da die Zuordnung nicht berücksichtigt was name wies auf vor dem Auftrag, Früher oder später wirst du Speicherlecks überall bekommen.

Explizite Definitionen

Da das memberweise Kopieren nicht den gewünschten Effekt hat, müssen Sie 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 Zustand abreißen, bevor wir ihn zuweisen name um Speicherlecks zu verhindern. Außerdem müssen wir uns vor der Selbstzuteilung des Formulars schützen x = x. Ohne diesen Scheck delete[] name würde das Array löschen, das das enthält Quelle Schnur, denn wenn du schreibst x = x, beide this->name und that.name enthält den gleichen Zeiger.

Ausnahmesicherheit

Leider wird diese Lösung fehlschlagen, wenn new char[...] löst aufgrund von Speichererschöpfung eine Ausnahme aus. 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 eine explizite Überprüfung. Eine noch robustere Lösung für dieses Problem ist die Kopieren-und-Tauschen-Idiom, Aber ich werde hier nicht auf die Einzelheiten der Ausnahmesicherheit eingehen. Ich habe nur Ausnahmen erwähnt, um den folgenden Punkt zu formulieren: 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 eine Definition zu geben:

private:

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

Alternativ können Sie von übernehmen boost::noncopyable oder sie als gelöscht deklarieren (C ++ 0x):

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

Die Regel von drei

Manchmal müssen Sie eine Klasse implementieren, die eine Ressource verwaltet. (Verwalten Sie niemals mehrere Ressourcen in einer einzelnen Klasse, Dies wird nur zu Schmerzen führen.) In diesem Fall erinnere dich an die Regel von drei:

Wenn Sie den Destruktor explizit deklarieren müssen,   Kopieren Sie den Konstruktor oder kopieren Sie den Zuweisungsoperator selbst,   Sie müssen wahrscheinlich alle drei explizit deklarieren.

(Leider wird diese "Regel" nicht vom C ++ - Standard oder einem mir bekannten Compiler erzwungen.)

Rat

Meistens müssen Sie keine Ressource selbst verwalten, weil eine bestehende Klasse wie std::string tut es schon für dich. Vergleichen Sie einfach den einfachen Code mit a std::string Mitglied zu der verschachtelten und fehleranfälligen Alternative mit a char* und du solltest überzeugt sein. Solange Sie sich von rohen Pointer-Mitgliedern fern halten, ist es unwahrscheinlich, dass die Drei-Regel Ihren eigenen Code betrifft.


1512
2017-11-13 13:27



Das Regel der Drei ist eine Faustregel für C ++, im Grunde gesagt

Wenn deine Klasse etwas benötigt

  • ein Konstruktor kopieren,
  • ein Aufgabenverwalter,
  • oder ein Destruktor,

Explizit definiert, dann ist es wahrscheinlich notwendig alle drei.

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 Ressource gibt, die Ihre Klasse verwaltet, dann sollten Sie das Kopieren durch Deklaration verbieten (nicht Definieren) der Kopierkonstruktor und der Zuweisungsoperator als private.

(Beachten Sie, dass die bevorstehende neue Version des C ++ - Standards (die C ++ 11 ist), fügt Bewegungssemantik zu C ++ hinzu, was wahrscheinlich die Dreierregel ändern wird, aber ich weiß zu wenig darüber, um einen C ++ 11-Abschnitt zu schreiben über die Dreiregel.)


450
2017-11-13 14:22



Das Gesetz der großen Drei ist wie oben angegeben.

Ein einfaches Beispiel, in einfachem Englisch, von der Art von Problem, das es löst:

Nicht standardmäßiger Destruktor

Sie haben Speicher in Ihrem Konstruktor zugewiesen, und Sie müssen einen Destruktor schreiben, um ihn zu löschen. Andernfalls verursachen Sie ein Speicherleck.

Sie könnten denken, dass dies erledigt ist.

Das Problem wird sein, wenn eine Kopie von Ihrem Objekt erstellt wird, dann zeigt die Kopie auf den gleichen Speicher wie das ursprüngliche Objekt.

Einmal löscht eine davon den Speicher in seinem Destruktor, die andere hat einen Zeiger auf einen ungültigen Speicher (dies wird als Dangling Pointer bezeichnet), wenn es versucht, es zu benutzen, werden die Dinge haarig werden.

Daher schreiben Sie einen Kopierkonstruktor, so dass er neuen Objekten ihre eigenen Speicherbereiche zuweist, um sie zu zerstören.

Zuweisungsoperator und Kopierkonstruktor

Sie haben Speicher in Ihrem Konstruktor einem Member-Zeiger Ihrer Klasse zugewiesen. Wenn Sie ein Objekt dieser Klasse kopieren, kopiert der Standardzuweisungsoperator und der Kopierkonstruktor den Wert dieses Mitgliedszeigers auf das neue Objekt.

Dies bedeutet, dass das neue Objekt und das alte Objekt auf dasselbe Speicherelement zeigen, so dass, wenn Sie es in einem Objekt ändern, es auch für das andere Objekt geändert wird. Wenn ein Objekt diesen Speicher löscht, wird der andere versuchen, es zu benutzen - 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 verweist, und nicht seine Adresse.


134
2018-05-14 14:22



Wenn Sie einen Destruktor haben (nicht den Standard-Destruktor), bedeutet dies, dass die von Ihnen definierte Klasse über eine Speicherzuweisung verfügt. Angenommen, die Klasse wird außerhalb von einem Client-Code 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 primitive typisierte Elemente hat, würde ein Standardzuweisungsoperator funktionieren, aber wenn er einige Zeigerelemente und Objekte hat, die keine Zuweisungsoperatoren haben, wäre das Ergebnis unvorhersehbar. Daher können wir sagen, dass wir, wenn es im Destruktor einer Klasse etwas zu löschen gibt, einen Operator für tiefe Kopien benötigen, was bedeutet, dass wir einen Kopierkonstruktor und einen Zuweisungsoperator bereitstellen sollten.


37
2017-12-31 19:29



Was bedeutet das Kopieren eines Objekts? Es gibt ein paar Möglichkeiten, wie Sie Objekte kopieren können - lassen Sie uns über die zwei Arten sprechen, auf die Sie sich am wahrscheinlichsten beziehen - tiefe Kopie und flache Kopie.

Da wir uns in einer objektorientierten Sprache befinden (oder zumindest davon ausgehen), nehmen wir an, Sie haben ein Stück Speicher zugewiesen. Da es sich um eine OO-Sprache handelt, können wir leicht auf Speicherblöcke verweisen, die wir zuordnen, weil sie normalerweise primitive Variablen (Inte, Chars, Bytes) oder Klassen sind, die wir aus unseren eigenen Typen und Primitiven gebildet haben. Nehmen wir an, wir haben eine Klasse von Auto 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 enden mit 2 Objekten in 2 vollständigen Sätzen von Speicher.

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. Nehmen wir an, car2 ist entweder falsch programmiert oder bewusst dazu gedacht, den tatsächlichen Speicher zu teilen, aus dem car1 besteht. (Es ist normalerweise ein Fehler, dies zu tun und in Klassen ist normalerweise die Decke, die es unter diskutiert wird.) So tun, wenn Sie nach car2 fragen, Sie wirklich einen Zeiger auf den Speicherbereich von Auto1 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.*/

Ungeachtet dessen, in welcher Sprache Sie schreiben, sollten Sie sehr vorsichtig sein, was Sie meinen, wenn es darum geht, Objekte zu kopieren, weil Sie die meiste Zeit eine tiefe Kopie haben wollen.

Was sind der Kopierkonstruktor und der Kopierzuweisungsoperator? Ich habe sie bereits oben benutzt. Der Kopierkonstruktor wird aufgerufen, wenn Sie Code wie z Car car2 = car1;  Wenn Sie eine Variable deklarieren und sie in einer Zeile zuweisen, wird der Kopierkonstruktor aufgerufen. Der Zuweisungsoperator ist, was passiert, wenn Sie ein Gleichheitszeichen verwenden--car2 = car1;. Beachten car2 wird nicht in derselben Anweisung deklariert. Die zwei Codeabschnitte, die Sie für diese Operationen schreiben, sind wahrscheinlich sehr ähnlich. In der Tat hat das typische Entwurfsmuster eine andere Funktion, die Sie aufrufen, um alles einzustellen, sobald Sie zufrieden sind, ist die anfängliche Kopie / Zuweisung legitim - wenn Sie sich den von mir geschriebenen Code ansehen, sind die Funktionen fast identisch.

Wann muss ich sie selbst erklären? Wenn Sie keinen Code schreiben, der auf irgendeine Weise geteilt oder für die Produktion verwendet werden soll, müssen Sie sie nur dann deklarieren, wenn Sie sie benötigen. Sie müssen sich dessen bewusst sein, was Ihre Programmsprache tut, wenn Sie sich dafür entscheiden, es "zufällig" zu benutzen und es nicht gemacht hat - d. Sie erhalten den Compiler-Standard. Ich benutze zum Beispiel selten Kopierkonstrukteure, aber Überschreibungen von Zuweisungsoperatoren sind sehr üblich. Wussten Sie, dass Sie auch überschreiben können, was Addition, Subtraktion usw. bedeuten?

Wie kann ich verhindern, dass meine Objekte kopiert werden? Überschreiben Sie alle Möglichkeiten, wie Sie Speicher für Ihr Objekt mit einer privaten Funktion zuweisen können, ist ein vernünftiger Start. Wenn Sie wirklich nicht wollen, dass Leute sie kopieren, können Sie sie öffentlich machen und den Programmierer warnen, indem Sie eine Ausnahme auslösen und das Objekt nicht kopieren.


27
2017-10-17 16:37



Wann muss ich sie selbst erklären?

Die Regel der Drei besagt, dass, wenn Sie eines von a

  1. Konstruktor kopieren
  2. Zuweisungsoperator kopieren
  3. Destruktor

dann solltest du alle drei deklarieren. Es entstand aus der Beobachtung, dass die Notwendigkeit, die Bedeutung einer Kopieroperation zu übernehmen, fast immer von der Klasse herrührt, die irgendeine Art von Ressourcenverwaltung durchführt, und dies beinhaltete fast immer dies

  • welche Ressourcenverwaltung in einer Kopieroperation durchgeführt wurde, musste wahrscheinlich in der anderen Kopieroperation ausgeführt werden

  • der Klassendestruktor würde auch an der Verwaltung der Ressource beteiligt sein (normalerweise wird sie freigegeben). Die klassische Ressource, die verwaltet werden sollte, war Speicher, und das ist der Grund, warum alle Klassen der Standardbibliothek das sind Speicherverwaltung (z. B. die STL-Container, die dynamische Speicherverwaltung durchführen) deklarieren alle "die großen drei": sowohl Kopiervorgänge als auch einen Destruktor.

Eine Folge der Dreierregel ist, dass die Anwesenheit eines vom Benutzer deklarierten Destruktors anzeigt, dass eine einfache kopierte Kopie wahrscheinlich nicht für die Kopiervorgänge in der Klasse geeignet ist. Dies legt wiederum nahe, dass, wenn eine Klasse einen Destruktor deklariert, die Kopieroperationen wahrscheinlich nicht automatisch erzeugt werden sollten, weil sie nicht das Richtige tun würden. Zu der Zeit, als C ++ 98 angenommen wurde, wurde die Bedeutung dieser Argumentationskette nicht vollständig erkannt, so dass in C ++ 98 die Existenz eines vom Benutzer deklarierten Destruktors keinen Einfluss auf die Bereitschaft von Compilern hatte, Kopieroperationen zu erzeugen. Dies ist in C ++ 11 weiterhin der Fall, aber nur, weil die Einschränkung der Bedingungen, unter denen die Kopieroperationen erzeugt werden, zu viel Legacy-Code zerstören 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 löschen

class MemoryBlock
{
public:
MemoryBlock(const MemoryBlock& other) = delete

// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other) =delete
};


int main()
{
   MemoryBlock a;
   MemoryBlock b(a);
}

19
2018-01-12 09:54



Viele der vorhandenen Antworten berühren bereits den Kopierkonstruktor, den Zuweisungsoperator und den Destruktor. In Post C ++ 11 kann die Einführung von move semantic dies jedoch über 3 hinaus erweitern.

Kürzlich hielt Michael Claisse einen Vortrag, der dieses Thema berührt: http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class


9
2018-01-07 05:38



Die Dreierregel in C ++ ist ein grundlegendes Prinzip des Entwurfs und der Entwicklung von drei Anforderungen. Wenn es eine klare Definition in einer der folgenden Elementfunktionen gibt, sollte der Programmierer die anderen beiden Elementfunktionen zusammen definieren. Die folgenden drei Mitgliedsfunktionen sind nämlich unverzichtbar: Destruktor, Kopierkonstruktor, Kopierzuweisungsoperator.

Copy-Konstruktor in C ++ ist ein spezieller Konstruktor. Es wird verwendet, um ein neues Objekt zu erstellen, bei dem es sich um das neue Objekt handelt, das einer Kopie eines vorhandenen Objekts entspricht.

Der Kopierzuweisungsoperator ist ein spezieller Zuweisungsoperator, der normalerweise verwendet wird, um ein existierendes Objekt für andere desselben Objekttyps zu spezifizieren.

Es gibt schnelle Beispiele:

// default constructor
My_Class a;

// copy constructor
My_Class b(a);

// copy constructor
My_Class c = a;

// copy assignment operator
b = a;

5
2017-08-12 04:27