Jaka jest zasada trzech?

  • co oznaczaKopiowanie obiektu ?
  • Czym są Konstruktor kopiujący i operator przypisania kopii?
  • Kiedy muszę je zgłosić?
  • Jak mogę zapobiec kopiowaniu moich obiektów?
Author: Rann Lifshitz, 2010-11-13

8 answers

Wprowadzenie

C++ traktuje zmienne typów zdefiniowanych przez użytkownika z semantyką wartości . Oznacza to, że obiekty są domyślnie kopiowane w różnych kontekstach, i powinniśmy zrozumieć, co tak naprawdę oznacza" kopiowanie obiektu".

Rozważmy prosty przykład:]}
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?
}

(jeśli dziwi Cię name(name), age(age) część, to się nazywa lista inicjalizatorów.)

Specjalne funkcje członków

Co to znaczy skopiować person obiekt? Funkcja main pokazuje dwa różne scenariusze kopiowania. Inicjalizacja {[10] } jest wykonywana przez Konstruktor kopiujący . Jego zadaniem jest skonstruowanie nowego obiektu w oparciu o stan istniejącego obiektu. Przypisanie {[11] } jest wykonywane przez Operator kopiowania przypisania . Jego praca jest na ogół trochę bardziej skomplikowana, ponieważ obiekt docelowy jest już w jakimś prawidłowym stanie, z którym należy się uporać.

Ponieważ nie zadeklarowaliśmy konstruktora kopiującego ani operator przypisania (ani Destruktor) siebie, są one dla nas zdefiniowane w sposób dorozumiany. Cytat ze standardu:

The [...] Konstruktor kopiujący i operator kopiowania, [...] i destruktor są funkcjami specjalnymi. [Uwaga: implementacja domyślnie deklaruje te funkcje Członkowskie dla niektórych typów klas, gdy program nie deklaruje ich jawnie. Implementacja w sposób dorozumiany zdefiniuje je, jeśli są używane. [...] Uwaga końcowa ] [n3126]pdf Sekcja 12 §1]

Domyślnie kopiowanie obiektu oznacza kopiowanie jego członków:

Niejawnie zdefiniowany Konstruktor kopiujący dla niezwiązanej klasy X wykonuje kopię memberwise swoich podobiektów. [n3126]pdf § 12.8 §16]

Niejawnie zdefiniowany operator przypisania kopii dla klasy X nie będącej unifikacją wykonuje przypisanie kopii memwise swoich podobiektów. [n3126]pdf § 12.8 §30]

Implicit definicje

Domyślnie zdefiniowane funkcje specjalne dla person wyglądają tak:

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

Kopiowanie Memwise jest dokładnie tym, czego chcemy w tym przypadku: name i age są kopiowane, więc otrzymujemy samodzielny, niezależny person obiekt. Domyślnie zdefiniowany Destruktor jest zawsze pusty. Jest to również w porządku w tym przypadku, ponieważ nie nabyliśmy żadnych zasobów w konstruktorze. Destruktory członków są niejawnie wywoływane po destruktorze person koniec:

Po wykonaniu ciała destruktora i zniszczeniu wszelkich automatycznych obiektów przydzielonych w ciele, destruktor dla klasy X wywołuje destruktory dla bezpośredniego [...] członkowie [n3126]pdf 12.4 §6]

Zarządzanie zasobami

Więc kiedy powinniśmy jawnie zadeklarować te specjalne funkcje Członkowskie? Gdy nasza klasa zarządza zasobem , czyli, gdy obiekt klasy jest odpowiedzialny za ten zasób. Że zwykle oznacza, że zasób jest nabyty w konstruktorze (lub przekazane do konstruktora) i wydane w destruktorze.

Cofnijmy się w czasie do pre-standardowego C++. Nie było czegoś takiego jak std::string, a programiści byli zakochani w wskaźnikach. Klasa person mogła wyglądać tak:
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;
    }
};

