- 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?
c++
copy-constructor
assignment-operator
c++-faq
rule-of-three
fredoverflow
źródło
źródło
c++-faq
wiki tag zanim głosować zamknąć .Odpowiedzi:
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:
(Jeśli zastanawia Cię ta
name(name), age(age)
część, nazywa się to listą inicjującą członka ).Specjalne funkcje członka
Co to znaczy skopiować
person
obiekt? Tamain
funkcja pokazuje dwa różne scenariusze kopiowania. Inicjalizacjaperson b(a);
jest wykonywana przez konstruktor kopii . Jego zadaniem jest konstruowanie świeżego obiektu na podstawie stanu istniejącego obiektu. Przypisanieb = 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:
Domyślnie kopiowanie obiektu oznacza kopiowanie jego elementów:
Domniemane definicje
Domyślnie zdefiniowane specjalne funkcje
person
składowe dla wyglądają następująco:Kopiowanie według członków jest dokładnie tym, czego chcemy w tym przypadku:
name
iage
są kopiowane, więc otrzymujemy niezależny, niezależnyperson
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ńczeniuperson
destruktora:Zarządzanie zasobami
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: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.Jawne definicje
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:
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 formularzax = x
. Bez tej kontroli,delete[] name
by usunąć tablicę zawierającą źródłowy łańcuch, bo kiedy piszeszx = x
, zarównothis->name
ithat.name
zawierają ten sam wskaźnik.Wyjątkowe bezpieczeństwo
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: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.
Zasoby, których nie można kopiować
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:Możesz też dziedziczyć je
boost::noncopyable
lub zadeklarować jako usunięte (w C ++ 11 i nowszych):Zasada trzech
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 :
(Niestety, ta „reguła” nie jest egzekwowana przez standard C ++ ani żaden kompilator, o którym wiem.)
Zasada pięciu
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:
Reguła zerowa
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.
Rada
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ą achar*
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.źródło
Reguła Trzech jest zasada dla C ++, w zasadzie mówi
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).
źródło
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.
źródło
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.
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.
źródło
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:
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.
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.
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;
. Powiadomieniecar2
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.
źródło
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.
Zadeklaruj konstruktora kopiowania i operatora przypisania kopii jako prywatny specyfikator dostępu.
W wersji C ++ 11 możesz także zadeklarować usunięcie konstruktora i operatora przypisania
źródło
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
źródło
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:
źródło