В C++ ключевое слово virtual используется для объявления виртуальных функций, которые будут переопределены в производных классах. Однако ключевое слово virtual также имеет другое использование, позволяющее определить виртуальный базовый класс. Для того чтобы понять, что собой представляет виртуальный базовый класс и почему ключевое слово virtual имеет второе значение, давайте начнем с короткой некорректной программы:
#include <iostream.h>
class base { public: int i; };
class d1 : public base { public: int j };
class d2 : public base { public: int k; };
/* d3 наследует как d1, так и d2. Это означает, что в d3 имеется две копии base! */
Как показывает комментарий в данной программе, оба класса d1 и d2 наследуют класс base. Однако класс d3 наследует оба класса dl и d2. Это означает, что в классе d3 представлены две копии класса base. Поэтому в выражении типа
d.i = 20;
не ясно, какое именно i имеется в виду — относящееся к d1 или же относящееся к d2? Поскольку имеется две копии класса base в объекте d, то там имеются также две переменные d.i. Как видно, инструкция является двусмысленной в силу описанного наследования.
Имеется два способа исправить программу. Первый заключается в использовании оператора области видимости для переменной i с дальнейшим выбором вручную одного из i. Например, следующая версия программы компилируется и исполняется так, как это необходимо:
#include <iostream.h>
class base { public: int i; };
class d1 : public base { public: int j; };
class d2 : public base { public: int k; };
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
Как можно видеть, используя оператор области видимости ::, в программе вручную выбирается версия i класса d2. Тем не менее, данное решение порождает более глубокие вопросы: что если требуется только одна копия класса base? Имеется ли какой-либо способ предотвратить включение двух копий в класс d3? Как можно было догадаться, ответ на этот вопрос положительный. Решение достигается путем использования виртуального базового класса.
Когда два или более класса порождаются от одного общего базового класса, можно предотвратить включение нескольких копий базового класса в объект-потомок этих классов путем объявления базового класса виртуальным при его наследовании. Например, ниже приведена другая версия предыдущей программы, в которой d3 содержит только одну копию класса base:
#include <iostream.h>
class base { public: int i; };
class d1 : virtual public base { public: int j; };
class d2 : virtual public base ( public: int k; };
/* d3 наследует как d1 так и d2. Тем не менее в d3 имеется только одна копия base! */
class d3 : public d1, public d2 { public: int m; };
Как видно, ключевое слово virtual предшествует спецификации наследуемого класса. Теперь оба класса d1 и d2 наследуют класс base как виртуальный. Любое множественное наследование с их участием порождает теперь включение только одной копии класса base. Поэтому в классе d3 имеется только одна копия класса base, и, следовательно, d.i = 10 теперь не является двусмысленным выражением.
Необходимо иметь в виду еще одно обстоятельство: хотя оба класса d1 и d2 используют класс base как виртуальный, тем не менее, всякий объект класса d1 или d2 будет содержать в себе base. Например, следующий код абсолютно корректен:
// определение объекта класса d1
d1 myclass;
myclass.i = 100;
Обычные и виртуальные базовые классы отличаются друг от друга только тогда, когда какой-либо объект наследует базовый класс более одного раза. При использовании виртуального базового класса только одна копия базового класса содержится в объекте. В случае использования обычного базового класса в объекте могут содержаться несколько копий.
Имеются два термина, часто используемые, когда речь заходит об объектно-ориентированных языках программирования: раннее и позднее связывание. По отношению к C++ эти термины соответствуют событиям, которые возникают на этапе компиляции и на этапе исполнения программы соответственно.
В терминах объектно-ориентированного программирования раннее связывание означает, что объект и вызов функции связываются между собой на этапе компиляции. Это означает, что вся необходимая информация для того, чтобы определить, какая именно функция будет вызвана, известна на этапе компиляции программы. В качестве примеров раннего связывания можно указать стандартные вызовы функций, вызовы перегруженных функций и перегруженных операторов. Принципиальным достоинством раннего связывания является его эффективность — оно более быстрое и обычно требует меньше памяти, чем позднее связывание. Его недостатком служит невысокая гибкость.
Позднее связывание означает, что объект связывается с вызовом функции только во время исполнения программы, а не раньше. Позднее связывание достигается в C++ с помощью использования виртуальных функций и производных классов. Его достоинством является высокая гибкость. Оно может использоваться для поддержки общего интерфейса, позволяя при этом различным объектам иметь свою собственную реализацию этого интерфейса. Более того, оно помогает создавать библиотеки классов, допускающие повторное использование и расширение.
Какое именно связывание должна использовать программа, зависит от предназначения программы. Сложные программы используют оба вида связывания. Позднее связывание является одним из самых мощных расширений языка C++ относительно языка С. Платой за такое увеличение мощи программы служит некоторое уменьшение ее скорости исполнения. Поэтому использование позднего связывания оправдано только тогда, когда оно улучшает структурированность и управляемость программы. Следует иметь в виду, что проигрыш в производительности невелик, поэтому когда ситуация требует позднего связывания, можно использовать его без всякого сомнения.