Nawet dzisiaj ludzie nadal piszą klasy w tym stylu i wpadają w kłopoty: "wepchnąłem człowieka w wektor i teraz mam zwariowaną pamięć błędy!" Pamiętaj, że domyślnie kopiowanie obiektu oznacza kopiowanie jego członków, ale kopiowanie elementu name kopiuje jedynie wskaźnik, , a nie tablicę znaków, na którą wskazuje! Ma to kilka nieprzyjemnych skutków:

  1. zmiany za pomocą {[20] } można zaobserwować za pomocą b.
  2. po zniszczeniu b, a.name jest zwisającym wskaźnikiem.
  3. jeśli a zostanie zniszczony, usunięcie zwisającego wskaźnika spowoduje niezdefiniowane zachowanie .
  4. ponieważ przypisanie nie uwzględnia tego, co name wskazywało przed przypisaniem, prędzej czy później dostaniesz wycieki pamięci wszędzie.

Jednoznaczne definicje

Ponieważ kopiowanie memwise nie daje pożądanego efektu, musimy jawnie zdefiniować Konstruktor kopiujący i operator przypisania kopii, aby tworzyć głębokie kopie tablicy znaków:

// 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;
}

Zwróć uwagę na różnicę między inicjalizacją a przypisaniem: musimy zburzyć stare Państwo przed przypisaniem do name, aby zapobiec wyciekom pamięci. Ponadto, musimy zabezpieczyć się przed samodzielnym przypisaniem formy x = x. Bez tego sprawdzenia, delete[] name usunie tablicę zawierającą łańcuchsource , ponieważ kiedy piszesz x = x, zarówno this->name jak i that.name zawierają ten sam wskaźnik.

Bezpieczeństwo WYJĄTKÓW

Niestety, to rozwiązanie nie powiedzie się, jeśli new char[...] wyrzuci wyjątek z powodu wyczerpania pamięci. Jednym z możliwych rozwiązań jest wprowadzenie zmiennej lokalnej i zmiana kolejności wypowiedzi:

// 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;
}

To również zajmuje się samo-przypisaniem bez wyraźnej kontroli. Jeszcze bardziej solidnym rozwiązaniem tego problemu jest idiom copy-and-swap , ale nie będę tu wchodzić w szczegóły bezpieczeństwa WYJĄTKÓW. Wspomniałem tylko o wyjątkach, aby dokonać następującego punktu: pisanie klas, które zarządzają zasobami, jest trudne.

Zasoby niekodowane

Niektóre zasoby nie mogą lub nie powinny być kopiowane, takie jak uchwyty plików lub muteksy. W w tym przypadku po prostu zadeklaruj Konstruktor kopiujący i operator przypisania kopii jako private, nie podając definicji:

private:

    person(const person& that);
    person& operator=(const person& that);
Można też dziedziczyć z boost::noncopyable lub zadeklarować je jako usunięte (C++0x):
person(const person& that) = delete;
person& operator=(const person& that) = delete;

Zasada trzech

Czasami trzeba zaimplementować klasę, która zarządza zasobem. (Nigdy nie zarządzaj wieloma zasobami w jednej klasie, to tylko doprowadzi do bólu.) W takim przypadku, pamiętaj zasada trzech :

Jeśli trzeba jednoznacznie zadeklarować albo Destruktor, Konstruktor kopiujący lub operator kopiowania samodzielnie, prawdopodobnie musisz wyraźnie zadeklarować wszystkie trzy z nich.

(niestety, ta "reguła" nie jest egzekwowana przez standard C++ ani żaden kompilator, którego znam.)

Porady

Przez większość czasu nie musisz samodzielnie zarządzać zasobem, ponieważ istniejąca Klasa, taka jak std::string, robi to już za Ciebie. Po prostu porównaj prosty kod używając std::string członek do zawiłej i podatnej na błędy alternatywy za pomocą char* i powinieneś być przekonany. Tak długo, jak trzymasz się z dala od surowych członków wskaźnika, reguła trzech jest mało prawdopodobne, aby dotyczyć własnego kodu.

 1542
Author: fredoverflow,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 10:31:37

Zasada trzech jest regułą dla C++, w zasadzie mówiącą

Jeśli twoja klasa potrzebuje któregokolwiek z

  • a Konstruktor kopiujący ,
  • AN Operator przydziału ,
  • lub Destruktor ,

Zdefiniowany jasno, wtedy prawdopodobnie będzie potrzebował wszystkich trzech.

Powodem tego jest to, że wszystkie trzy z nich są zwykle używane do zarządzania zasobem, a jeśli twoja klasa zarządza zasób, zwykle musi zarządzać kopiowaniem, a także uwalnianiem.

Jeśli nie ma dobrego semantyki do kopiowania zasobu, którym zarządza twoja klasa, rozważ zabronienie kopiowania przez zadeklarowanie (Nie definiowanie) Konstruktor kopiujący i operator przypisania jako private.

(zauważ, że nadchodząca nowa wersja standardu C++ (którym jest C++11) dodaje semantykę move do C++, co prawdopodobnie zmieni regułę Three. Jednak wiem zbyt mało na ten temat, aby napisać C++11 sekcja o zasadzie trzech.)

 456
Author: sbi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:03:09

Prawo Wielkiej Trójki jest określone powyżej.

Prosty przykład, w prostym języku angielskim, rodzaju problemu, który rozwiązuje:

Non default destructor

Przypisałeś pamięć do konstruktora, więc musisz napisać Destruktor, aby go usunąć. W przeciwnym razie spowodujesz wyciek pamięci.

Można by pomyśleć, że to robota wykonana.

Problem będzie polegał na tym, że jeśli zostanie wykonana kopia Twojego obiektu, to Kopia wskaże tę samą pamięć co oryginalny obiekt.

Raz, jeden z nich usunie pamięć w swoim destruktorze, drugi będzie miał wskaźnik do nieprawidłowej pamięci (nazywa się to zwisającym wskaźnikiem), gdy spróbuje go użyć, rzeczy będą się Owłosione.

Dlatego piszesz Konstruktor kopiujący, który przydziela nowym obiektom ich własne kawałki pamięci do zniszczenia.

Operator przypisania i Konstruktor kopiujący

Przypisałeś pamięć w konstruktorze do wskaźnika członka twojej klasy. Podczas kopiowania obiektu tej klasy domyślny operator przypisania i Konstruktor kopiujący skopiują wartość tego wskaźnika członka do nowego obiektu.

Oznacza to, że nowy obiekt i stary obiekt będą wskazywać na ten sam kawałek pamięci, więc gdy zmienisz go w jednym obiekcie, będzie on również zmieniany dla drugiego obiektu. Jeśli jeden obiekt usunie tę pamięć, drugi będzie próbował jej użyć - eek.

Aby to rozwiązać piszesz własną wersję kopii konstruktor i operator przydziałów. Wersje przydzielają oddzielną pamięć nowym obiektom i kopiują wartości, na które wskazuje pierwszy wskaźnik, a nie jego adres.

 137
Author: Stefan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-01-09 18:27:49

Zasadniczo jeśli masz Destruktor (Nie domyślny Destruktor), oznacza to, że klasa, którą zdefiniowałeś, ma pewną alokację pamięci. Załóżmy, że klasa jest używana na zewnątrz przez jakiś kod klienta lub przez Ciebie.

    MyClass x(a, b);
    MyClass y(c, d);
    x = y; // This is a shallow copy if assignment operator is not provided

Jeśli MyClass ma tylko kilka prymitywnych członków typowanych, domyślny operator przypisania będzie działał, ale jeśli ma kilka elementów wskaźnikowych i obiektów, które nie mają operatorów przypisania, wynik byłby nieprzewidywalny. Dlatego możemy powiedzieć, że jeśli jest coś do usunięcia w Destruktor klasy, możemy potrzebować operatora głębokiej kopii, co oznacza, że powinniśmy dostarczyć Konstruktor kopiujący i operator przypisania.

 39
