W C++ do kopiowania obiektów z reguły używa się mechanizmu memberwise copying: dla każdego członu obiektu wywoływana jest własna operacja kopiowania. Dla wbudowanych typów jest to bezpieczne, ale dla dynamicznych zasobów występuje problem: kopiowane są tylko wskaźniki, a nie same dane.
Jeśli obiekt zawiera wskaźnik do pamięci przydzielonej w heapie, to po skopiowaniu dwóch obiektów będą one wskazywać na ten sam obszar pamięci. Wówczas, przy zniszczeniu jednego obiektu, pamięć zostanie zwolniona, a wskaźnik drugiego stanie się nieprawidłowy ("dziki" wskaźnik). Prowadzi to do błędów w czasie wykonywania i wycieków pamięci.
Aby kopia była niezależna, potrzebny jest deep copy — kopiowanie bajtowe i przydzielenie własnego bufora. Realizuje się to poprzez napisanie niestandardowego konstruktora kopiowania i operatora przypisania.
Przykład kodu:
class MyString { char* data; public: MyString(const char* s) { data = new char[strlen(s)+1]; strcpy(data, s); } // Głęboki konstruktor kopiowania MyString(const MyString& src) { data = new char[strlen(src.data) + 1]; strcpy(data, src.data); } // Głęboki operator przypisania MyString& operator=(const MyString& src) { if (this != &src) { delete[] data; data = new char[strlen(src.data) + 1]; strcpy(data, src.data); } return *this; } ~MyString() { delete[] data; } };
Kluczowe cechy:
Po co potrzebny jest niestandardowy destruktor, jeśli klasa zawiera tylko wskaźnik, ale pamięć nie jest przydzielona?
Destruktor jest potrzebny tylko jeśli jawnie przydzielano pamięć (lub inny zasób) wewnątrz klasy. Jeśli wskaźnik nie przydziela pamięci, domyślny destruktor jest wystarczający.
Co się stanie, jeśli nie zaimplementujesz operator= w klasie z dynamiczną pamięcią, ale konstruktor kopiowania jest zadeklarowany?
Jeśli ręcznie zdefiniujesz konstruktor kopiowania, kompilator nie zaimplementuje operator= automatycznie; zostanie albo zadeklarowany niejawnie, albo kompilator wyświetli błąd/ostrzeżenie (zależy od standardu). Prowadzi to do źle zdefiniowanego zachowania przy przypisaniu: nastąpi memberwise copy i wystąpi double free lub wyciek.
Przykład kodu:
MyString a("hi"); MyString b = a; // Ok: twój konstruktor kopiujący MyString c("bye"); c = a; // Problem! Jeśli operator= nie został zaimplementowany ręcznie, nastąpi shallow copy
Jakie są niebezpieczeństwa przypisywania do samego siebie podczas ręcznej realizacji operator=?
Jeśli dzielisz zasoby, nie sprawdzając this!=&rhs, to przy przypisaniu do samego siebie nastąpi delete[] data, a następnie kopiowanie już zniszczonej tablicy, co prowadzi do segfaulta. Samozabezpieczenie: zawsze sprawdzaj self-assignment.
if (this != &rhs) { ... }
Programista kopiuje obiekt klasy z wbudowanym wskaźnikiem, nie realizując deep copy. Po skopiowaniu kilka obiektów dzieli tę samą przestrzeń pamięci. Dwa destruktory wywołują double delete, program się zawiesza.
Zalety:
Wady:
Programista poprawnie realizuje konstruktor kopiowania, operator przypisania i destruktor. Każdy obiekt ma swoją pamięć.
Zalety:
Wady: