programowanieProgramista C++

Wyjaśnij różnicę między shallow copy a deep copy dla klas z dynamiczną pamięcią. Kiedy i dlaczego potrzebny jest głęboki konstruktor kopiowania? Jak ręcznie zrealizować deep copy?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź.

Historia pytania

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.

Problem

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.

Rozwiązanie

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:

  • Wymagane dla klas zarządzających zewnętrznymi zasobami
  • Niezbędna implementacja własnych copy constructor i operator=
  • Możliwe wycieki pamięci bez prawidłowej realizacji Zasady Trzech

Pytania podchwytliwe.

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) { ... }

Typowe błędy i antywzorce

  • Nie zaimplementowano jednej z metod Zasady Trzech (konstruktor kopiowania, operator=, destruktor)
  • Nie sprawdzano self-assignment
  • Wyciek pamięci zapomnianego delete[]
  • Double free jeśli wskaźniki okazały się wspólne

Przykład z życia

Negatywny przypadek

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:

  • Łatwo napisać kod („kopiowanie działa” na pierwszy rzut oka)

Wady:

  • Zawieszenie programu, nieprzewidywalne błędy
  • Wyciek pamięci lub double-free

Pozytywny przypadek

Programista poprawnie realizuje konstruktor kopiowania, operator przypisania i destruktor. Każdy obiekt ma swoją pamięć.

Zalety:

  • Bezpieczeństwo podczas kopiowania i niszczenia
  • Brak wycieków

Wady:

  • Przy dużej liczbie kopii zwiększa się narzut kopiowania