Author: fatma.ekici,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-09-11 11:39:15

Co oznacza kopiowanie obiektu? Istnieje kilka sposobów na kopiowanie obiektów-porozmawiajmy o 2 rodzajach, do których najprawdopodobniej się odnosisz-głębokiej kopii i płytkiej kopii.

Ponieważ jesteśmy w języku zorientowanym obiektowo (a przynajmniej tak Zakładamy), Załóżmy, że masz przydzieloną część pamięci. Ponieważ jest to język OO, możemy łatwo odwoływać się do przydzielanych przez nas fragmentów pamięci, ponieważ są one zazwyczaj prymitywnymi zmiennymi (ints, chars, bajty) lub zdefiniowanymi przez nas klasami składającymi się z naszych własne typy i prymitywy. Powiedzmy więc, że mamy klasę samochodu w następujący sposób:

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;
}

}

Głęboka kopia jest wtedy, gdy deklarujemy obiekt, a następnie tworzymy całkowicie oddzielną kopię obiektu...kończymy z 2 obiektami w 2 kompletnych zestawach pamięci.

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.
Zróbmy coś dziwnego. Załóżmy, że car2 jest albo źle zaprogramowany, albo celowo ma dzielić rzeczywistą pamięć, z której jest zbudowany car1. (Zwykle jest to błąd, aby to zrobić i na zajęciach jest zwykle koc jest omawiany pod.) Udawaj, że za każdym razem, gdy pytasz o car2, naprawdę rozwiązujesz wskaźnik do przestrzeni pamięci car1...mniej więcej tym jest płytka Kopia.
//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.*/

Więc niezależnie od tego, w jakim języku piszesz, bądź bardzo ostrożny z tym, co masz na myśli, jeśli chodzi o kopiowanie obiektów, ponieważ przez większość czasu potrzebujesz głębokiej kopii.

Czym są Konstruktor kopiujący i operator kopiowania? Korzystałem już z nich powyżej. Konstruktor kopiujący jest wywoływany podczas wpisywania kodu na przykład Car car2 = car1; zasadniczo jeśli zadeklarujesz zmienną i przypisasz ją w jednej linii, wtedy wywoływany jest Konstruktor kopiujący. Operatorem przypisania jest to, co się dzieje, gdy używasz znaku równości -- car2 = car1;. Notice car2 nie jest zadeklarowana w tym samym oświadczeniu. Dwa fragmenty kodu, które piszesz dla tych operacji, są prawdopodobnie bardzo podobne. W rzeczywistości typowy wzorzec projektowy ma inną funkcję, którą wywołujesz, aby ustawić wszystko, gdy będziesz zadowolony, początkowa Kopia/przypisanie jest uzasadnione-jeśli spojrzysz na długoręczny kod, który napisałem, funkcje są prawie identyczne.

Kiedy muszę je zgłosić? Jeśli nie piszesz kodu, który ma być udostępniony lub do produkcji w jakiś sposób, tak naprawdę musisz zadeklarować je tylko wtedy, gdy ich potrzebujesz. Musisz być świadomy tego, co robi twój język programu, jeśli zdecydujesz się go użyć "przez przypadek" i go nie stworzyłeś-tzn. otrzymujesz domyślny kompilator. Rzadko używam na przykład konstruktorów kopiujących, ale nadpisania operatorów przyporządkowania są bardzo pospolite. Czy wiesz, że możesz zastąpić dodawanie, odejmowanie itp. wredny też?

