базовые концепции ООП — копированиеbaseline OOP concepts — copying
Копирование в C++ — не встроенная магия, а такой же вызываемый код, как любая функция: copy-конструктор и copy-присваивание. Их неявная генерация компилятором удобна, но именно она стоит за одной из самых частых на собеседовании ошибок — слайсингом.Copying in C++ isn't built-in magic — it's callable code just like any function: the copy constructor and copy assignment operator. Their implicit generation by the compiler is convenient, but it's exactly what's behind one of the most common interview mistakes — slicing.
Copy-конструктор (T(const T&)) создаёт новый объект как копию существующего. Copy-присваивание (T& operator=(const T&)) заменяет содержимое уже существующего объекта содержимым другого. Если ни один из членов пятёрки специальных функций не объявлен явно, компилятор генерирует оба неявно — почленное копирование каждого поля.The copy constructor (T(const T&)) creates a new object as a copy of an existing one. Copy assignment (T& operator=(const T&)) replaces an already-existing object's contents with another's. If none of the special member functions are declared explicitly, the compiler implicitly generates both — a member-wise copy of every field.
struct Point { int x, y; };
Point a{1, 2};
Point b = a; // copy-конструктор: b.x=1, b.y=2, независимая копия// copy constructor: b.x=1, b.y=2, an independent copy
Point c;
c = a; // copy-присваивание: c уже существовал, его поля перезаписаны// copy assignment: c already existed, its fields are overwritten
Почленное копирование корректно для типов-значений (int, string, vector — они сами умеют копироваться правильно), но некорректно, если класс владеет ресурсом через сырой указатель: копируется значение указателя, а не то, на что он указывает — оба объекта начинают владеть ОДНИМ и тем же ресурсом, и первый же деструктор освобождает его для обоих (double free при разрушении второго).Member-wise copying is correct for value types (int, string, vector — they already know how to copy themselves correctly), but incorrect if the class owns a resource through a raw pointer: the pointer's VALUE gets copied, not what it points to — both objects end up owning the SAME resource, and the first destructor to run frees it for both (a double free when the second is destroyed).
class Buffer {
int* data;
public:
Buffer(size_t n) : data(new int[n]) {}
~Buffer() { delete[] data; }
// copy-конструктор и copy= НЕ объявлены — компилятор генерирует почленное копирование// copy constructor and copy= are NOT declared — the compiler generates member-wise copying
};
Buffer a(10);
Buffer b = a; // b.data == a.data — один и тот же массив, два "владельца"// b.data == a.data — the same array, two "owners"
// оба деструктора вызовут delete[] на одном адресе — UB, скорее всего крэш// both destructors call delete[] on the same address — UB, most likely a crash
Правильное решение — либо объявить copy-конструктор/присваивание, реально выполняющие глубокое копирование (deep copy, аллокация нового массива и копирование содержимого), либо, что предпочтительнее на практике, вообще не владеть ресурсом напрямую — использовать std::vector/unique_ptr, которые уже правильно реализуют семантику копирования (или запрещают её) сами.The correct fix is either to declare a copy constructor/assignment that actually performs a deep copy (allocating a new array and copying the contents), or, preferably in practice, to not own the resource directly at all — use std::vector/unique_ptr, which already implement (or forbid) copy semantics correctly themselves.
Когда объект производного класса передаётся, возвращается или присваивается ПО ЗНАЧЕНИЮ базовому типу, копируется только базовая часть объекта — все поля, добавленные в производном классе, "срезаются" (slicing). Это не ошибка компиляции и не исключение — тихая потеря данных и состояния.When a derived-class object is passed, returned, or assigned BY VALUE to a base type, only the base portion of the object is copied — every field added in the derived class is "sliced off." This isn't a compile error or an exception — it's a silent loss of data and state.
struct Shape { double area = 0; };
struct Circle : Shape { double radius; };
void print(Shape s) { std::cout << s.area; } // принимает по значению — опасность слайсинга// takes by value — slicing danger
Circle c{{3.14}, 1.0};
print(c); // внутри print получен Shape, radius потерян — не Circle// print receives a Shape, radius is lost — not a Circle
Shape s = c; // то же самое: copy-конструктор Shape(const Shape&) вызван от Circle, radius не скопирован// same thing: Shape's Shape(const Shape&) is invoked on a Circle, radius isn't copied
Причина в том, что тип параметра/переменной фиксирован на этапе компиляции — вызывается ровно Shape'ов copy-конструктор, который физически не знает о существовании Circle::radius. Полиморфное поведение (виртуальные функции) при слайсинге тоже теряется — у полученного объекта статический тип Shape, а не Circle, поэтому виртуальный вызов пойдёт по таблице Shape.The reason is that the parameter/variable's type is fixed at compile time — exactly Shape's copy constructor is called, which has no way of knowing Circle::radius even exists. Polymorphic behavior (virtual functions) is lost through slicing too — the resulting object's static type is Shape, not Circle, so a virtual call dispatches through Shape's table.
Лечится передачей/хранением по ссылке или указателю (const Shape&, Shape*, unique_ptr<Shape>) везде, где предполагается полиморфное поведение — если тип принимает базовый класс по значению, это почти всегда признак того, что полиморфизм для этого места не был продуман.The fix is passing/storing by reference or pointer (const Shape&, Shape*, unique_ptr<Shape>) anywhere polymorphic behavior is expected — a function taking a base class by value is almost always a sign that polymorphism wasn't thought through for that spot.
Если тип не должен копироваться вообще (владеет уникальным ресурсом, представляет соединение, мьютекс) — T(const T&) = delete; и T& operator=(const T&) = delete; дают ошибку компиляции с понятным сообщением при попытке скопировать, вместо неверного поведения в рантайме.If a type shouldn't be copyable at all (owns a unique resource, represents a connection, a mutex) — T(const T&) = delete; and T& operator=(const T&) = delete; produce a clear compile error on any copy attempt, instead of incorrect runtime behavior.
std::vector<Shape> хранит объекты по значению — попытка положить туда Circle слайсит его при вставке. Хранение полиморфных объектов требует контейнера указателей/умных указателей (vector<unique_ptr<Shape>>), а не значений базового класса.std::vector<Shape> stores objects by value — trying to put a Circle in there slices it on insertion. Storing polymorphic objects requires a container of pointers/smart pointers (vector<unique_ptr<Shape>>), not base-class values.
Написанный вручную operator=, освобождающий старый ресурс перед копированием нового, сломается на a = a;, если не проверить this != &other (или не использовать copy-and-swap идиому, которая безопасна по построению) — старый ресурс будет освобождён до того, как из него скопируют данные.A hand-written operator= that frees the old resource before copying the new one breaks on a = a; if it doesn't check this != &other (or use the copy-and-swap idiom, which is safe by construction) — the old resource gets freed before its data is copied from.