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

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

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

Ответ.

Механизм копирования объектов в C++ делится на поверхностное копирование (shallow copy) и глубокое копирование (deep copy). Разница особенно важна для классов с динамически выделяемой памятью.

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

В C++ многие структуры данных работают с динамической памятью (new/delete). По умолчанию компилятор генерирует конструктор копирования и оператор присваивания, совершающий копирование побайтово (shallow copy). Это быстро, но опасно, если объект управляет внешними ресурсами.

Проблема

Shallow copy копирует только адреса динамически выделенных ресурсов. При удалении одного объекта память будет освобождена, а другой экземпляр останется с "висячим" (dangling) указателем. В результате возникают double delete, утечки памяти и сбои.

Решение

Глубокое копирование задействует явное создание копии всех динамических ресурсов. Для этого в классе необходимо самостоятельно реализовать конструктор копирования и оператор присваивания, чтобы обеспечивать копию каждого элемента.

Пример кода для класса с массивом:

class DynArray { int* data; size_t size; public: DynArray(size_t n) : size(n), data(new int[n]) {} ~DynArray() { delete[] data; } // Deep copy constructor 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]; } // Deep copy assignment 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; } };

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

  • Shallow copy копирует указатели, deep copy создаёт новые экземпляры динамической памяти.
  • Для deep copy требуется реализовывать свою логику копирования в конструкторе и операторе=
  • Игнорирование необходимости deep copy приводит к сложно отлавливаемым багам.

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

Компилятор всегда корректно генерирует copy constructor и assignment operator, верно?

Ответ:

Неверно. Для классов с динамическими ресурсами копирование по умолчанию некорректно: оба объекта будут владеть одним и тем же ресурсом. Deep copy необходимо реализовывать явно при владении внешними ресурсами.

Нужно ли реализовывать деструктор, если написан only deep copy constructor/assignment?

Ответ:

Да, иначе возникнет утечка памяти: если освобождать память в пользовательском конструкторе копирования, но не реализовать деструктор, память никем не будет освобождаться при разрушении объектов.

Может ли std::vector хранить указатели и почему при его копировании возможны утечки?

Ответ:

Да, std::vector спокойно хранит указатели. При копировании такого std::vector копируются сами указатели, а не объекты, на которые они указывают. Это shallow copy: если нужно deep copy всего содержимого, потребуется вручную копировать каждый объект и размещать их в памяти независимо.

Пример:

std::vector<int*> v1; v1.push_back(new int(42)); std::vector<int*> v2 = v1; // Копируются указатели, а не *int

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

  • Игнорировать необходимость реализовать Rule of Three.
  • Копировать указатели, полагая, что это копия объекта.
  • Не освобождать динамические ресурсы в деструкторе.
  • Использовать shallow copy для классов с владеемыми ресурсами.

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

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

Программист реализует класс-обёртку массива, не переопределяя конструктор копирования/оператор присваивания. В результате оба объекта владеют одной памятью, destroy одного приводит к краху при доступе ко второму.

Плюсы:

  • Быстро работает (нет копий).

Минусы:

  • Очень сложно отлавливаемые ошибки runtime; наличие double free/segfault.

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

Разработчик реализует глубокое копирование: копируются содержимое массива, есть свой деструктор и оператор присваивания с защитой от self-assignment.

Плюсы:

  • Безопасное копирование и освобождение памяти.
  • Код сопровождаем и расширяем.

Минусы:

  • Немного больше кода и расходов памяти.
  • Сложнее для классов с несколькими динамическими ресурсами.