Что такое конструктор копирования в C++
Что проверяют
Понимаете ли вы, что копии в C++ возникают неявно, и умеете ли управлять владением ресурсом. Вопрос про копирующий конструктор почти всегда ведёт к следующему: кто освобождает память, что делает компилятор без вашего участия и какие функции он перестаёт генерировать.
Как отвечать
Копирующий конструктор создаёт новый объект как копию существующего того же типа, сигнатура T(const T&). Принимать параметр по значению нельзя, это бесконечная рекурсия; const нужен, чтобы копировать константные и временные объекты.
Ключевое: компилятор генерирует его сам, и генерация почленная. Для указателя копируется сам указатель, а не то, на что он указывает. Отсюда разница между поверхностным и глубоким копированием, и отсюда двойное освобождение одной области памяти, когда оба объекта дойдут до деструктора.
Скажите, когда он вызывается: инициализация от объекта того же типа, передача и возврат по значению. Часть таких копий устраняется оптимизацией, и для временных объектов современный стандарт делает это обязательным.
Дальше правила. Правило трёх: нужен свой деструктор, копирующий конструктор или копирующее присваивание, значит нужны все три. Правило пяти добавляет перемещающие. Лучший вариант это правило нуля: хранить ресурс в std::string, std::vector или std::unique_ptr и не писать ничего.
Где спотыкаются
Пишут класс с ручным выделением памяти, аккуратно делают копирующий конструктор и забывают оператор присваивания. Выражение a = b пройдёт почленно, прежний буфер утечёт, а один адрес освободится дважды при выходе из области видимости.
Не знают о побочном эффекте: объявив копирующий конструктор или деструктор, вы подавляете неявную генерацию перемещающих операций. Класс продолжает работать, но там, где раньше было перемещение, теперь тихо выполняется полное копирование, и производительность падает без единой ошибки компиляции.
Боятся возврата объекта по значению и начинают возвращать указатели или писать return std::move(x) из локальной переменной. Первое возвращает ручное управление памятью со всеми его рисками, второе мешает оптимизации и делает код медленнее, чем простой возврат.