На этом шаге мы рассмотрим использование виртуальных базовых классов.
Чтобы устранить дублирование объектов непрямого базового класса при множественном наследовании, этот базовый класс объявляют виртуальным. Для этого в списке базовых классов перед именем класса необходимо поместить ключевое слово virtual. Например, класс X будет виртуальным базовым классом при таком описании:
class X { ... f(); ... }; class Y: virtual public X { ... }; class Z: virtual public X { ... ); class D: public Y, public Z { ... );
Теперь класс D будет включать только один экземпляр X, доступ х которому равноправно имеют классы Y и Z. Графически это очень наглядно:
Рис.1. Исключение дублирования базового класса
Иллюстрацией сказанного может служить иерархия классов в следующей программе:
//OOР28_1.СРР - множественное наследование с виртуальным // базовым классом. #include <iostream.h> class base // Первичный (основной) базовый класс. { int jj; char cc; char w[10]; public: base (int j = 0, char c = '*') { jj = j; cc = c; } }; class dbase: public virtual base { double dd; public: dbase(double d = 0.0) : base() { dd = d; } }; class fbase: public virtual base { float ff; public: fbase (float f = 0.0): base() { ff = f; } }; class top: public dbase, public fbase { long tt; public: top (long t = 0) : dbase(), fbase() { tt = t; } }; void main () { cout <<"\nОсновной базовый класс: sizeof(base) = " << sizeof(base); cout <<"\nНепосредственная база: sizeof(dbase) = " << sizeof(dbase); cout <<"\nНепосредственная база: sizeof(fbase) = " << sizeof (fbase); cout <<"\nПроизводный класс: sizeof(top) = " << sizeof(top); }
Результаты выполнения программы:
Основной базовый класс: sizeof (base) = 13 Непосредственная база: sizeof(dbase) = 23 Непосредственная база: sizeof(fbase) = 19 Производный класс: sizeof (top) = 33
Основной базовый класс base в соответствии с размерами своих компонентов стандартных типов int и char [11] имеет размер 13 байт. Создаваемые на его основе классы dbase и fbase занимают соответственно 23 и 19 байт. (В dbase входит переменная типа double, занимающая 8 байт, наследуется базовый класс base, для которого требуется 13 байт, и 2 байта нужны для связи в иерархии виртуальных классов.) Производный класс top включает: один экземпляр базового класса base (13 байт); данные и связи класса dbase (10 байт); данные и связи класса fbase (6 байт); компонент long tt (4 байта).
Если в той же программе убрать требование виртуальности (атрибут virtual) при наследовании base в классах dbase и fbase, то программа будет выглядеть так:
//OOР28_2.СРР - множественное наследование с невиртуальным // базовым классом. #include <iostream.h> class base // Первичный (основной) базовый класс. { int jj; char cc; char w[10]; public: base (int j = 0, char c = '*') { jj = j; cc = c; } }; class dbase: public base { double dd; public: dbase(double d = 0.0) : base() { dd = d; } }; class fbase: public base { float ff; public: fbase (float f = 0.0): base() { ff = f; } }; class top: public dbase, public fbase { long tt; public: top (long t = 0) : dbase(), fbase() { tt = t; } }; void main () { cout <<"\nОсновной базовый класс: sizeof(base) = " << sizeof(base); cout <<"\nНепосредственная база: sizeof(dbase) = " << sizeof(dbase); cout <<"\nНепосредственная база: sizeof(fbase) = " << sizeof (fbase); cout <<"\nПроизводный класс: sizeof(top) = " << sizeof(top); }
А результаты будут такими:
Основной базовый класс: sizeof (base) = 13 Непосредственная база: sizeof(dbase) = 21 Непосредственная база: sizeof(fbase) = 17 Производный класс: sizeof (top) = 42
Обратите внимание, что размеры производных классов при отсутствии виртуальных базовых равны сумме длин их компонентов и длин унаследованных базовых классов. "Накладные расходы" памяти здесь отсутствуют.
При множественном наследовании один и тот же базовый класс может быть включен в производный класс одновременно несколько раз, причем и как виртуальный, и как невиртуальный. Для иллюстрации этого положения изобразим направленный граф, а затем приведем структуру соответствующей ему иерархии классов:
Рис.2. Пример включения базового класса как виртуального и невиртуального
class X {...}; class Y: virtual public X { ...}; class Z: virtual public X { ...}; class B: virtual public X { ...}; class C: virtual public X { ...}; class E: public X { ...}; class D: public X { ...}; class A: public D, public B, public Y, public Z, public C, public E {...};
В данном примере объект класса A включает три экземпляра объектов класса X: один виртуальный, совместно используемый классами B, Y, C, Z, и два невиртуальных относящихся соответственно к классам D и Е. Таким образом, можно констатировать, что виртуальность класса в иерархии производных классов является не свойством класса как такового, а результатом особенностей процедуры наследования.
Возможны и другие комбинации виртуальных и невиртуальных базовых классов. Например:
class ВВ { ... }; class АА: virtual public ВВ { ... }; class CC: virtual public ВВ { ... }; class DD: public AA, public CC, virtual public BB { ... };
Соответствующий НАГ имеет вид:
Рис.3. НАГ комбинации виртуальных и невиртуальных классов
При использовании наследования и множественного наследования могут возникать неоднозначности при доступе к одноименным компонентам разных базовых классов. Простейший и самый надежный способ устранения неоднозначностей - использование квалифицированных имен компонентов. Как обычно, для квалификации имени компонента используется имя класса. Следующий пример иллюстрирует упомянутую неоднозначность и ее разрешение с помощью квалифицированных имен компонентов:
class X { public: int d; ... } ; class Y { public: int d; ... }; class Z: public X, public Y { public: int d; . . . d = X::d + Y::d; . . . };
На следующем шаге мы начнем знакомиться с виртуальными функциями.