Если порожденный класс наследует элементы одного базового класса, то такое наследование называется одиночным. Однако, возможно и множественное наследование.
Множественное наследование позволяет порожденному классу наследовать элементы более, чем от одного базового класса. Синтаксис заголовков классов расширяется так, чтобы разрешить создание списка базовых классов и обозначения их уровня доступа:
{…};
class Y
{…};
class Z
{…};
class A : public X, public Y, public Z
{…};
Класс А обобщенно наследует элементы всех трех основных классов.
Для доступа к членам порожденного класса, унаследованного от нескольких базовых классов, используются те же правила, что и при порождении из одного базового класса.
Проблемы могут возникнуть в следующих случаях:
- если в порожденном классе используется член с таким же именем, как в одном из базовых классов;
- когда в нескольких базовых классах определены члены с одинаковыми именами.
В этих случаях необходимо использовать оператор разрешения контекста для уточнения элемента, к которому осуществляется доступ, именем базового класса.
Так как объект порожденного класса состоит из нескольких частей, унаследованных от базовых классов, то конструктор порожденного класса должен обеспечивать инициализацию всех наследуемых частей. В списке инициализации в заголовке конструктора порожденного класса должны быть перечислены конструкторы всех базовых классов.
Порядок выполнения конструкторов при порождении из нескольких классов следующий:
- конструкторы базовых классов в порядке их задания;
- конструкторы членов, являющихся объектами класса;
- конструктор порожденного класса.
Деструкторы вызываются в порядке обратном вызову конструкторов.
Пример
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>
using namespace std;
class X
{
protected:
int key;
public:
X(int i = 0) { cout << "X constructor" << endl; key = i; };
~X() { cout << "X destroyed" << endl; cin.get(); };
};
class Y
{
protected:
int key;
public:
Y(int i = 0) { cout << "Y constructor" << endl; key = i; };
~Y() { cout << "Y destroyed" << endl; cin.get(); };
};
class Z
{
protected:
int key;
public:
Z(int i = 0) { cout << "Z constructor" << endl; key = i; };
~Z() { cout << "Z destroyed" << endl; cin.get(); };
};
class A : public X, public Y, public Z
{
int key;
public:
A(int i = 0) : X(i + 1), Y(i + 2), Z(i + 3)
{
key = X::key + Y::key + Z::key;
}
int getkey(void) { return(key); }
};
int main()
{
A object(4);
cout << "object.key = " << object.getkey();
cin.get();
return 0;
}
Результат выполнения
Виртуальные базовые классы
Базовый класс может быть задан только один раз в списке порождения нового класса. Однако базовый класс может встретиться несколько раз в иерархии порождения.
Такая иерархия порождения несет двусмысленность при доступе к наследуемым членам класса X и может привести к ошибкам. В этом случае класс X будет дважды присутствовать в A. Хорошо это или плохо — зависит от решаемой задачи.
Если двойное вхождение объектов класса X в объект класса А не является допустимым, существует два выхода для разрешения такой ситуации:
- преобразование порождения из нескольких классов в порождение из одного класса и объявление дружественных классов.
class Y : public X
{friend class Z; }; // класс Z имеет доступ к Y - изменение механизма наследования таким образом, чтобы вне зависимости от того, как часто базовый класс будет встречаться в иерархии порождения, генерировалась бы только одна его копия. Такой механизм заключается в возможности создания виртуальных базовых классов.
Базовый класс определяется как виртуальный заданием ключевого слова virtual в списке порождения перед именем базового класса или указанием типа наследования
{
// тело_класса
};
Если базовый класс объявляется как виртуальный при порождении нового класса, то каждый объект будет содержать свою собственную часть, а вместо базовой части будет содержать указатель на виртуальный базовый класс.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>
using namespace std;
class X
{
protected:
int key;
public:
X(int i = 0) { cout << "X constructor" << endl; key = i; };
};
class Y : virtual public X
{
public:
Y(int i = 0) { cout << "Y constructor" << endl; key = i; };
};
class Z : virtual public X
{
public:
Z(int i = 0) { cout << "Y constructor" << endl; key = i; };
};
class A : public Y, public Z {
int key;
public:
A(int i = 0) : Y(i + 2), Z(i + 3)
{
key = Y::key + Z::key;
}
int getkey(void) { return(key); }
};
int main()
{
A object(4);
cout << "object.key = " << object.getkey();
cin.get();
return 0;
}
Результат выполнения
Как видим, объект класса X был создан только один раз.
Значение поля key изменяется каждый раз при доступе к нему как через класс Y, так и через класс Z.
Если в строках 11 и 17 этого примера убрать слово virtual, то результат работы программы изменится следующим образом:
То есть в этом случае и объект класса Y, и объект класса Z, которые наследует объект класса A, будут содержать свою собственную копию объекта класса X.
Конструкторы и деструкторы при использовании виртуальных базовых классов выполняются в следующем порядке:
- конструкторы виртуальных базовых классов выполняются до конструкторов не виртуальных базовых классов, независимо от того, как эти классы заданы в списке порождения;
- если класс имеет несколько виртуальных базовых классов, то конструкторы этих классов вызываются в порядке объявления виртуальных базовых классов в списке порождения;
- деструкторы виртуальных базовых классов выполняются после деструкторов не виртуальных базовых классов.
При порождении с использованием виртуальных базовых классов сохраняются те же правила видимости, что и при порождении с не виртуальными классами.
Хорошее объяснение. Спасибо.
Только стоит упомянуть, что если верхний класс виртуального наследования (в последнем примере класс Х) не имеет конструктора по-умолчанию, то компилятор потребует или предоставить данный конструктор верхнему классу, или явно сконструировать Х в списке инициализации.
Т.е., если конструктор X(int i = 0) был бы X(int i), то конструктор класса А необходимо исправить следующим образом: A(int i = 0) : X(аргумент), Y(i + 2), Z(i + 3).
Компиляторы последних версий доступно это объяснят, но если использовать что-то не очень актуальное, то можно долго биться на тем, почему при сборке последнего наследника, компилятор видит вызов конструктора по-умолчанию там, где его быть не должно.