ПрограммированиеC++ разработчик

Объясните разницу между shallow copy и deep copy для классов с динамической памятью. Когда и почему необходим глубокий конструктор копирования? Как реализовать deep copy вручную?

Проходите собеседования с ИИ помощником Hintsage

Ответ.

История вопроса

В C++ изначально при копировании объектов используется механизм memberwise copying: для каждого члена объекта вызывается собственная операция копирования. Для встроенных типов это безопасно, но для динамических ресурсов возникает проблема: копируются только указатели, но не сами данные.

Проблема

Если объект содержит указатель на выделенную в heap память, то после копирования двух объектов они будут указывать на одну и ту же область памяти. Тогда при уничтожении одного объекта память освободится и указатель второго станет невалидным ("дикий" указатель). Это приводит к ошибкам времени выполнения и утечкам.

Решение

Чтобы копия была независимой, необходим deep copy — побайтовое копирование и выделение собственного буфера. Это реализуется путём написания пользовательского конструктора копирования и оператора присваивания.

Пример кода:

class MyString { char* data; public: MyString(const char* s) { data = new char[strlen(s)+1]; strcpy(data, s); } // Deep copy constructor MyString(const MyString& src) { data = new char[strlen(src.data) + 1]; strcpy(data, src.data); } // Deep copy assignment 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; } };

Ключевые особенности:

  • Требуется для классов, управляющих внешними ресурсами
  • Необходима реализация собственных copy constructor и operator=
  • Возможны утечки памяти без правильной реализации Rule of Three

Вопросы с подвохом.

Зачем нужен пользовательский деструктор, если класс содержит только указатель, но память не выделяется?

Деструктор необходим только если вы явно выделяли память (или иной ресурс) внутри класса. Если указатель не выделяет память, дефолтного деструктора достаточно.


Что произойдет, если не реализовать operator= в классе с динамической памятью, но конструктор копирования объявлен?

Если вы определили конструктор копирования вручную, compiler не реализует operator= автоматически; будет либо объявлен неявно, либо компилятор выдаст ошибку/предупреждение (зависит от стандарта). Это приведёт к плохо определённому поведению при присваивании: произойдёт memberwise copy и возникнет double free или утечка.

Пример кода:

MyString a("hi"); MyString b = a; // Ok: ваш копирующий конструктор MyString c("bye"); c = a; // Problem! Если operator= не реализован вручную, будет shallow copy

Чем опасно присваивание самому себе при ручной реализации operator=?

Если делить ресурсы, не проверяя this!=&rhs, то при присваивании самому себе будет выполнено delete[] data, а потом копирование уже уничтоженного массива, что приводит к segfault. Самозащита: всегда проверяйте self-assignment.

if (this != &rhs) { ... }

Типовые ошибки и анти-паттерны

  • Не реализован один из Rule of Three методов (конструктор копирования, operator=, деструктор)
  • Не проверяется self-assignment
  • Утечка памяти при забытом delete[]
  • Double free если указатели оказались общими

Пример из жизни

Негативный кейс

Разработчик копирует объект класса со встроенным указателем, не реализуя deep copy. После копирования несколько объектов разделяют одну область памяти. Два деструктора вызывают double delete, падает программа.

Плюсы:

  • Просто написать код («копирование работает» на первый взгляд)

Минусы:

  • Краш программы, непредсказуемые баги
  • Утечки памяти или double-free

Позитивный кейс

Разработчик грамотно реализует конструктор копирования, оператор присваивания и деструктор. Каждый объект владеет своей памятью.

Плюсы:

  • Безопасность при копировании и уничтожении
  • Нет утечек

Минусы:

  • При большом количестве копий повышается overhead копирования