Наследование в C++: как правильно вызывать конструктор базового класса и не сломать код

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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

Каждый, кто начинал изучать наследование в C++, рано или поздно сталкивался с ситуацией: вы создаёте класс-потомок, компилятор ругается, программа не собирается, а вы не понимаете — почему? Чаще всего корень зла кроется в неправильном вызове конструктора базового класса. Этот вопрос на Stack Overflow собрал тысячи просмотров, и не зря: даже опытные разработчики иногда забывают элементарные правила.

В этой статье мы разберём правила вызова конструктора базового класса в C++, пройдёмся по топ-5 самых популярных ошибок и покажем, как их избежать. Если вы хотите писать надёжный код без сюрпризов — читайте до конца.

Краткий ликбез: как работают конструкторы при наследовании

В C++ конструкторы не наследуются. Когда вы создаёте объект производного класса, сначала вызывается конструктор базового класса (неявно или явно), а затем — конструктор потомка. Если вы не укажете, какой конструктор предка вызывать, компилятор попытается вызвать конструктор по умолчанию базового класса. Если его нет — получите ошибку компиляции.

Явный вызов делается через список инициализации:

class Base {
public:
    Base(int x) { /* ... */ }
};

class Derived : public Base {
public:
    // Правильно: явный вызов конструктора Base(int)
    Derived() : Base(42) { /* ... */ }
};

Кажется просто? Но именно здесь новички совершают 90% ошибок. Давайте разберём их по порядку.

Топ-5 ошибок при вызове конструктора базового класса

Ошибка №1: Забыли вызвать конструктор предка с параметрами

Самая распространённая проблема. Если у базового класса нет конструктора по умолчанию, а вы его не вызвали явно — компилятор выдаст ошибку.

class Base {
public:
    Base(int x) { /* инициализация */ }
};

class Derived : public Base {
public:
    // ОШИБКА: компилятор пытается вызвать Base(), которого нет
    Derived() { }
};

Как исправить:

class Derived : public Base {
public:
    // ПРАВИЛЬНО: явно вызываем Base(int)
    Derived() : Base(0) { }
    // Или передаём параметр извне:
    Derived(int x) : Base(x) { }
};

Ошибка №2: Неправильный порядок инициализации

Многие думают, что порядок вызова конструкторов в списке инициализации определяет порядок выполнения. Это не так. Базовый класс всегда инициализируется первым, независимо от того, где вы написали его вызов.

class Derived : public Base {
private:
    int value;
public:
    // Кажется, что value инициализируется первым? Нет!
    Derived() : value(10), Base(5) { }
    // На самом деле: сначала Base(5), потом value(10)
};

Это может привести к логическим ошибкам, если инициализация value зависит от состояния базового класса.

Ошибка №3: Вызов конструктора предка в теле конструктора

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

class Derived : public Base {
public:
    Derived() {
        // ОШИБКА: так конструктор не вызывается
        Base(10); // Просто создаст временный объект и удалит его
    }
};

Ошибка №4: Путаница с конструкторами копирования

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

class Derived : public Base {
public:
    // ОШИБКА: Base скопируется неправильно
    Derived(const Derived& other) { /* ... */ }

    // ПРАВИЛЬНО:
    Derived(const Derived& other) : Base(other) { /* ... */ }
};

Ошибка №5: Множественное наследование и виртуальные базовые классы

При множественном наследовании порядок вызова конструкторов усложняется. Если один и тот же базовый класс наследуется несколько раз (ромбовидное наследование), его конструктор по умолчанию будет вызван несколько раз, если не использовать виртуальное наследование.

class A { public: A() { cout << "A"; } };
class B : virtual public A { public: B() : A() { cout << "B"; } };
class C : virtual public A { public: C() : A() { cout << "C"; } };
class D : public B, public C { public: D() : A(), B(), C() { cout << "D"; } };
// Вывод: A B C D (A вызывается только один раз!)

Без virtual конструктор A был бы вызван дважды, что часто приводит к ошибкам.

Лучшие практики: как избежать ошибок

Правило 1: Всегда используйте список инициализации

Не смешивайте инициализацию базового класса с присваиванием в теле конструктора. Список инициализации — единственное правильное место для вызова конструктора предка.

Правило 2: Явно вызывайте конструктор, даже если он по умолчанию

Это повышает читаемость кода и защищает от случайных изменений в базовом классе. Если вы добавите параметры в конструктор базового класса, компилятор сразу укажет на места, где нужно внести правки.

Правило 3: Помните о порядке инициализации

Всегда располагайте вызов конструктора базового класса первым в списке инициализации — это общепринятый стиль, который помогает избежать путаницы.

Правило 4: Используйте делегирующие конструкторы (C++11)

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

class Derived : public Base {
public:
    Derived() : Derived(0) { } // делегирование
    Derived(int x) : Base(x) { }
};

Реальные примеры из практики

Представьте, что вы пишете игру. У вас есть базовый класс Entity с конструктором, принимающим координаты. Класс Player должен передать начальные координаты:

class Entity {
protected:
    int x, y;
public:
    Entity(int startX, int startY) : x(startX), y(startY) { }
};

class Player : public Entity {
private:
    int health;
public:
    // Правильная инициализация
    Player() : Entity(0, 0), health(100) { }
    Player(int startX, int startY) : Entity(startX, startY), health(100) { }
};

Если вы забудете вызвать Entity(startX, startY), компилятор будет

Блоги

Рекомендаций по книгам