Программирование на C и C++

Онлайн справочник программиста на C и C++

Виртуальный базовый класс

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

// программа содержит ошибку и не будет компилироваться
#include <iostream.h>
class base {
public:
int i;
};
// d1 наследует base.
class d1 : public base {
public:
int j;
};
// d2 наследует base.
class d2 : public base {
public:
int k;
};
/* d3 наследует как d1, так и d2 . Это означает, что в d3 имеется две копии base! */
class d3 : public d1, public d2 {
public:
int m;
};
int main()
{
d3 d;
d.i = 10; // неопределенность, какое i???
d.j = 20;
d.k = 30;
d.m = 40;
// также неопределенность, какое i???
cout << d.i << " ";
cout << d.j << " " << d.k << " ";
cout << d.m;
return 0;
}

Как показывает комментарий в данной программе, оба класса d1 и d2 наследуют класс base. Однако класс d3 наследует оба класса d1 и d2. Это означает, что в классе d3 представлены две копии класса base. Поэтому в выражении типа
d.i = 20;
не ясно, какое именно i имеется в виду — относящееся к d1 или же относящееся к d2? Поскольку имеется две копии класса base в объекте d, то там имеются также две переменные d.i. Как видно, инструкция является двусмысленной в силу описанного наследования.

Имеется два способа исправить программу. Первый заключается в использовании оператора области видимости для переменной i с дальнейшим выбором вручную одного из i. Например, следующая версия программы компилируется и исполняется так, как это необходимо:

#include <iostream.h>
class base {
public:
int i;
};
// d1 наследует base.
class d1 : public base {
public:
int j;
};
// d2 наследует base.
class d2 : public base {
public:
int k;
};
/* d3 наследует как d1, так и d2. Это означает, что в d3 имеется две копии base! */
class d3 : public d1, public d2 {
public:
int m;
};
int main()
{
d3 d;
d.d2::i = 10; // область видимости определена, используется i для d2
d.j = 20;
d.k = 30;
d.m = 40;
// область видимости определена, используется i для d2
cout << d.d2::i << " ";
cout << d.j << " " << d.k << " ";
cout << d.m;
return 0;
}

Как можно видеть, используя оператор области видимости ::, в программе вручную выбирается версия d2 класса base. Тем не менее, данное решение порождает более глубокие вопросы: что если требуется только одна копия класса base? Имеется ли какой-либо способ предотвратить вклю­чение двух копий в класс d3? Как можно было догадаться, ответ на этот вопрос положительный. Решение достигается путем использования виртуального базового класса.

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

#include <iostream.h>
class base {
public:
int i;
};
// d1 наследует base как virtual
class d1 : virtual public base {
public:
int j;
};
// d2 наследует base как virtual
class d2 : virtual public base {
public:
int k;
};
/* d3 наследует как d1 так и d2. Тем не менее в d3 имеется только одна копия base! */
class d3 : public d1, public d2 {
public:
int m;
};
int main()
{
d3 d;
d.i = 10; // неопределенности больше нет
d.j = 20;
d.k = 30;
d.m = 40;
cout << d.i << " "; // неопределенности больше нет
cout << d.j << " " << d.k << " ";
cout << d.m;
return 0;
}

Как видно, ключевое слово virtual предшествует спецификации наследуемого класса. Теперь оба класса d1 и d2 наследуют класс base как виртуальный. Любое множественное наследование с их участием порождает теперь включение только одной копии класса base. Поэтому в классе d3
имеется только одна копия класса base, и, следовательно, d.i = 10 теперь не является двусмыслен­ным выражением.

Необходимо иметь в виду еще одно обстоятельство: хотя оба класса d1 и d2 используют класс base как виртуальный, тем не менее всякий объект класса d1 или d2 будет содержать в себе base. Например, следующий код абсолютно корректен:

// определение класса типа d1
d1 myclass;
myclass.i = 100;

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