Wiem, że kompilator C ++ tworzy konstruktor kopiujący dla klasy. W takim przypadku musimy napisać konstruktor kopiujący zdefiniowany przez użytkownika? Czy możesz podać kilka przykładów?
Wiem, że kompilator C ++ tworzy konstruktor kopiujący dla klasy. W takim przypadku musimy napisać konstruktor kopiujący zdefiniowany przez użytkownika? Czy możesz podać kilka przykładów?
Odpowiedzi:
Konstruktor kopiujący wygenerowany przez kompilator wykonuje kopiowanie według elementów członkowskich. Czasami to nie wystarcza. Na przykład:
class Class {
public:
Class( const char* str );
~Class();
private:
char* stored;
};
Class::Class( const char* str )
{
stored = new char[srtlen( str ) + 1 ];
strcpy( stored, str );
}
Class::~Class()
{
delete[] stored;
}
w tym przypadku kopiowanie stored
elementu składowego nie spowoduje zduplikowania bufora (skopiowany zostanie tylko wskaźnik), więc pierwsza zniszczona kopia udostępniająca bufor zostanie delete[]
pomyślnie wywołana, a druga będzie działać w niezdefiniowanym zachowaniu. Potrzebujesz konstruktora kopiującego do głębokiego kopiowania (oraz operatora przypisania).
Class::Class( const Class& another )
{
stored = new char[strlen(another.stored) + 1];
strcpy( stored, another.stored );
}
void Class::operator = ( const Class& another )
{
char* temp = new char[strlen(another.stored) + 1];
strcpy( temp, another.stored);
delete[] stored;
stored = temp;
}
delete stored[];
i uważam, że powinno byćdelete [] stored;
std::string
. Ogólna idea jest taka, że tylko klasy narzędziowe, które zarządzają zasobami, muszą przeciążać Wielką Trójkę, a wszystkie inne klasy powinny po prostu używać tych klas narzędzi, eliminując potrzebę definiowania którejkolwiek z Wielkiej Trójki.
Trochę się denerwuję, że Rule of Five
nie przytoczono zasady z .
Ta zasada jest bardzo prosta:
Zasada pięciu : za
każdym razem, gdy piszesz jeden z Destructor, Copy Constructor, Copy Assignment Operator, Move Constructor lub Move Assignment Operator, prawdopodobnie będziesz musiał napisać pozostałe cztery.
Jest jednak bardziej ogólna wskazówka, której należy przestrzegać, która wynika z potrzeby pisania kodu bezpiecznego dla wyjątków:
Każdy zasób powinien być zarządzany przez dedykowany obiekt
Tutaj @sharptooth
kod jest nadal (w większości) w porządku, jednak gdyby miał dodać drugi atrybut do swojej klasy, to by tak nie było. Rozważ następującą klasę:
class Erroneous
{
public:
Erroneous();
// ... others
private:
Foo* mFoo;
Bar* mBar;
};
Erroneous::Erroneous(): mFoo(new Foo()), mBar(new Bar()) {}
Co się stanie, jeśli new Bar
rzuci? Jak usunąć wskazany obiekt mFoo
? Istnieją rozwiązania (try / catch ...), po prostu się nie skalują.
Właściwym sposobem radzenia sobie z tą sytuacją jest użycie odpowiednich klas zamiast surowych wskaźników.
class Righteous
{
public:
private:
std::unique_ptr<Foo> mFoo;
std::unique_ptr<Bar> mBar;
};
Dzięki tej samej implementacji konstruktora (a właściwie używaniu make_unique
) mam teraz wyjątek bezpieczeństwa za darmo !!! Czy to nie jest ekscytujące? A co najważniejsze, nie muszę już martwić się o odpowiedni destruktor! Muszę napisać własne Copy Constructor
i Assignment Operator
choć, bo unique_ptr
nie definiuje tych operacji ... ale to nie ma znaczenia;)
Dlatego sharptooth
ponownie odwiedzono klasę:
class Class
{
public:
Class(char const* str): mData(str) {}
private:
std::string mData;
};
Nie wiem jak Ty, ale moje jest dla mnie łatwiejsze;)
Mogę przypomnieć sobie z mojej praktyki i pomyśleć o następujących przypadkach, w których mamy do czynienia z jawnym zadeklarowaniem / zdefiniowaniem konstruktora kopiującego. Pogrupowałem sprawy w dwie kategorie
W tej sekcji umieszczam przypadki, w których zadeklarowanie / zdefiniowanie konstruktora kopiującego jest niezbędne do poprawnego działania programów używających tego typu.
Po przeczytaniu tej sekcji dowiesz się o kilku pułapkach związanych z zezwoleniem kompilatorowi na samodzielne wygenerowanie konstruktora kopiującego. Dlatego, jak zauważył Seand w swojej odpowiedzi , zawsze można bezpiecznie wyłączyć kopiowalność dla nowej klasy i celowo włączyć ją później, gdy jest to naprawdę potrzebne.
Zadeklaruj prywatny konstruktor kopiujący i nie dostarczaj dla niego implementacji (aby kompilacja nie powiodła się na etapie łączenia, nawet jeśli obiekty tego typu są kopiowane we własnym zakresie klasy lub przez jej znajomych).
Zadeklaruj konstruktora kopiującego z =delete
at end.
Jest to najlepiej zrozumiany przypadek i właściwie jedyny wymieniony w innych odpowiedziach. shaprtooth został pokryty go całkiem dobrze. Chcę tylko dodać, że głębokie kopiowanie zasobów, które powinny być wyłączną własnością obiektu, może dotyczyć dowolnego typu zasobów, z których dynamicznie przydzielana pamięć jest tylko jednym rodzajem. W razie potrzeby może również wymagać głębokiego skopiowania obiektu
Rozważ klasę, w której wszystkie obiekty - bez względu na to, jak zostały zbudowane - MUSZĄ zostać w jakiś sposób zarejestrowane. Kilka przykładów:
Najprostszy przykład: utrzymywanie całkowitej liczby aktualnie istniejących obiektów. Rejestracja obiektu polega tylko na zwiększaniu licznika statycznego.
Bardziej złożonym przykładem jest pojedynczy rejestr, w którym przechowywane są odwołania do wszystkich istniejących obiektów tego typu (tak, aby powiadomienia mogły być dostarczane do wszystkich z nich).
Inteligentne wskaźniki liczone jako referencje można uznać za szczególny przypadek w tej kategorii: nowy wskaźnik „rejestruje się” we współdzielonym zasobie, a nie w rejestrze globalnym.
Taka operacja samodzielnej rejestracji musi zostać wykonana przez DOWOLNEGO konstruktora typu, a konstruktor kopiujący nie jest wyjątkiem.
Niektóre obiekty mogą mieć nietrywialną strukturę wewnętrzną z bezpośrednimi odsyłaczami między ich różnymi podobiektami (w rzeczywistości wystarczy jeden taki wewnętrzny odsyłacz, aby wywołać ten przypadek). Konstruktor kopiujący dostarczony przez kompilator przerwie wewnętrzne powiązania między obiektami , konwertując je na skojarzenia między obiektami .
Przykład:
struct MarriedMan;
struct MarriedWoman;
struct MarriedMan {
// ...
MarriedWoman* wife; // association
};
struct MarriedWoman {
// ...
MarriedMan* husband; // association
};
struct MarriedCouple {
MarriedWoman wife; // aggregation
MarriedMan husband; // aggregation
MarriedCouple() {
wife.husband = &husband;
husband.wife = &wife;
}
};
MarriedCouple couple1; // couple1.wife and couple1.husband are spouses
MarriedCouple couple2(couple1);
// Are couple2.wife and couple2.husband indeed spouses?
// Why does couple2.wife say that she is married to couple1.husband?
// Why does couple2.husband say that he is married to couple1.wife?
Mogą istnieć klasy, w których obiekty można bezpiecznie kopiować w jakimś stanie (np. Domyślny stan skonstruowany) i nie można ich kopiować w inny sposób. Jeśli chcemy zezwolić na kopiowanie obiektów bezpiecznych do kopiowania, to - jeśli programujemy defensywnie - potrzebujemy sprawdzenia w czasie wykonywania w konstruktorze kopiującym zdefiniowanym przez użytkownika.
Czasami klasa, która powinna być kopiowalna, agreguje niepodlegające kopiowaniu podobiekty. Zwykle dzieje się tak w przypadku obiektów ze stanem nieobserwowalnym (ten przypadek jest omówiony bardziej szczegółowo w sekcji „Optymalizacja” poniżej). Kompilator jedynie pomaga rozpoznać ten przypadek.
Klasa, która powinna być kopiowalna, może agregować podobiekt typu quasi-kopiowalnego. Typ quasi-kopiowalny nie zapewnia konstruktora kopiującego w ścisłym tego słowa znaczeniu, ale ma inny konstruktor, który pozwala na utworzenie koncepcyjnej kopii obiektu. Przyczyną tworzenia typu quasi-kopiowalnego jest brak pełnej zgody co do semantyki kopiowania typu.
Na przykład, ponownie analizując przypadek samodzielnej rejestracji obiektu, możemy argumentować, że mogą wystąpić sytuacje, w których obiekt musi być zarejestrowany w globalnym menedżerze obiektów tylko wtedy, gdy jest to kompletny samodzielny obiekt. Jeśli jest to podobiekt innego obiektu, odpowiedzialność za zarządzanie nim spoczywa na obiekcie zawierającym go.
Lub musi być obsługiwane zarówno płytkie, jak i głębokie kopiowanie (żadne z nich nie jest domyślne).
Ostateczna decyzja należy wtedy do użytkowników tego typu - kopiując obiekty, muszą oni jednoznacznie określić (poprzez dodatkowe argumenty) zamierzony sposób kopiowania.
W przypadku nieobronnego podejścia do programowania możliwe jest również, że obecny jest zarówno zwykły konstruktor kopiujący, jak i konstruktor quasi-kopiujący. Może to być uzasadnione, gdy w zdecydowanej większości przypadków należy zastosować jedną metodę kopiowania, aw rzadkich, ale dobrze zrozumiałych sytuacjach, należy zastosować alternatywne metody kopiowania. Wówczas kompilator nie będzie narzekał, że nie może niejawnie zdefiniować konstruktora kopiującego; wyłączną odpowiedzialnością użytkowników będzie zapamiętanie i sprawdzenie, czy podobiekt tego typu powinien zostać skopiowany za pomocą konstruktora quasi-kopiującego.
W rzadkich przypadkach podzbiór obserwowalnego stanu obiektu może stanowić (lub być uważany) za nieodłączną część tożsamości obiektu i nie powinien być przenoszony na inne obiekty (chociaż może to być nieco kontrowersyjne).
Przykłady:
UID obiektu (ale ten również należy do przypadku "samorejestracji" z góry, ponieważ identyfikator należy uzyskać w akcie samorejestracji).
Historia obiektu (np. Stos Cofnij / Ponów) w przypadku, gdy nowy obiekt nie może dziedziczyć historii obiektu źródłowego, ale zamiast tego rozpoczynać się od pojedynczego elementu historii „ Skopiowano o <CZAS> z <OTHER_OBJECT_ID> ”.
W takich przypadkach konstruktor kopiujący musi pominąć kopiowanie odpowiednich podobiektów.
Podpis konstruktora kopiującego dostarczonego przez kompilator zależy od tego, jakie konstruktory kopiujące są dostępne dla podobiektów. Jeśli co najmniej jeden podobiekt nie ma prawdziwego konstruktora kopiującego (pobierającego obiekt źródłowy przez stałe odniesienie), ale zamiast tego ma mutującego konstruktora kopiującego (pobierającego obiekt źródłowy przez niestałe odniesienie), kompilator nie będzie miał wyboru ale niejawnie zadeklarować, a następnie zdefiniować mutujący konstruktor kopiujący.
A co, jeśli „mutujący” konstruktor kopiujący typu podobiektu w rzeczywistości nie mutuje obiektu źródłowego (i został po prostu napisany przez programistę, który nie wie o const
słowie kluczowym)? Jeśli nie możemy naprawić tego kodu przez dodanie brakującego const
, to inną opcją jest zadeklarowanie własnego konstruktora kopiującego zdefiniowanego przez użytkownika z poprawnym podpisem i popełnienie grzechu przejścia na plik const_cast
.
Kontener COW, który podaje bezpośrednie odniesienia do swoich danych wewnętrznych, MUSI zostać głęboko skopiowany w czasie konstruowania, w przeciwnym razie może zachowywać się jak uchwyt zliczający odniesienia.
Chociaż COW jest techniką optymalizacji, ta logika w konstruktorze kopiującym jest kluczowa dla jej poprawnej implementacji. Dlatego umieściłem ten przypadek tutaj, a nie w sekcji „Optymalizacja”, do której przechodzimy dalej.
W następujących przypadkach możesz chcieć / chcieć zdefiniować własny konstruktor kopiujący z powodów związanych z optymalizacją:
Rozważmy kontener obsługujący operacje usuwania elementów, ale można to zrobić, po prostu oznaczając usunięty element jako usunięty, a następnie ponownie wykorzystaj jego gniazdo. Kiedy tworzona jest kopia takiego kontenera, sensowne może być skompaktowanie zachowanych danych zamiast zachowania „usuniętych” gniazd w takiej postaci, w jakiej są.
Obiekt może zawierać dane, które nie są częścią jego obserwowalnego stanu. Zwykle są to buforowane / zapamiętywane dane gromadzone przez cały okres istnienia obiektu w celu przyspieszenia niektórych powolnych operacji zapytań wykonywanych przez obiekt. Można bezpiecznie pominąć kopiowanie tych danych, ponieważ zostaną one ponownie obliczone, gdy (i jeśli!) Zostaną wykonane odpowiednie operacje. Kopiowanie tych danych może być nieuzasadnione, ponieważ może zostać szybko unieważnione, jeśli obserwowalny stan obiektu (z którego pochodzą dane z pamięci podręcznej) zostanie zmodyfikowany przez operacje mutacyjne (a jeśli nie zamierzamy modyfikować obiektu, to dlaczego tworzymy głęboki skopiować?)
Optymalizacja ta jest uzasadniona tylko wtedy, gdy dane pomocnicze są duże w porównaniu z danymi reprezentującymi obserwowalny stan.
C ++ umożliwia wyłączenie niejawnego kopiowania poprzez zadeklarowanie konstruktora kopiującego explicit
. Wówczas obiekty tej klasy nie mogą być przekazywane do funkcji i / lub zwracane z funkcji przez wartość. Ta sztuczka może być użyta dla typu, który wydaje się lekki, ale w rzeczywistości jest bardzo drogi do skopiowania (chociaż uczynienie go quasi-kopiowalnym może być lepszym wyborem).
W C ++ 03 zadeklarowanie konstruktora kopiującego również wymagało zdefiniowania go (oczywiście, jeśli zamierzałeś go używać). W związku z tym wybranie takiego konstruktora kopiującego tylko z omawianego problemu oznaczało, że trzeba było napisać ten sam kod, który kompilator wygeneruje automatycznie.
C ++ 11 i nowsze standardy pozwalają na deklarowanie specjalnych funkcji składowych (konstruktorów domyślnych i kopiujących, operatora przypisania kopii i destruktora) z jawnym żądaniem użycia domyślnej implementacji (po prostu zakończ deklarację
=default
).
Tę odpowiedź można poprawić w następujący sposób:
- Dodaj więcej przykładowego kodu
- Zilustruj przypadek „Obiekty z wewnętrznymi odsyłaczami”
- Dodaj linki
Jeśli masz klasę, która ma dynamicznie przydzielaną zawartość. Na przykład przechowujesz tytuł książki jako znak * i ustawiasz tytuł na nowy, kopiowanie nie będzie działać.
Musiałbyś napisać konstruktora kopiującego, który to robi, title = new char[length+1]
a potem strcpy(title, titleIn)
. Konstruktor kopiujący wykonałby po prostu „płytką” kopię.
Copy Constructor jest wywoływana, gdy obiekt jest przekazywany przez wartość, zwracany przez wartość lub jawnie kopiowany. Jeśli nie ma konstruktora kopiującego, c ++ tworzy domyślny konstruktor kopiujący, który tworzy płytką kopię. Jeśli obiekt nie ma wskaźników do dynamicznie przydzielanej pamięci, zrobi to płytka kopia.
Często dobrym pomysłem jest wyłączenie ctor kopiowania i operator =, chyba że klasa wyraźnie tego potrzebuje. Może to zapobiec nieefektywności, takim jak przekazywanie argumentu przez wartość, gdy jest zamierzone odniesienie. Również metody wygenerowane przez kompilator mogą być nieprawidłowe.
Rozważmy poniższy fragment kodu:
class base{
int a, *p;
public:
base(){
p = new int;
}
void SetData(int, int);
void ShowData();
base(const base& old_ref){
//No coding present.
}
};
void base :: ShowData(){
cout<<this->a<<" "<<*(this->p)<<endl;
}
void base :: SetData(int a, int b){
this->a = a;
*(this->p) = b;
}
int main(void)
{
base b1;
b1.SetData(2, 3);
b1.ShowData();
base b2 = b1; //!! Copy constructor called.
b2.ShowData();
return 0;
}
Output:
2 3 //b1.ShowData();
1996774332 1205913761 //b2.ShowData();
b2.ShowData();
daje niepotrzebne dane wyjściowe, ponieważ istnieje konstruktor kopiujący zdefiniowany przez użytkownika, który nie został napisany w celu jawnego kopiowania danych. Więc kompilator nie tworzy tego samego.
Pomyślałem o podzieleniu się tą wiedzą z wami wszystkimi, chociaż większość z was już to wie.
Pozdrawiam ... Miłego kodowania !!!