Jak mogę zapobiec kopiowaniu moich obiektów? Zastąpienie wszystkich sposobów przydzielania pamięci obiektowi za pomocą funkcji prywatnej to rozsądny początek. Jeśli naprawdę nie chcesz, aby ludzie je kopiowali, możesz to upublicznić i ostrzec programistę, rzucając wyjątek, a także nie kopiując obiektu.

 30
Author: user1701047,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-12-12 13:09:19
Kiedy muszę je zgłosić?

Zasada trzech mówi, że jeśli zadeklarujesz jakąkolwiek

  1. Konstruktor kopiujący
  2. Operator kopiowania
  3. destructor

Następnie należy zadeklarować wszystkie trzy. Wyrosło to z obserwacji, że potrzeba przejęcia znaczenia operacji kopiowania prawie zawsze wynikała z klasy wykonującej jakiś rodzaj zarządzania zasobami, a to prawie zawsze oznaczało, że

  • Niezależnie od tego, czy zarządzanie zasobami było wykonywane w jednej operacji kopiowania, prawdopodobnie należało to zrobić w drugiej operacji kopiowania i

  • Destruktor klas również bierze udział w zarządzaniu zasobem (Zwykle go zwalnia). Klasycznym zasobem do zarządzania była pamięć. dlatego wszystkie standardowe klasy biblioteczne, które Zarządzaj pamięcią (np. kontenery STL wykonujące dynamiczne zarządzanie pamięcią) wszystkie deklarują "wielką trójkę": oba kopiują Centrala i destruktor.

Konsekwencją reguły trzech jest to, że obecność zadeklarowanego przez użytkownika destruktora wskazuje, że prosta Kopia member wise jest mało prawdopodobna dla operacji kopiowania w klasie. To z kolei sugeruje, że jeśli Klasa deklaruje Destruktor, operacje kopiowania prawdopodobnie nie powinny być generowane automatycznie, ponieważ nie zrobiłyby one dobrze. W momencie przyjęcia C++98 znaczenie tej linii rozumowanie nie było w pełni doceniane, więc w C++98 istnienie użytkownika deklarowanego destruktorem nie miało wpływu na gotowość kompilatorów do generowania operacji kopiowania. Tak jest nadal w C++11, ale tylko dlatego, że ograniczenie warunków, w jakich generowane są operacje kopiowania, złamałoby zbyt wiele kodu źródłowego.

Jak mogę zapobiec kopiowaniu moich obiektów?

Declare copy constructor & copy assignment operator as private access konkretniej.

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);
}

W C++11 można również zadeklarować usunięcie konstruktora kopiującego i operatora przypisania

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

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


int main()
{
   MemoryBlock a;
   MemoryBlock b(a);
}
 21
Author: Ajay yadav,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-01-12 10:19:01

Wiele z istniejących odpowiedzi dotyka już konstruktora kopiującego, operatora przypisania i destruktora. Jednak w post C++11 wprowadzenie move semantic może rozszerzyć to poza 3.

Ostatnio Michael Claisse wygłosił wykład, który dotyka tego tematu: http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class

 11
Author: wei,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-01-07 05:38:51

Zasada trzech w C++ jest podstawową zasadą projektowania i rozwoju trzech wymagań, że jeśli w jednej z poniższych funkcji członowych istnieje jasna definicja, to programista powinien zdefiniować pozostałe dwie funkcje członowe razem. Mianowicie niezbędne są następujące trzy funkcje członowe: Destruktor, Konstruktor kopiujący, operator kopiowania.

Konstruktor kopiujący w C++ jest specjalnym konstruktorem. Służy do budowy nowego obiektu, który jest nowym obiekt równoważny kopii istniejącego obiektu.

Operator kopiowania jest operatorem specjalnego przydziału, który jest zwykle używany do określenia istniejącego obiektu innym obiektom tego samego typu.

Są szybkie przykłady:

// default constructor
My_Class a;

// copy constructor
My_Class b(a);

// copy constructor
My_Class c = a;

// copy assignment operator
b = a;
 7
Author: Marcus Thornton,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-10-16 04:57:08