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?
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 jakstd::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:
- zmiany za pomocą {[20] } można zaobserwować za pomocą
b
. - po zniszczeniu
b
,a.name
jest zwisającym wskaźnikiem. - jeśli
a
zostanie zniszczony, usunięcie zwisającego wskaźnika spowoduje niezdefiniowane zachowanie . - 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.
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.)
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.
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.
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.
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.
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
- Konstruktor kopiujący
- Operator kopiowania
- 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);
}
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
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;
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