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

Я столкнулся с некоторыми вопросами о списке инициализации конструктора, когда я столкнулся с этим.

Рассмотрим это:

class Student {
    public:
        Student() {
            id = 0;
        }
        Student(int i) {
            id = i;
        }
    private:
        int id;
};

Теперь, проверьте это:

К тому моменту, когда вы попадаете в тело конструктора, все поля уже построены; если у них есть конструкторы по умолчанию, они уже вызывались. Теперь, если вы назначили им значение в теле конструктора, вы вызываете конструктор копирования. Это неэффективно, потому что два конструктора в конечном итоге вызывают вместо одного.

Источник: Что делает двоеточие после имени конструктора С++?

Итак, означает ли это, что когда я вызываю конструктор без параметров, конструктор копирования также вызывается?

Пожалуйста, объясните. Это действительно запутывает.

В частности, значение первой строки:

К тому моменту, когда вы попадаете в тело конструктора, все поля уже построены

Ответ 1

Это означает, что int id уже инициализирован, прежде чем вы перейдете к строке id = 0. Поскольку явный инициализатор не указан, он инициализируется по умолчанию. Кроме того, поскольку это int, правила инициализации говорят, что они будут иметь некоторое неопределенное значение.

На практике, для int или любого типа, который чрезвычайно дешев для инициализации, это не имеет большого значения.

Если вместо использования члена int мы использовали класс, мы могли бы более четко видеть, что происходит на самом деле за кадром:

#include <iostream>

class Verbose {
    public:
        Verbose() {
            std::cout << __PRETTY_FUNCTION__ << "\n";
        }

        Verbose(int) {
            std::cout << __PRETTY_FUNCTION__ << "\n";
        }

        Verbose(Verbose const &) {
            std::cout << __PRETTY_FUNCTION__ << "\n";
        }

        Verbose & operator=(Verbose const &) {
            std::cout << __PRETTY_FUNCTION__ << "\n";
            return *this;
        }

        ~Verbose() {
            std::cout << __PRETTY_FUNCTION__ << "\n";
        }
};

class Object {
    public:
        Verbose v;

        Object() {
            v = Verbose(3);
        }
};

int main() {
    Object o;
}

Этот код выведет:

Verbose::Verbose()
Verbose::Verbose(int)
Verbose &Verbose::operator=(const Verbose &)
Verbose::~Verbose()
Verbose::~Verbose()

Заметим, что мы:

  • Мы используем конструктор по умолчанию для создания v.
  • Создаем временный Verbose(3)
  • Затем мы используем оператор присваивания для назначения временной переменной-члену.
  • Затем мы уничтожаем временное.
  • Когда Object o выходит за пределы области видимости, мы уничтожаем переменную-член.

Обратите внимание, что мы в основном построили Verbose v дважды! Сначала мы использовали конструктор по умолчанию, а затем мы в основном перестроили его с помощью вызова operator=. Если мы использовали список инициализаторов , мы могли бы уменьшить это до одного вызова Verbose(int).

Ответ 2

Они означают это

Student() {
    id = 0;
}

Менее эффективен, чем

Student() : id(0) {}

В этом конкретном примере id будет инициализирован за один шаг, так как элемент является просто int.

В отличие от этого, последний способ имел бы значение, если Student имел более сложные элементы, например, Backpack и Books. Если эти классы не были POD, первый метод выполнил бы два шага для инициализации члена, а второй выполнил бы только один шаг для инициализации.