17.3 – Порядок создания объектов производных классов
В предыдущем уроке об основах наследования в 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
.
Поскольку Derived
наследует функции и переменные от Base
, вы можете предположить, что члены Base
скопированы в Derived
. Однако это не так. Вместо этого мы можем рассматривать Derived
как класс, состоящий из двух частей: одна часть Derived
и одна часть Base
.
Вы уже видели множество примеров того, что происходит, когда мы создаем экземпляр обычного (не производного) класса:
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++ создает объекты производных классов поэтапно, начиная с самого базового класса (наверху дерева наследования) и заканчивая самым дочерним классом (внизу дерева наследования). По мере создания каждого класса для инициализации части этого класса вызывается соответствующий конструктор из этого класса.
Вы можете заметить, что все наши примеры классов в этом разделе использовали конструкторы базового класса по умолчанию (для простоты). В следующем уроке мы более подробно рассмотрим роль конструкторов в процессе создания объектов производных классов (включая то, как явно выбрать конструктор базового класса, который вы хотите использовать в производном классе).