Множественное наследование

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

class A
{
};
class B
{
};
class C : public A, public B
{
};

При создании объектов-представителей производного класса, порядок расположения непосредственных базовых классов в списке баз определяет очерёдность вызова конструкторов умолчания.

Этот порядок влияет и на очерёдность вызова деструкторов при уничтожении этих объектов. Но эти проблемы, также как и алгоритмы выделения памяти для базовых объектов, скорее всего, относятся к вопросам реализации. Вряд ли программист должен акцентировать на этом особое внимание.

Более существенным является ограничение, согласно которому одно и то же имя класса не может входить более одного раза в список баз при объявлении производного класса. Это означает, что в наборе непосредственных базовых классов, которые участвуют в формировании производного класса не должно встречаться повторяющихся элементов.

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

class A
{
 public:
 int x0, xA;
};
class B : public A
{
 public:
 int xB;
};
class C : public A
{
 public:
 int x0, xC;
};
class D : public B, public C
{
 public:
 int x0, xD;
};

В этом примере класс A дважды используется при объявлении класса D в качестве непрямого базового класса.

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

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

Такой фрагмент объекта мы будем называть производным фрагментом-представителем данного класса.

Верхние узлы графа и верхние уровни схем классов и объектов соответствуют базовым классам и фрагментам объектов, представляющих базовые и непосредственные базовые классы.

Эти фрагменты объекта мы будем называть базовыми и непосредственными базовыми фрагментами-представителями класса.

Вот как выглядит граф ранее приведённого в качестве примера производного класса D:

               A                          A
               B                          C
                             D

А вот как представляется структура производного класса в виде неполной схемы класса. Базовые классы располагаются на этой схеме в порядке, который соответствует списку базовых элементов в описании базы производного класса. Этот же порядок будет использован при изображении диаграмм объектов. И это несмотря на то обстоятельство, что порядок вызова конструкторов базовых классов определяется конкретной реализацией. За порядком вызова конструкторов базовых классов всегда можно наблюдать после определения их собственных версий.

A
B
A
C
D

А вот и схема объекта производного класса.

D MyD;
MyD ::=
A
	(int)x0;
	(int)xA;
B
	(int)xB;
A
	(int)x0;
	(int)xA;
C
	(int)x0;
D
	(int)x0;
	(int)xD;

Первое, что бросается в глаза - это множество одноимённых переменных, "разбросанных" по базовым фрагментам объекта. Да и самих базовых фрагментов здесь немало.

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

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

Например, неоднозначность содержится в следующем операторе:

MyD.xA = 100;

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

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

MyD.B::x0 = 100;

Этот оператор обеспечивает изменение значения данного-члена базового фрагмента - представителя класса B. Здесь нет никаких проблем, поскольку непосредственный базовый класс B наследует данные-члены базового класса A. Поскольку в классе B отсутствуют данные-члены с именем x0, транслятор однозначно определяет принадлежность этого элемента. Итак, доступ к данному-члену базового класса A "со стороны" непосредственного базового класса B не представляет особых проблем.

MyD.C::x0 = 100;

А теперь изменяется значение данного-члена базового фрагмента - представителя класса С. И опять же транслятор однозначно определяет местоположение изменяемой переменной. Переменная x0 была объявлена в непосредственном базовом классе C. И операция доступа указывает на эту переменную. А вот попытка изменения значения переменной x0, расположенной базовом фрагменте-представителе класса A "со стороны" непосредственного базового класса C обречена. Так, оператор

MyD.A::x0 = 777;

некорректен по причине неоднозначности соотнесения класса и его члена, поскольку непонятно, о каком базовом фрагменте-представителе класса A идёт речь. Выражения доступа с составными квалифицированными именами, как например,

MyD.C::A::x0

в контексте нашей программы также некорректны: составное квалифицированное имя предполагает вложенное объявление класса. Это свойство операции доступа уже обсуждалось ранее, в разделах, непосредственно посвящённых операциям. Вложенные объявления будут рассмотрены ниже.

Операция :: оставляет в "мёртвой зоне" целые фрагменты объектов. Однако возможность доступа к членам класса, которые оказались вне пределов досягаемости операции доступа всё же существует. Она обеспечивается указателями и операциями явного преобразования типа.

Идея состоит в том, чтобы, объявив указатель на объект-представитель базового класса, попытаться его настроить с помощью операций явного преобразования типа на соответствующий фрагмент объекта производного класса. В результате недосягаемые с помощью операции доступа фрагменты объекта превращаются в безымянные объекты простой конфигурации. Доступ к их членам в этом случае обеспечивается обычными операциями косвенного обращения. Рассмотрим несколько строк, которые демонстрируют такую технику работы с недосягаемыми фрагментами.

A* pObjA;
B* pObjB;
C* pObjC;
D* pObjD = &MyD;
// Мы начинаем с объявления соответствующих указателей.
pObjC = (C*)&MyD;
pObjA = (A*)pObjC;
// Произведена настройка указателей на требуемые фрагменты.
pObjA->x0 = 999;
// А это уже элементарно!

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

((A*)(C*)pObjD)->x0 = 5;
((A*)(B*)pObjD)->x0 = 55;
// Разным фрагментам - разные значения.

Аналогичным образом обстоят дела с функциями-членами базовых классов. Этот раздел мы завершаем небольшой программой, демонстрирующей методы доступа к членам базовых фрагментов объекта производного класса.

#include <iostream.h>
class A
{
public:
int x0;
int Fun1(int key);
};
int A::Fun1(int key)
{
 cout << " Fun1( " << key << " ) from A " << endl;
 cout << " x0 == " << x0 << "..." << endl;
 return 0;
}
class B: public A
{
public:
int x0;
int Fun1(int key);
int Fun2(int key);
};
int B::Fun1(int key)
{
 cout << " Fun1( " << key << " ) from B " << endl;
 cout << " x0 == " << x0 << "..." << endl;
 return 0;
}
int B::Fun2(int key)
{
 Fun1(key * 5);
 cout << " Fun2( " << key << " ) from B " << endl;
 cout << " x0 == " << x0 << "..." << endl;
 return 0;
}
class C: public A
{
public:
int x0;
int Fun2(int key);
};
int C::Fun2(int key)
{
 A::x0 = 25;
 Fun1(key * 5);
 cout << " Fun2( " << key << " ) from C " << endl;
 cout << " x0 == " << x0 << "..." << endl;
 return 0;
}
class D: public B, public C
{
public:
int x0;
int Fun1(int key);
};
int D::Fun1(int key)
{
 cout << " Fun1( " << key << " ) from D " << endl;
 cout << " x0 == " << x0 << "..." << endl;
 return 0;
}
void main ()
{
 D MyD;
 ObjD.x0 = 111;
 A* pObjA;
 B* pObjB;
 C* pObjC;
 D* pObjD = &MyD;
 MyD.B::x0 = 100;
 MyD.C::x0 = 333;
 MyD.Fun1(1);
 pObjD->B::Fun1(1);
 pObjD->C::Fun2(1);
 pObjA = (A*) (B*) pObjD;
 ((A*) ((C*) pObjD))->Fun1(111);
 ((A*) ((B*) pObjD))->Fun1(111);
 pObjA->Fun1(111);
 pObjC = (C*)&MyD;
 pObjA = (A*)pObjC;
 ((A*)(B*)pObjD)->x0 = 1;
 ((A*)(B*)pObjD)->Fun1(777);
 ((A*)(C*)pObjD)->x0 = 2;
 ((A*)(C*)pObjD)->Fun1(999);
}

Назад | Содержание | Вперед