17.3 – Порядок создания объектов производных классов

Добавлено 31 июля 2021 в 01:22

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

Во-первых, давайте представим несколько новых классов, которые помогут нам проиллюстрировать некоторые важные моменты.

class Base
{
public:
    int m_id;
 
    Base(int id=0)
        : m_id(id)
    {
    }
 
    int getId() const { return m_id; }
};
 
class Derived: public Base
{
public:
    double m_cost;
 
    Derived(double cost=0.0)
        : m_cost(cost)
    {
    }
 
    double getCost() const { return m_cost; }
};

В этом примере класс Derived является производным от класса Base.

Рисунок 1 Диаграмма наследования
Рисунок 1 – Диаграмма наследования

Поскольку Derived наследует функции и переменные от Base, вы можете предположить, что члены Base скопированы в Derived. Однако это не так. Вместо этого мы можем рассматривать Derived как класс, состоящий из двух частей: одна часть Derived и одна часть Base.

Рисунок 2 Состав класса Derived
Рисунок 2 – Состав класса Derived

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

int main()
{
    Base base;
 
    return 0;
}

Base – это не производный класс, потому что он не наследуется ни от каких других классов. C++ выделяет память для Base, а затем для инициализации вызывает конструктор Base по умолчанию.

Теперь давайте посмотрим, что происходит, когда мы создаем экземпляр производного класса:

int main()
{
    Derived derived;
 
    return 0;
}

Если бы вы попробовали этот код сами, вы бы не заметили никаких отличий от предыдущего примера, где мы создавали экземпляр непроизводного класса Base. Но за кулисами всё происходит немного иначе. Как упоминалось выше, на самом деле класс Derived состоит из двух частей: часть Base и часть Derived. Когда C++ создает объект Derived, он делает это поэтапно. Сначала создается самый базовый класс (на вершине дерева наследования). Затем по порядку создается каждый дочерний класс, пока самый дочерний класс (в нижней части дерева наследования) не будет построен последним.

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

Этот процесс на самом деле легко проиллюстрировать.

#include <iostream>
 
class Base
{
public:
    int m_id;
 
    Base(int id=0)
        : m_id(id)
    {
        std::cout << "Base\n";
    }
 
    int getId() const { return m_id; }
};
 
class Derived: public Base
{
public:
    double m_cost;
 
    Derived(double cost=0.0)
        : m_cost(cost)
    {
        std::cout << "Derived\n";
    }
 
    double getCost() const { return m_cost; }
};
 
int main()
{
    std::cout << "Instantiating Base\n";
    Base cBase;
 
    std::cout << "Instantiating Derived\n";
    Derived cDerived;
 
    return 0;
}

Эта программа дает следующий результат:

Instantiating Base
Base
Instantiating Derived
Base
Derived

Как видите, когда мы создавали Derived, сначала была построена часть Base класса Derived. В этом есть смысл: логически ребенок не может существовать без родителя. Это также безопасный способ выполнения чего-либо: дочерний класс часто использует переменные и функции от родительского класса, но родительский класс ничего не знает о дочернем. Создание экземпляра родительского класса первым гарантирует, что эти переменные уже инициализированы к моменту создания производного класса и готовы к использованию.

Порядок создания объектов из цепочек наследования

Иногда классы являются производными от других классов, которые сами являются производными от других классов. Например:

class A
{
public:
    A()
    {
        std::cout << "A\n";
    }
};
 
class B: public A
{
public:
    B()
    {
        std::cout << "B\n";
    }
};
 
class C: public B
{
public:
    C()
    {
        std::cout << "C\n";
    }
};
 
class D: public C
{
public:
    D()
    {
        std::cout << "D\n";
    }
};

Помните, что C++ всегда сначала создает «первый» или «самый базовый» класс. Затем он по порядку проходит по дереву наследования и создает каждый последующий производный класс.

Вот небольшая программа, которая иллюстрирует порядок создания по всей цепочке наследования.

int main()
{
    std::cout << "Constructing A: \n";
    A cA;
 
    std::cout << "Constructing B: \n";
    B cB;
 
    std::cout << "Constructing C: \n";
    C cC;
 
    std::cout << "Constructing D: \n";
    D cD;
}

Этот код печатает следующее:

Constructing A:
A
Constructing B:
A
B
Constructing C:
A
B
C
Constructing D:
A
B
C
D

Заключение

C++ создает объекты производных классов поэтапно, начиная с самого базового класса (наверху дерева наследования) и заканчивая самым дочерним классом (внизу дерева наследования). По мере создания каждого класса для инициализации части этого класса вызывается соответствующий конструктор из этого класса.

Вы можете заметить, что все наши примеры классов в этом разделе использовали конструкторы базового класса по умолчанию (для простоты). В следующем уроке мы более подробно рассмотрим роль конструкторов в процессе создания объектов производных классов (включая то, как явно выбрать конструктор базового класса, который вы хотите использовать в производном классе).

Теги

C++ / CppLearnCppДля начинающихКласс (программирование)НаследованиеОбучениеПрограммирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.