Виртуальные функции

Очередная модификация базового класса приводит к неожиданным последствиям. Эта модификация состоит в изменении спецификатора функции-члена базового класса. Мы (впервые!) используем спецификатор 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();
 }

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