c++ - 3er regeln




Was ist die Regel der Drei? (6)

Was bedeutet das Kopieren eines Objekts ? Was sind der Kopierkonstruktor und der Kopierzuweisungsoperator ? Wann muss ich sie selbst erklären? Wie kann ich verhindern, dass meine Objekte kopiert werden?


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 zu verwaltende klassische Ressource war Speicher, und deshalb deklarieren alle Standardbibliotheksklassen, die Speicher verwalten (z. B. die STL-Container, die dynamische Speicherverwaltung durchführen) alle "die großen Drei": sowohl Kopieroperationen als auch einen Destruktor.

Eine Folge der Drei-Regel besagt, dass das Vorhandensein eines vom Benutzer deklarierten Destruktors anzeigt, dass eine einfache Kopie durch Mitglieder wahrscheinlich nicht für die Kopiervorgänge in der Klasse geeignet ist. Das wiederum legt 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 von einem 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);
}

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 "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 sich über den name(name), age(age) wundern, wird dies als Mitgliedinitialisierungsliste bezeichnet .)

Spezielle Mitgliederfunktionen

Was bedeutet es, ein person zu kopieren? Die Hauptfunktion zeigt zwei verschiedene Kopierszenarien. Die Initialisierungsperson person b(a); wird vom Kopierkonstruktor ausgeführt . Seine Aufgabe besteht darin, ein neues Objekt basierend auf dem Status eines vorhandenen Objekts zu konstruieren. Die Zuweisung b = a wird vom Kopierzuweisungsoperator durchgeführt . Ihre Aufgabe ist in der Regel ein wenig 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 -destruktor sind spezielle Elementfunktionen. [ Hinweis : Die Implementierung deklariert diese Memberfunktionen 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]

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 seiner Unterobjekte durch. [n3126.pdf Abschnitt 12.8 §30]

Implizite Definitionen

Die implizit definierten speziellen Member-Funktionen für die 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()
{
}

Memberwise-Kopieren ist genau das, was wir in diesem Fall wollen: name und age werden kopiert, so dass wir ein in sich geschlossenes, unabhängiges person . 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 aufgerufen, nachdem der person beendet ist:

Nachdem der Körper des Destruktors ausgeführt wurde und alle im Körper zugewiesenen automatischen Objekte gelöscht wurden, ruft ein Destruktor für die Klasse X 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 eine Ressource verwaltet, dh wenn ein Objekt der Klasse für diese Ressource verantwortlich ist. Das bedeutet normalerweise, dass die Ressource im Konstruktor erworben (oder an den Konstruktor übergeben) und im Destruktor freigegeben wird .

Lassen Sie uns zurück in die Zeit vor dem Standard C ++ gehen. Es gab kein std::string , und Programmierer waren in Zeiger verliebt. Die person könnte folgendermaßen aussehen:

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 Leute Klassen in diesem Stil und geraten in Schwierigkeiten: " Ich habe eine Person in einen Vektor gedrängt und bekomme jetzt verrückte Speicherfehler! " Denken Sie daran, dass das Kopieren eines Objekts standardmäßig das Kopieren seiner Mitglieder bedeutet, aber das Kopieren des name Kopiert nur einen Zeiger, nicht das Zeichenfeld, auf das er zeigt! Dies hat mehrere unangenehme Auswirkungen:

  1. Änderungen über a können über b beobachtet werden.
  2. Sobald b zerstört ist, ist a.name ein a.name Zeiger.
  3. Wenn a zerstört ist, führt das Löschen des Dangling Pointers zu undefiniertem Verhalten .
  4. Da bei der Zuweisung nicht berücksichtigt wird, auf welchen name vor der Zuweisung verwiesen wird, werden Sie früher oder später überall Speicherlecks erhalten.

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 vor dem Zuweisen von name abreißen, um Speicherlecks zu vermeiden. Außerdem müssen wir vor der Selbstzuweisung der Form x = x schützen. Ohne diese Prüfung würde delete[] name das Array löschen, das die that.name enthält, denn wenn Sie x = x schreiben, enthalten sowohl this->name als auch that.name denselben Zeiger.

Ausnahmesicherheit

Leider schlägt diese Lösung fehl, wenn ein new char[...] Zeichen wegen Speichermüdigkeit eine Ausnahme auslöst. 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 stabilere Lösung für dieses Problem ist das Kopieren-und-Tauschen-Idiom , aber ich werde hier nicht auf die Details der Ausnahmesicherheit eingehen. Ich habe nur Ausnahmen erwähnt, um den folgenden Punkt zu verdeutlichen: Es ist schwer, Klassen zu schreiben, die Ressourcen verwalten.

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 anzugeben:

private:

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

Alternativ können Sie boost::noncopyable erben 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 führt nur zu Schmerzen.) Denken Sie in diesem Fall an die Dreiregel :

Wenn Sie den Destruktor, den Kopierkonstruktor oder den Kopierzuweisungsoperator explizit explizit deklarieren müssen, müssen Sie wahrscheinlich alle drei explizit deklarieren.

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

Rat

In den meisten Fällen müssen Sie eine Ressource nicht selbst verwalten, da eine vorhandene Klasse wie std::string bereits für Sie erledigt. Vergleichen Sie einfach den einfachen Code mit einem std::string Member mit der gewundenen und fehleranfälligen Alternative mit einem char* und Sie sollten überzeugt werden. Solange Sie sich von rohen Pointer-Mitgliedern fern halten, ist es unwahrscheinlich, dass die Drei-Regel Ihren eigenen Code betrifft.


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

Wenn deine Klasse etwas benötigt

  • ein Kopierkonstrukteur ,
  • ein Zuweisungsoperator ,
  • oder ein Destruktor ,

Definitiv definiert, dann wird es wahrscheinlich alle drei brauchen.

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, dann sollten Sie das Kopieren verbieten, indem Sie den Kopierkonstruktor und den Zuweisungsoperator als private deklarieren (nicht defining ).

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


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;

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 völlig 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 in der Regel die Decke, die es unter diskutiert wird.) So tun, wenn Sie nach car2 fragen, Sie wirklich einen Zeiger auf den Speicherbereich von car1 auflösen ... das ist mehr oder weniger was für 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 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 car2 = car1; . Hinweis car2 wird nicht in der gleichen Aussage erklärt. 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 Programmiersprache tut, wenn Sie sie "aus Versehen" verwenden und nicht einen machen - dh 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.


Wenn Sie einen Destruktor haben (nicht den Standard-Destruktor), bedeutet das, dass die von Ihnen definierte Klasse eine gewisse Speicherzuweisung hat. 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.





rule-of-three