Mechanizm kopiowania obiektów w C++ dzieli się na powierzchowne kopiowanie (shallow copy) i głębokie kopiowanie (deep copy). Różnica ma szczególne znaczenie dla klas z dynamicznie przydzielaną pamięcią.
W C++ wiele struktur danych działa z dynamiczną pamięcią (new/delete). Domyślnie kompilator generuje konstruktor kopiowania i operator przypisania, który dokonuje kopiowania bajtowego (shallow copy). To szybkie, ale niebezpieczne, jeśli obiekt zarządza zewnętrznymi zasobami.
Shallow copy kopiuje tylko adresy dynamicznie przydzielonych zasobów. Po usunięciu jednego obiektu pamięć zostanie zwolniona, a inny egzemplarz pozostanie z "zwisającym" (dangling) wskaźnikiem. W rezultacie występują podwójne usunięcia, wycieki pamięci i awarie.
Głębokie kopiowanie wymaga jawnego utworzenia kopii wszystkich dynamicznych zasobów. W tym celu w klasie należy samodzielnie zaimplementować konstruktor kopiowania i operator przypisania, aby zapewnić kopię każdego elementu.
Przykład kodu dla klasy z tablicą:
class DynArray { int* data; size_t size; public: DynArray(size_t n) : size(n), data(new int[n]) {} ~DynArray() { delete[] data; } // Konstruktor głębokiego kopiowania DynArray(const DynArray& other) : size(other.size), data(new int[other.size]) { for (size_t i = 0; i < size; ++i) data[i] = other.data[i]; } // Operator głębokiego kopiowania DynArray& operator=(const DynArray& other) { if (this != &other) { delete[] data; size = other.size; data = new int[size]; for (size_t i = 0; i < size; ++i) data[i] = other.data[i]; } return *this; } };
Kluczowe cechy:
Kompilator zawsze poprawnie generuje copy constructor i operator przypisania, prawda?
Odpowiedź:
Nieprawda. Dla klas z dynamicznymi zasobami kopiowanie domyślnie jest niepoprawne: oba obiekty będą posiadały jeden i ten sam zasób. Głębokie kopiowanie należy zaimplementować jawnie przy posiadaniu zewnętrznych zasobów.
Czy należy implementować destruktor, jeśli napisano tylko konstruktor/operador głębokiego kopiowania?
Odpowiedź:
Tak, w przeciwnym razie wystąpi wyciek pamięci: jeśli zwolnisz pamięć w niestandardowym konstruktorze kopiowania, ale nie zaimplementujesz destruktora, pamięć nie będzie zwalniana przy zniszczeniu obiektów.
Czy std::vector może przechowywać wskaźniki i dlaczego przy jego kopiowaniu mogą wystąpić wycieki?
Odpowiedź:
Tak, std::vector spokojnie przechowuje wskaźniki. Podczas kopiowania takiego std::vector kopiowane są same wskaźniki, a nie obiekty, na które wskazują. To jest shallow copy: jeśli potrzebna jest głęboka kopia całej zawartości, potrzeba ręcznie skopiować każdy obiekt i umieścić je w pamięci niezależnie.
Przykład:
std::vector<int*> v1; v1.push_back(new int(42)); std::vector<int*> v2 = v1; // Kopiowane są wskaźniki, a nie *int
Programista implementuje klasę-opakowanie dla tablicy, nie nadpisując konstruktora kopiowania/operatora przypisania. W rezultacie oba obiekty posiadają jedną pamięć, zniszczenie jednego prowadzi do awarii przy dostępie do drugiego.
Zalety:
Wady:
Programista implementuje głębokie kopiowanie: kopiowane są zawartości tablicy, jest własny destruktor oraz operator przypisania z ochroną przed self-assignment.
Zalety:
Wady: