Когда вызывается конструктор копирования в C++?

Ответ

Конструктор копирования вызывается для создания нового объекта как копии существующего объекта того же типа. Основные сценарии:

  1. Явная инициализация копией:
    MyClass obj1;
    MyClass obj2(obj1);      // Прямой вызов конструктора копирования
    MyClass obj3 = obj1;     // Копирующая инициализация (также вызывает конструктор копирования)
  2. Передача аргумента в функцию по значению:
    void foo(MyClass param); // Функция принимает объект по значению
    MyClass obj;
    foo(obj);                // При вызове `param` создаётся копированием `obj`
  3. Возврат объекта из функции по значению (в определённых условиях):
    MyClass createObject() {
        MyClass localObj;
        return localObj;     // До C++17 мог вызываться конструктор копирования/перемещения.
                             // Начиная с C++17, здесь гарантировано применяется оптимизация
                             // (mandatory copy elision), и вызова конструктора не происходит.
    }
  4. Инициализация элементов или базовых классов: При создании объекта, члены которого или базовые классы инициализируются другим объектом.

Практический пример:

#include <iostream>

class Resource {
public:
    Resource() { std::cout << "Resource defaultn"; }
    Resource(const Resource&) { std::cout << "Resource copyn"; }
};

class MyClass {
    Resource res;
public:
    MyClass() { std::cout << "MyClass defaultn"; }
    MyClass(const MyClass& other) : res(other.res) { // Вызов конструктора копирования Resource
        std::cout << "MyClass copyn";
    }
};

void byValue(MyClass c) { /* ... */ }

int main() {
    std::cout << "--- Creating original ---n";
    MyClass original;

    std::cout << "n--- Copy initialization ---n";
    MyClass copy = original; // Вызов MyClass::MyClass(const MyClass&)

    std::cout << "n--- Passing by value ---n";
    byValue(original);       // Ещё один вызов конструктора копирования
    return 0;
}

Важно: Современный C++ (C++11 и новее) стремится заменять копирование на перемещение (через конструктор перемещения) там, где это возможно, например, при возврате локальной переменной из функции.

Ответ 18+ 🔞

Э, слушай, давай разберём эту хрень с конструктором копирования, а то народ путается, как последний распиздяй. Представь, у тебя есть крутая тачка. Конструктор копирования — это когда ты делаешь её точную копию, чтоб на такой же кататься. Не воруешь, а именно копируешь, болт в болт.

Вот основные случаи, когда эта магия срабатывает, ёпта:

  1. Когда ты тупо копируешь объект в лоб. Самый простой случай, тут даже думать не надо.

    MyClass obj1;
    MyClass obj2(obj1);      // Всё, просто скопировал. Прямой вызов.
    MyClass obj3 = obj1;     // Тоже копирование, хоть и знак равенства. Это инициализация, а не присваивание!
  2. Когда суёшь объект в функцию по значению. Вот тут народ часто охуевает, откуда лишние вызовы берутся.

    void foo(MyClass param); // Функция жрёт объект ПО ЗНАЧЕНИЮ
    MyClass obj;
    foo(obj);                // Ёб твою мать! Сейчас `obj` скопируют в `param`. Вот тебе и вызов конструктора копирования.
  3. Когда функция возвращает объект по значению. Тут история пиздец интересная. Раньше (до C++17) это был геморрой и лишние копии. Сейчас компиляторы умные, часто вырезают лишнее (copy elision), а с C++17 это вообще гарантировано в таких случаях. Но принцип ты должен понимать.

    MyClass createObject() {
        MyClass localObj;
        return localObj;     // В теории — может копировать. На практике сейчас — обычно оптимизируют.
    }
  4. Когда копируешь член класса. Если в твоём классе есть другие объекты, их тоже надо скопировать правильно, а не просто хуй с горы бросить.

Смотри, живой пример, чтоб вообще всё стало ясно:

#include <iostream>

class Resource {
public:
    Resource() { std::cout << "Resource defaultn"; }
    Resource(const Resource&) { std::cout << "Resource copyn"; } // Его конструктор копирования
};

class MyClass {
    Resource res; // Член-объект
public:
    MyClass() { std::cout << "MyClass defaultn"; }
    // Вот наш герой! Копируем не только себя, но и член `res`!
    MyClass(const MyClass& other) : res(other.res) {
        std::cout << "MyClass copyn";
    }
};

void byValue(MyClass c) { /* ... */ } // Функция-пожиратель копий

int main() {
    std::cout << "--- Создаём оригинал ---n";
    MyClass original; // Вызовы: Resource default, MyClass default

    std::cout << "n--- Копируем инициализацией ---n";
    MyClass copy = original; // БАМ! Resource copy, MyClass copy

    std::cout << "n--- Передаём в функцию по значению ---n";
    byValue(original);       // И ещё раз! Resource copy, MyClass copy
    return 0;
}

Важный момент на посошок: Современный C++ (C++11 и дальше) — он хитрая жопа. Он не любит лишнюю работу. Поэтому если объект можно не копировать, а переместить (как будто переставить кресло, а не делать новое), он так и сделает, через конструктор перемещения. Особенно когда функция возвращает временный объект. Но это уже другая история, про то, как избежать лишнего геморроя.