Очередная модификация базового класса приводит к неожиданным последствиям. Эта модификация состоит в изменении спецификатора функции-члена базового класса. Мы (впервые!) используем спецификатор virtual в объявлении функции. Функции, объявленные со спецификатором virtual, называются виртуальными функциями. Введение виртуальных функций в объявление базового класса (всего лишь один спецификатор) имеет столь значительные последствия для методологии объектно-ориентированного программирования, что мы лишний раз приведём модифицированное объявление класса A:
class A { public: virtual int Fun1(int); };
Один дополнительный спецификатор в объявлении функции и больше никаких (пока никаких) изменений в объявлениях производных классов. Как всегда, очень простая функция main(). В ней мы определяем указатель на объект базового класса, настраиваем его на объект производного типа, после чего по указателю мы вызываем функцию Fun1():
void main () { A *pObj; A MyA; AB MyAB; pObj = &MyA; pObj->Fun1(1); AC MyAC; pObj = &MyAC; pObj->Fun1(1); }
Если бы не спецификатор virtual, результат выполнения выражения вызова
pObj->Fun1(1);
был бы очевиден: как известно, выбор функции определяется типом указателя.
Однако спецификатор virtual меняет всё дело. Теперь выбор функции определяется типом объекта, на который настраивается указатель базового класса. Если в производном классе объявляется нестатическая функция, у которой имя, тип возвращаемого значения и список параметров совпадают с аналогичными характеристиками виртуальной функции базового класса, то в результате выполнения выражения вызова вызывается функция-член производного класса.
Сразу надо заметить, что возможность вызова функции-члена производного класса по указателю на базовый класс не означает, что появилась возможность наблюдения за объектом "сверху вниз" из указателя на объект базового класса. Невиртуальные функции-члены и данные по-прежнему недоступны. И в этом можно очень легко убедиться. Для этого достаточно попробовать сделать то, что мы уже однажды проделали - вызвать неизвестную в базовом классе функцию-член производного класса:
//pObj->Fun2(2); //pObj->AC::Fun1(2);
Результат отрицательный. Указатель, как и раньше, настроен лишь на базовый фрагмент объекта производного класса. И всё же вызов функций производного класса возможен. Когда-то, в разделах, посвящённых описанию конструкторов, нами был рассмотрен перечень регламентных действий, которые выполняются конструктором в ходе преобразования выделенного фрагмента памяти в объект класса. Среди этих мероприятий упоминалась инициализация таблиц виртуальных функций.
Наличие этих самых таблиц виртуальных функций можно попытаться обнаружить с помощью операции sizeof. Конечно, здесь всё зависит от конкретной реализации, но, по крайней мере, в версии Borland C++ объект-представитель класса, содержащего объявления виртуальных функций, занимает больше памяти, нежели объект аналогичного класса, в котором те же самые функции объявлены без спецификатора virtual.
cout << "Размеры объекта: " << sizeof(MyAC) << "…" << endl;
Так что объект производного класса приобретает дополнительный элемент - указатель на таблицу виртуальных функций. Схему такого объекта можно представить следующим образом (указатель на таблицу мы обозначим идентификатором vptr, таблицу виртуальных функций - идентификатором vtbl):
MyAC::= vptr A AC vtbl::= &AC::Fun1
На нашей новой схеме объекта указатель на таблицу (массив из одного элемента) виртуальных функций не случайно отделён от фрагмента объекта, представляющего базовый класс лишь пунктирной линией. Он находится в поле зрения этого фрагмента объекта. Благодаря доступности этого указателя оператор вызова виртуальной функции Fun1
pObj->Fun1(1);
можно представить следующим образом:
(*(pObj->vptr[0])) (pObj,1);
Здесь только на первый взгляд всё запутано и непонятно. На самом деле, в этом операторе нет ни одного не известного нам выражения.
Здесь буквально сказано следующее:
ВЫЗВАТЬ ФУНКЦИЮ, РАСПОЛОЖЕННУЮ ПО НУЛЕВОМУ ИНДЕКСУ ТАБЛИЦЫ ВИРТУАЛЬНЫХ ФУНКЦИЙ vtbl (в этой таблице у нас всего один элемент), АДРЕС НАЧАЛА КОТОРОЙ МОЖНО НАЙТИ ПО УКАЗАТЕЛЮ vptr.
В СВОЮ ОЧЕРЕДЬ, ЭТОТ УКАЗАТЕЛЬ ДОСТУПЕН ПО УКАЗАТЕЛЮ pObj, НАСТРОЕННОМУ НА ОБЪЕКТ MyAC. ФУНКЦИИ ПЕРЕДАЁТСЯ ДВА (!) ПАРАМЕТРА, ПЕРВЫЙ ИЗ КОТОРЫХ ЯВЛЯЕТСЯ АДРЕСОМ ОБЪЕКТА MyAC (значение для this указателя!), ВТОРОЙ - ЦЕЛОЧИСЛЕННЫМ ЗНАЧЕНИЕМ, РАВНЫМ 1.
Вызов функции-члена базового класса обеспечивается посредством квалифицированного имени.
pObj->A::Fun1(1);
В этом операторе мы отказываемся от услуг таблицы виртуальных функций. При этом мы сообщаем транслятору о намерении вызвать функцию-член базового класса. Механизм поддержки виртуальных функций строг и очень жёстко регламентирован. Указатель на таблицу виртуальных функций обязательно включается в самый "верхний" базовый фрагмент объекта производного класса. В таблицу указателей включаются адреса функций-членов фрагмента самого "нижнего" уровня, содержащего объявления этой функции.
Мы в очередной раз модифицируем объявление классов A, AB и объявляем новый класс ABC.
Модификация классов A и AB сводится к объявлению в них новых функций-членов:
class A { public: virtual int Fun1(int key); virtual int Fun2(int key); }; ::::: int A::Fun2(int key) { cout << " Fun2( " << key << " ) from A " << endl; return 0; } class AB: public A { public: int Fun1(int key); int Fun2(int key); }; ::::: int AB::Fun2(int key) { cout << " Fun2( " << key << " ) from AB " << endl; return 0; } Класс ABC является производным от класса AB: class ABC: public AB { public: int Fun1(int key); }; int ABC::Fun1(int key) { cout << " Fun1( " << key << " ) from ABC " << endl; return 0; }
В этот класс входит объявление функции-члена Fun1, которая объявляется в косвенном базовом классе A как виртуальная функция. Кроме того, этот класс наследует от непосредственной базы функцию-член Fun2. Эта функция также объявляется в базовом классе A как виртуальная. Мы объявляем объект-представитель класса ABC:
ABC MyABC;
Его схему можно представить следующим образом:
MyABC::= vptr A AB ABC vtbl::= &AB::Fun2 &ABC::Fun1
Таблица виртуальных функций сейчас содержит два элемента. Мы настраиваем указатель на объект базового класса на объект MyABC, затем вызываем функции-члены:
pObj = &MyABC; pObj->Fun1(1); pObj->Fun2(2);
В этом случае невозможно вызвать функцию-член AB::Fun1(), поскольку её адрес не содержится в списке виртуальных функций, а с верхнего уровня объекта MyABC, на который настроен указатель pObj, она просто не видна. Таблица виртуальных функций строится конструктором в момент создания объекта соответствующего объекта. Безусловно, транслятор обеспечивает соответствующее кодирование конструктора. Но транслятор не в состоянии определить содержание таблицы виртуальных функций для конкретного объекта. Это задача времени исполнения. Пока таблица виртуальных функций не будет построена для конкретного объекта, соответствующая функция-член производного класса не сможет быть вызвана. В этом легко убедиться, после очередной модификации объявления классов.
Программа невелика, поэтому имеет смысл привести её текст полностью. Не следует обольщаться по поводу операции доступа к компонентам класса ::. Обсуждение связанных с этой операцией проблем ещё впереди.
#include <iostream.h> class A { public: virtual int Fun1(int key); }; int A::Fun1(int key) { cout << " Fun1( " << key << " ) from A." << endl; return 0; } class AB: public A { public: AB() {Fun1(125);}; int Fun2(int key); }; int AB::Fun2(int key) { Fun1(key * 5); cout << " Fun2( " << key << " ) from AB." << endl; return 0; } class ABC: public AB { public: int Fun1(int key); }; int ABC::Fun1(int key) { cout << " Fun1( " << key << " ) from ABC." << endl; return 0; } void main () { ABC MyABC; // Вызывается A::Fun1(). MyABC.Fun1(1); // Вызывается ABC::Fun1(). MyABC.Fun2(1); // Вызываются AB::Fun2() и ABC::Fun1(). MyABC.A::Fun1(1); // Вызывается A::Fun1(). A *pObj = &MyABC; // Определяем и настраиваем указатель. cout << "==========" << endl; pObj->Fun1(2); // Вызывается ABC::Fun1(). //pObj->Fun2(2); // Эта функция через указатель недоступна !!! pObj->A::Fun1(2); // Вызывается A::Fun1(). }
Теперь в момент создания объекта MyABC
ABC MyABC;
из конструктора класса AB (а он вызывается раньше конструктора класса ABC), будет вызвана функция A::Fun1(). Эта функция является членом класса A. Объект MyABC ещё до конца не сформирован, таблица виртуальных функций ещё не заполнена, о существовании функции ABC::Fun1() ещё ничего не известно. После того, как объект MyABC будет окончательно сформирован, таблица виртуальных функций заполнится, а указатель pObj будет настроен на объект MyABC, вызов функции A::Fun1() через указатель pObj будет возможен лишь с использованием полного квалифицированного имени этой функции:
pObj->Fun1(1); // Это вызов функции ABC::Fun1()! pObj->A::Fun1(1); // Очевидно, что это вызов функции A::Fun1()!
Заметим, что вызов функции-члена Fun1 непосредственно из объекта MyABC приводит к аналогичному результату:
MyABC.Fun1(1); // Вызов функции ABC::Fun1().
А попытка вызова невиртуальной функции AB::Fun2() через указатель на объект базового класса заканчивается неудачей. В таблице виртуальных функций адреса этой функции нет, а с верхнего уровня объекта "посмотреть вниз" невозможно.
//pObj->Fun2(2); // Так нельзя!
Результат выполнения этой программки наглядно демонстрирует специфику использования виртуальных функций. Всего несколько строк…
Fun1(125) from A. Fun1(1) from ABC. Fun1(5) from ABC. Fun2(1) from AB. Fun1(1) from A. ========== Fun1(2) from ABC. Fun1(2) from A.
Один и тот же указатель в ходе выполнения программы может настраиваться на объекты-представители различных производных классов. В результате в буквальном смысле одно и то выражение вызова функции-члена обеспечивает выполнение совершенно разных функций. Впервые мы сталкиваемся с так называемым ПОЗДНИМ или ОТЛОЖЕННЫМ СВЯЗЫВАНИЕМ.
Заметим, что спецификация virtual относится только к функциям. Виртуальных данных-членов не существует. Это означает, что не существует возможности обратиться к данным-членам объекта производного класса по указателю на объект базового класса, настроенному на объект производного класса.
С другой стороны, очевидно, что если можно вызвать замещающую функцию, то непосредственно "через" эту функцию открывается доступ ко всем функциям и данным-членам членам производного класса и далее "снизу-вверх" ко всем неприватным функциям и данным-членам непосредственных и косвенных базовых классов. При этом из функции становятся доступны все неприватные данные и функции базовых классов.
И ещё один маленький пример, демонстрирующий изменение поведение объекта-представителя производного класса после того, как одна из функция базового класса становится виртуальной.
#include <iostream.h> class A { public: void funA () {xFun();}; /*virtual*/void xFun () {cout <<"this is void A::xFun();"<< endl;}; }; class B: public A { public: void xFun () {cout <<"this is void B::xFun ();"<<endl;}; }; void main() { B objB; objB.funA(); }
В начале спецификатор virtual а определении функции A::xFun() закомментирован. Процесс выполнения программы состоит в определении объекта-представителя objB производного класса B и вызова для этого объекта функции-члена funA(). Эта функция наследуется из базового класса, она одна и очевидно, что её идентификация не вызывает у транслятора никаких проблем. Эта функция принадлежит базовому классу, а это означает, что в момент её вызова, управление передаётся "на верхний уровень" объекта objB. На этом же уровне располагается одна из функций с именем xFun(), и именно этой функции передаётся управление в ходе выполнения выражения вызова в теле функции funA(). Мало того, из функции funA() просто невозможно вызвать другую одноименную функцию. В момент разбора структуры класса A транслятор вообще не имеет никакого представления о структуре класса B. Функция xFun() - член класса B оказывается недостижима из функции funA().
Но если раскомментировать спецификатор virtual в определении функции A::xFun(), между двумя одноименными функциями установится отношение замещения, а порождение объекта objB будет сопровождаться созданием таблицы виртуальных функций, в соответствии с которой будет вызываться замещающая функция член класса B. Теперь для вызова замещаемой функции необходимо использовать её квалифицированное имя:
void A::funA () { xFun(); A::xFun(); }
Назад | Содержание | Вперед