Правила вызова конструктора базового класса в C++
Основные правила вызова конструктора базового класса
В C++ при создании объекта производного класса всегда вызывается один из конструкторов базового класса. Это происходит до выполнения тела конструктора производного класса. Если вы не укажете явно, какой конструктор базового класса вызывать, компилятор вызовет конструктор по умолчанию (без параметров). Если такого конструктора нет, код не скомпилируется.
Явный вызов конструктора базового класса
Чтобы передать параметры в конструктор базового класса, используйте список инициализации производного класса:
class Dog : public Animal {
public:
Dog() : Animal() { cout << "Dog created\n"; } // явно, но необязательно
Dog(string name) : Animal(name) { cout << "Dog " << name << " created\n"; }
};
Теперь при создании Dog d("Buddy") будет вызван Animal(string name), и поле name_ получит значение "Buddy".
Правила и особенности
- Конструктор базового класса вызывается первым — до инициализации членов производного класса и до выполнения его тела.
- Если не указать иное, вызывается конструктор по умолчанию базового класса. Если его нет — ошибка компиляции.
- Виртуальное наследование — конструктор самого производного класса отвечает за вызов конструктора виртуального базового класса, даже если есть промежуточные классы.
- Множественное наследование — конструкторы всех базовых классов вызываются в порядке их объявления при наследовании.
- Копирующий/перемещающий конструктор также может быть вызван явно:
Dog(const Dog& other) : Animal(other) { }.
Пример с разными сценариями
class Animal {
public:
Animal() : name_("unknown") { cout << "Animal()\n"; }
Animal(const string& name) : name_(name) { cout << "Animal(" << name << ")\n"; }
Animal(const Animal& other) : name_(other.name_) { cout << "Animal(copy)\n"; }
private:
string name_;
};
class Dog : public Animal {
public:
// Вызов конструктора по умолчанию
Dog() : Animal() { cout << "Dog()\n"; }
// Вызов параметризованного конструктора
Dog(const string& name) : Animal(name) { cout << "Dog(" << name << ")\n"; }
// Копирование — делегируем базовому классу
Dog(const Dog& other) : Animal(other) { cout << "Dog(copy)\n"; }
};
int main() {
Dog d1; // Animal() → Dog()
Dog d2("Rex"); // Animal("Rex") → Dog("Rex")
Dog d3(d2); // Animal(copy) → Dog(copy)
return 0;
}
Что будет, если не вызывать явно?
Если в производном классе не указать : Animal(...), компилятор попытается вызвать Animal(). Если его нет, получите ошибку. Всегда явно указывайте, какой конструктор базового класса нужен, особенно если у базового класса нет конструктора по умолчанию.
Итоговые рекомендации
- Всегда используйте список инициализации для вызова конструктора базового класса, если нужно передать параметры.
- Не полагайтесь на конструктор по умолчанию, если он не подходит по логике.
- Помните, что конструкторы вызываются от самого базового к самому производному (цепочка наследования).
Альтернативный взгляд: делегирующие конструкторы и C++11
В C++11 появилась возможность делегировать конструкторы внутри одного класса. Это может упростить код, но не отменяет правил для базового класса. Например:
class Dog : public Animal {
public:
Dog() : Dog("unknown") { } // делегирование конструктору Dog(string)
Dog(const string& name) : Animal(name) { cout << "Dog(" << name << ")\n"; }
};
Здесь Dog() делегирует вызов Dog(string), который уже явно вызывает Animal(name). Это удобно, чтобы не дублировать логику инициализации.
Важный нюанс: порядок инициализации
Даже если вы используете делегирующие конструкторы, порядок остаётся прежним: сначала вызывается конструктор базового класса (через цепочку делегирования), затем инициализируются члены производного класса, и только потом выполняется тело конструктора. Никогда не пытайтесь вызвать конструктор базового класса из тела конструктора — это невозможно. Используйте только список инициализации.
Когда явный вызов обязателен?
- Если у базового класса нет конструктора по умолчанию.
- Если базовый класс требует нетривиальной инициализации (например, захват ресурсов).
- Если вы хотите скопировать или переместить объект базового класса корректно.
В остальных случаях можно полагаться на неявный вызов, но лучше всегда указывать явно для читаемости и контроля.