- Co oznacza kopiowanie obiektu ?
- Co to jest konstruktor kopii i operator przypisania kopii ?
- Kiedy sam muszę je zadeklarować?
- Jak mogę zapobiec kopiowaniu moich obiektów?
Odpowiedzi:
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 zastanawia Cię ta name(name), age(age)
część, nazywa się to listą inicjującą członka ).
Co to znaczy skopiować person
obiekt? Ta main
funkcja pokazuje dwa różne scenariusze kopiowania. Inicjalizacja person b(a);
jest wykonywana przez konstruktor kopii . Jego zadaniem jest konstruowanie świeżego obiektu na podstawie stanu istniejącego obiektu. Przypisanie b = a
jest wykonywane przez operatora przypisania kopii . Jego zadanie jest na ogół nieco bardziej skomplikowane, ponieważ obiekt docelowy jest już w pewnym prawidłowym stanie, z którym należy sobie poradzić.
Ponieważ sami nie zadeklarowaliśmy ani konstruktora kopiowania, ani operatora przypisania (ani destruktora), są one dla nas domyślnie zdefiniowane. Cytat ze standardu:
Konstruktor [...] kopii i operator przypisania kopii, [...] i destruktor są specjalnymi funkcjami składowymi. [ Uwaga : Implementacja domyślnie zadeklaruje te funkcje składowe dla niektórych typów klas, gdy program ich jawnie nie zadeklaruje. Implementacja domyślnie je zdefiniuje, jeśli zostaną użyte. [...] uwaga końcowa ] [n3126.pdf sekcja 12 §1]
Domyślnie kopiowanie obiektu oznacza kopiowanie jego elementów:
Niejawnie zdefiniowany konstruktor kopii dla nieunijnej klasy X wykonuje członkową kopię swoich podobiektów. [n3126.pdf sekcja 12.8 §16]
Niejawnie zdefiniowany operator przypisania kopii dla nieunijnej klasy X wykonuje przypisanie członkowe swoich podobiektów. [n3126.pdf sekcja 12.8 §30]
Domyślnie zdefiniowane specjalne funkcje person
składowe dla wyglądają następująco:
// 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 według członków jest dokładnie tym, czego chcemy w tym przypadku:
name
i age
są kopiowane, więc otrzymujemy niezależny, niezależny person
obiekt. Domyślnie zdefiniowany destruktor jest zawsze pusty. Jest to również w porządku w tym przypadku, ponieważ nie uzyskaliśmy żadnych zasobów w konstruktorze. Destruktory członków są domyślnie wywoływane po zakończeniu person
destruktora:
Po wykonaniu korpusu destruktora i zniszczeniu wszelkich automatycznych obiektów przydzielonych w ciele, destruktor klasy X wywołuje destruktory dla bezpośrednich członków X [...] [n3126.pdf 12.4 §6]
Kiedy więc powinniśmy wyraźnie zadeklarować te specjalne funkcje członka? Kiedy nasza klasa zarządza zasobem , to znaczy, gdy obiekt klasy jest odpowiedzialny za ten zasób. Zazwyczaj oznacza to, że zasób jest nabywany w konstruktorze (lub przekazywany do konstruktora) i uwalniany w destruktorze.
Cofnijmy się w czasie do wstępnie standardowego C ++. Nie było czegoś takiego std::string
, a programiści byli zakochani we wskaźnikach. person
Klasa mógł 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ą zajęcia w tym stylu i wpadają w kłopoty: „ Zepchnąłem osobę na wektor, a teraz dostaję szalonych błędów pamięci! ” Pamiętaj, że domyślnie kopiowanie obiektu oznacza kopiowanie jego elementów, ale kopiowanie name
tylko elementu kopiuje wskaźnik, a nie tablicę znaków, na którą wskazuje! Ma to kilka nieprzyjemnych efektów:
a
można obserwować za pomocą b
.b
zniszczony, a.name
zwisający wskaźnik.a
zostanie zniszczony, usunięcie zwisającego wskaźnika daje niezdefiniowane zachowanie .name
wskazywał przed przydziałem, prędzej czy później w całym miejscu pojawią się wycieki pamięci.Ponieważ kopiowanie członków nie daje pożądanego efektu, musimy jawnie zdefiniować konstruktor kopii i operator przypisania kopii, aby wykonać 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 rozebrać stary stan przed przypisaniem, name
aby zapobiec wyciekom pamięci. Musimy również chronić przed samodzielnym przypisaniem formularza x = x
. Bez tej kontroli, delete[] name
by usunąć tablicę zawierającą źródłowy łańcuch, bo kiedy piszesz x = x
, zarówno this->name
i that.name
zawierają ten sam wskaźnik.
Niestety, to rozwiązanie zawiedzie, jeśli new char[...]
zgłosi wyjątek z powodu wyczerpania pamięci. Jednym z możliwych rozwiązań jest wprowadzenie zmiennej lokalnej i zmiana kolejności instrukcji:
// 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;
}
Dotyczy to również samodzielnego przydzielania bez wyraźnej kontroli. Jeszcze bardziej niezawodnym rozwiązaniem tego problemu jest idiom kopiowania i zamiany , ale nie będę tu wchodził w szczegóły dotyczące wyjątkowego bezpieczeństwa. Wspomniałem tylko o wyjątkach, aby przedstawić następujący punkt: Pisanie klas zarządzających zasobami jest trudne.
Niektórych zasobów nie można lub nie należy kopiować, takich jak uchwyty plików lub muteksy. W takim przypadku po prostu zadeklaruj konstruktor kopiowania i operator przypisania kopii jako private
bez podania definicji:
private:
person(const person& that);
person& operator=(const person& that);
Możesz też dziedziczyć je boost::noncopyable
lub zadeklarować jako usunięte (w C ++ 11 i nowszych):
person(const person& that) = delete;
person& operator=(const person& that) = delete;
Czasami musisz zaimplementować klasę zarządzającą zasobem. (Nigdy nie zarządzaj wieloma zasobami w jednej klasie, spowoduje to tylko ból.) W takim przypadku pamiętaj o zasadzie trzech :
Jeśli musisz samodzielnie zadeklarować destruktor, konstruktor kopii lub operator przypisania kopii, prawdopodobnie musisz jawnie zadeklarować wszystkie trzy z nich.
(Niestety, ta „reguła” nie jest egzekwowana przez standard C ++ ani żaden kompilator, o którym wiem.)
Począwszy od C ++ 11 obiekt ma 2 dodatkowe specjalne funkcje składowe: konstruktor ruchu i przypisanie ruchu. Zasada pięciu stanów, aby również wprowadzić te funkcje.
Przykład z podpisami:
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
};
Reguła 3/5 jest również określana jako reguła 0/3/5. Zerowa część reguły stanowi, że nie wolno pisać żadnych specjalnych funkcji składowych podczas tworzenia klasy.
Przez większość czasu nie musisz samodzielnie zarządzać zasobem, ponieważ istniejąca klasa, taka jak std::string
już, robi to za Ciebie. Wystarczy porównać prosty kod za pomocą std::string
elementu członkowskiego ze skomplikowaną i podatną na błędy alternatywą za pomocą a char*
i powinieneś być przekonany. Tak długo, jak trzymasz się z daleka od surowych elementów wskaźnika, jest mało prawdopodobne, aby zasada trzech dotyczyła twojego własnego kodu.
Reguła Trzech jest zasada dla C ++, w zasadzie mówi
Jeśli twoja klasa potrzebuje czegoś
- konstruktor kopiujący ,
- operatorowi przypisanie ,
- lub destruktor ,
zdefiniowane dokładnie, wtedy prawdopodobnie będą potrzebować wszystkich trzech .
Powodem tego jest to, że wszystkie trzy są zwykle używane do zarządzania zasobem, a jeśli klasa zarządza zasobem, zwykle musi zarządzać zarówno kopiowaniem, jak i zwalnianiem.
Jeśli nie ma dobrego semantycznego narzędzia do kopiowania zasobu, którym zarządza klasa, rozważ zakaz zabrania się kopiowania, deklarując (nie definiując ) konstruktora kopiowania i operatora przypisania jako private
.
(Zauważ, że nadchodząca nowa wersja standardu C ++ (czyli C ++ 11) dodaje semantykę przenoszenia do C ++, co prawdopodobnie zmieni Regułę Trzech. Jednak wiem za mało o tym, aby napisać sekcję C ++ 11 o zasadzie trzech).
boost::noncopyable
). Może być również znacznie jaśniejszy. Myślę, że C ++ 0x i możliwość „usuwania” funkcji mogłyby tu pomóc, ale zapomniałem o składni: /
noncopyable
jest częścią standardowej biblioteki, nie uważam tego za poprawę. (Aha, a jeśli zapomniałeś o składni usuwania, zapomniałeś o moim :)
Prawo wielkiej trójki jest określone powyżej.
Prosty przykład, w prostym języku angielskim, rodzaju problemu, który rozwiązuje:
Niszczyciel inny niż domyślny
Przydzieliłeś pamięć do swojego konstruktora, więc musisz napisać destruktor, aby ją usunąć. W przeciwnym razie spowodujesz wyciek pamięci.
Możesz pomyśleć, że to robota wykonana.
Problem będzie polegał na tym, że jeśli twoja kopia zostanie wykonana z twojego obiektu, wówczas kopia wskaże tę samą pamięć co oryginalny obiekt.
Raz jeden z nich usuwa pamięć z niszczyciela, drugi będzie miał wskaźnik do nieprawidłowej pamięci (zwany to zwisającym wskaźnikiem), gdy spróbuje go użyć, rzeczy staną się owłosione.
Dlatego piszesz konstruktor kopii, aby przydzielał nowym obiektom własne fragmenty pamięci do zniszczenia.
Operator przypisania i konstruktor kopii
Przydzieliłeś pamięć w swoim konstruktorze wskaźnikowi członkowskiemu twojej klasy. Podczas kopiowania obiektu tej klasy domyślny operator przypisania i konstruktor kopiowania skopiują wartość tego wskaźnika elementu do nowego obiektu.
Oznacza to, że nowy obiekt i stary obiekt będą wskazywały na ten sam fragment pamięci, więc kiedy zmienisz go w jednym obiekcie, zostanie on również zmieniony na inny obiekt. Jeśli jeden obiekt usunie tę pamięć, drugi będzie próbował z niej skorzystać - np.
Aby rozwiązać ten problem, napisz własną wersję konstruktora kopiowania i operatora przypisania. Twoje wersje przydzielają osobną pamięć nowym obiektom i kopiują wartości, które wskazuje pierwszy wskaźnik, a nie jego adres.
Zasadniczo, jeśli masz destruktor (nie domyślny destruktor), oznacza to, że zdefiniowana klasa ma pewien przydział 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 niektóre prymitywne elementy o typie, działałby domyślny operator przypisania, ale jeśli ma on pewne elementy wskaźnikowe i obiekty, które nie mają operatorów przypisania, wynik byłby nieprzewidywalny. Dlatego możemy powiedzieć, że jeśli coś jest do usunięcia w destruktorze klasy, możemy potrzebować operatora głębokiego kopiowania, co oznacza, że powinniśmy udostępnić konstruktor kopiujący i operator przypisania.
Co oznacza kopiowanie obiektu? Istnieje kilka sposobów kopiowania obiektów - porozmawiajmy o 2 rodzajach, do których najprawdopodobniej masz na myśli - głębokiej i płytkiej kopii.
Ponieważ jesteśmy w języku zorientowanym obiektowo (lub przynajmniej tak zakładamy), powiedzmy, że masz przydzieloną pamięć. Ponieważ jest to język OO, możemy z łatwością odwoływać się do przydzielanych przez nas fragmentów pamięci, ponieważ są to zwykle prymitywne zmienne (int, znaki, bajty) lub zdefiniowane przez nas klasy, które są zbudowane z naszych własnych typów i prymitywów. Powiedzmy, że mamy klasę samochodów 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 ma miejsce, gdy zadeklarujemy obiekt, a następnie utworzymy całkowicie oddzielną kopię obiektu ... otrzymamy 2 obiekty w 2 kompletach 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 teraz coś dziwnego. Powiedzmy, że car2 jest źle zaprogramowany lub celowo ma na celu współdzielenie faktycznej pamięci, z której składa się car1. (Zazwyczaj jest to pomyłka, a na zajęciach zwykle jest to koc, o którym mowa.) Udawaj, że za każdym razem, gdy pytasz o car2, naprawdę rozwiązujesz wskaźnik do przestrzeni pamięci car1 ... to mniej więcej taka płytka kopia jest.
//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.*/
Bez względu na to, w jakim języku piszesz, bądź bardzo ostrożny, jeśli chodzi o kopiowanie obiektów, ponieważ przez większość czasu potrzebujesz głębokiej kopii.
Co to jest konstruktor kopii i operator przypisania kopii? Użyłem ich już powyżej. Konstruktor kopiowania jest wywoływany podczas wpisywania kodu, takiego jak Car car2 = car1;
Zasadniczo, jeśli deklarujesz zmienną i przypisujesz ją w jednym wierszu, wtedy wywoływany jest konstruktor kopiowania. Operator przypisania jest tym, co dzieje się, gdy używasz znaku równości-- car2 = car1;
. Powiadomienie car2
nie jest zadeklarowane w tym samym oświadczeniu. Dwie części 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 jesteś zadowolony, że początkowe kopiowanie / przypisanie jest uzasadnione - jeśli spojrzysz na napisany odręcznie kod, funkcje są prawie identyczne.
Kiedy sam muszę je zadeklarować? Jeśli nie piszesz kodu, który ma być w jakiś sposób udostępniany lub produkowany, naprawdę musisz go zadeklarować tylko wtedy, gdy jest potrzebny. Musisz zdawać sobie sprawę z tego, co robi Twój język programu, jeśli zdecydujesz się go użyć „przypadkowo”, ale go nie utworzyłeś - tzn. Otrzymasz domyślny kompilator. Na przykład rzadko używam konstruktorów kopiowania, ale przesłonięcia operatora przypisania są bardzo częste. Czy wiesz, że możesz pominąć znaczenie dodawania, odejmowania itp.?
Jak mogę zapobiec kopiowaniu moich obiektów? Zastąpienie wszystkich sposobów przydzielania pamięci dla obiektu za pomocą funkcji prywatnej jest rozsądnym początkiem. Jeśli naprawdę nie chcesz, aby ludzie je kopiowali, możesz upublicznić je i ostrzec programistę, zgłaszając wyjątek, a także nie kopiując obiektu.
Kiedy sam muszę je zadeklarować?
Reguła Trzech stanowi, że jeśli zadeklarujesz którykolwiek z
wtedy powinieneś zadeklarować wszystkie trzy. Wyrosło z obserwacji, że potrzeba przejęcia znaczenia operacji kopiowania prawie zawsze wynikała z klasy wykonującej pewnego rodzaju zarządzanie zasobami, a to prawie zawsze implikowało, że
cokolwiek zarządzanie zasobami było wykonywane w jednej operacji kopiowania, prawdopodobnie musiało być wykonane w drugiej operacji kopiowania i
niszczyciel klas uczestniczyłby również w zarządzaniu zasobem (zwykle zwalniając go). Klasycznym zasobem do zarządzania była pamięć i dlatego wszystkie klasy Biblioteki Standardowej, które zarządzają pamięcią (np. Kontenery STL, które wykonują dynamiczne zarządzanie pamięcią) wszystkie deklarują „wielką trójkę”: zarówno operacje kopiowania, jak i destruktor.
Konsekwencją reguły trzech jest to, że obecność destruktora zadeklarowanego przez użytkownika wskazuje, że prosta kopia mądrego elementu nie jest odpowiednia do operacji kopiowania w klasie. To z kolei sugeruje, że jeśli klasa zadeklaruje destruktor, operacje kopiowania prawdopodobnie nie powinny być generowane automatycznie, ponieważ nie zrobiłyby właściwej rzeczy. W momencie przyjęcia C ++ 98 znaczenie tego rozumowania nie zostało w pełni docenione, dlatego w C ++ 98 istnienie deklarowanego przez użytkownika destruktora 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 których generowane są operacje kopiowania, spowodowałoby uszkodzenie zbyt dużej ilości starszego kodu.
Jak mogę zapobiec kopiowaniu moich obiektów?
Zadeklaruj konstruktora kopiowania i operatora przypisania kopii jako prywatny specyfikator dostępu.
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 wersji C ++ 11 możesz także zadeklarować usunięcie konstruktora 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);
}
Wiele istniejących odpowiedzi już dotyczy konstruktora kopiowania, operatora przypisania i destruktora. Jednak w post C ++ 11 wprowadzenie semantyki ruchu może rozszerzyć tę liczbę poza 3.
Niedawno Michael Claisse wygłosił przemówienie na ten temat: http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class
Zasada trzech w C ++ jest podstawową zasadą projektowania i rozwijania trzech wymagań, że jeśli istnieje jednoznaczna definicja jednej z poniższych funkcji składowych, wówczas programista powinien zdefiniować funkcje pozostałych dwóch składowych razem. Niezbędne są następujące trzy funkcje składowe: destruktor, konstruktor kopii, operator przypisania kopii.
Konstruktor kopii w C ++ jest specjalnym konstruktorem. Służy do budowy nowego obiektu, który jest nowym obiektem równoważnym z kopią istniejącego obiektu.
Kopiuj operator przypisania jest specjalnym operatorem przypisania, który zwykle służy do określania istniejącego obiektu innym obiektom tego samego typu.
Istnieją 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;
c++-faq
wiki tag zanim głosować zamknąć .