все свойства базовых, а затем переопределяет некоторые их функции
и добавляет новые. В принципе ничто не препятствует на любом
уровне разработки перейти к традиционному программированию и создавать линейную программу, используя объекты уже существующих
классов. Следование же технологии объектно-ориентированного программирования "до конца" предполагает, что прикладная программа
представляет собой класс самого верхнего уровня, в ее выполнение
- создание объекта этого класса или выполнение для него некоторой функции типа "run".
Лекция 7. Виртуальные функции.
-----------------------------
7.1 Понятие виртуальной функции
------------------------------
Достаточно часто программисту требуется создавать структуры данных, включающих в себя переменное число объектов различных типов. Для представления их в программах используются списки или массивы ссылок на эти объекты. Объекты разных классов имеют соответственно различные типы ссылок, а для хранения в массиве или списке требуется один тип ссылок. Для преодоления этого противоречия все эти классы объектов требуется сделать производными от одного и того же базового класса, а при записи в массив преобразовывать ссылку на объект производного класса в ссылку на объект базового.
p[] A1
+---+ -b---------¬
¦ --------------------->-a-------¬¦======== b::f()
+---+ ¦L---------¦===¬
¦ ------------¬ L----------- ¦
+---+ ¦ C1 ¦
¦ ----------¬ ¦ -c---------¬ ¦
+---+ ¦ L-------->-a-------¬¦======== c::f()
¦ ¦L---------¦===¦
¦ L----------- ¦
¦ A1 ¦
L---------->-a-------¬ ===¦==== a::f()
L--------
class a
{ ... void f(); };
class b : public a
{ ... void f(); };
class c : public a
{ ... void f(); };
a A1;
b B1;
c C1;
a *p[3]; // Массив ссылок на объекты БК
p[0] = &B1; // Ссылки на объекты БК в
p[1] = &C1; // объектах ПК
p[2] = &A1;
Однако при таком преобразовании типа "ссылка на объект ПК" к
типу "ссылка на объект БК" происходит потеря информации о том,
какой объект производного класса "окружает" доступный через ссылку объект базового класса. Поэтому вместо переопределенных функций в производных классах будут вызываться функции в базовом, то
есть
p[0]->f(); // Вызов a::f()
p[1]->f(); // во всех случаях, хотя f()
p[2]->f(); // переопределены
Однако по логике поставленной задачи требуется, чтобы вызываемая функция соответствовала тому объекту, который реально находится под ссылкой. Наиболее просто это сделать так:
- хранить в объекте базового класса идентификатор "окружающего" его производного класса;
- в списке или таблице хранить ссылки на объект базового
класса;
- при вызове функции по ссылке на объект базового класса
идентифицировать тип производного класса и явно вызывать для него
переопределенную функцию;
- идентификатор класса устанавливать при создании объекта ,
то есть в его конструкторе.
class a
{
public: int id; // Идентификатор класса
void f();
void newf(); // Новая функция f() с идентификацией ПК
}
a::a() // Конструкторы объектов
{ ...
id = 0;
}
b::b()
{ ...
id = 1;
}
c::c()
{ ...
id = 2
}
void a::newf()
{
switch (id)
{
case 0: a::f(); break;
case 1: b::f(); break;
case 2: c::f(); break;
}
}
p[0]->newf(); // Вызов b::f() для B1
p[1]->newf(); // Вызов c::f() для C1
p[2]->newf(); // Вызов a::f() для А1
Отсюда следует определение виртуальной функции. Виртуальная функция (ВФ) - это функция, определяемая в базовом и наследуемая или переопределяемая в производных классах. При вызове ее по ссылке на объект базового класса происходит вызов той функции, которая соответствует классу объекта, включающему в себя данный объект базового класса.
Таким образом, если при преобразовании типа "ссылка на ПК" к типу "ссылка на БК" происходит потеря информации об объекте производного класса, то при вызове виртуальной функции происходит обратный процесс неявного восстановления типа объекта.
Реализация механизма виртуальных функций заключается в создании компилятором таблицы адресов виртуальных функций (ссылок).
Такая таблица создается для базового класса и для каждого включения базового класса в производный. В объекте базового класса создается дополнительный элемент - ссылка на таблицу адресов его виртуальных функций. Эта ссылка устанавливается конструктуром при создании объекта производного класса. При вызове виртуальной функции по ссылке на объект базового класса из объекта берется ссылка на таблицу функций и из нее берется адрес функции по фиксированному смещению. Ниже иллюстрируется реализация этого механизма (подчеркнуты элементы, создаваемые неявно компилятром).
class A
{
------> void (**ftable)(); // Ссылка на таблицу адресов
// виртуальных функций
public:
virtual void x();
virtual void y();
virtual void z();
A();
~A();
};
// Таблица адресов функций класса А
------> void (*TableA[])() = { A::x, A::y, A::z };
A::A()
{
------> ftable = TableA; // Установка таблицы для класса А
}
class B : public A
{
public:
void x();
void z();
B();
~B();
};
// Таблица адресов функций класса A
// в классе B
--> void (*TableB[])() = { B::x, A::y, B::z };
¦ L переопределяется в B
B::B() L------ наследуется из A
{
--> ftable = TableB; // Установка таблицы для класса B
}
void main()
{
A* p; // Ссылка p базового класса A
B nnn; // ссылается на объект производp = &nnn; // ного класса B
реализация
p->z(); ------------------> (*(p->ftable[2]))();
}
p nnn TableB B::z()
-----¬ -------->--B-----¬ ----->---------¬ --->----------¬
¦ ------ ftable¦--A---¬¦ ¦ 0+--------+ ¦ ¦ ¦
L----- ¦¦ ------ 1+--------+ ¦ ¦ ¦
¦+-----+¦ 2¦ --------- L--------- ¦¦ ¦¦ L--------
7.2 Абстрактные классы
---------------------
Если базовый класс используется только для порождения производных классов, то виртуальные функции в базовом классе могут
быть "пустыми", поскольку никогда не будут вызваны для объекта
базового класса. Такой базовый класс называется абстрактным. Виртуальные функции в определении класса обозначаются следующим образом:
class base
{
public:
virtual print() =0;
virtual get() =0;
}
Естественно, что определять тела этих функций не требуется.
7.3 Множественное наследование и виртуальные функции
---------------------------------------------------
Множественным наследованием называется процесс создания производного класса из двух и более базовых. В этом случае производный класс наследует данные и функции всех своих базовых классов.
Существенным для реализации множественного наследования является
то, что адреса объектов второго и т.д. базовых классов не совпадают с адресом объекта производного и первого базового классов,
то есть имеют фиксированные смещения относительно начала объекта:
class d : public a,public b, public c { };
d D1;
pd = &D1; // #define db sizeof(a)
pa = pd; // #define dc sizeof(a)+sizeof(b)
pb = pd; // pb = (char*)pd + db
pc = pd; // pc = (char*)pd + dc
D1
pd -------------------->-d---------¬
pa --------------------->-a-------¬¦T T
¦¦ ¦¦¦ ¦ db = sizeof(a)
¦L---------¦¦ +
pb --------------------->-b-------¬¦¦ dc = sizeof(a) + sizeof(b)
¦L---------¦¦
pc --------------------->-c-------¬¦+
¦L---------¦
¦ ¦
L----------
Преобразование ссылки на объект производного класса к ссылке
на объект базового класса требует добавления к указателю текущего
объекта this соответствующего смещения (db,dc), обратное преобразование - вычитание этого же смещения. Такое действие выполняется
компилятором, когда в объекте производного класса наследуется
функция из второго и т.д. базового класса, например при определении в классе "b" функции "f()" и ее наследовании в классе "d" вызов D1.f() будет реализован следующим образом:
this = &D1; // Адрес объекта производного класса
this = (char*)this + db // Адрес объекта класса b в нем
b::f(this); // Вызов функции в классе b со своим
// объектом
Рассмотрим особенности механизма виртуальных функций при
множественном наследовании. Во-первых, на каждый базовый класс в
производном классе создается своя таблица виртуальных функций (в
нашем случае - для "a" в "d", для "b" в "d" и для "c" в "d").
Во-вторых, если функция базового класса переопределена в производном, то при вызове виртуальной функции требуется преобразовать
ссылку на объект базового класса в ссылку на объект производного,
то есть для второго и т.д. базовых классов вычесть из this соответствующее смещение. Для этого транслятор включает соответствующий код, корректирующий значение this в виде "заплаты", передающей управление командой перехода к переопределяемой функции.
class a
{
public: virtual void f();
virtual void g();
};
class b
{
public: virtual void h();
virtual void t();
};
class c : public a, public b
{ // f(),t() наследуются
public: void g(); // g() переопределяется
void h(); // h() переопределяется
}
a A1;
b B1;
c C1;
pa = &A1;
pb = &B1;
pa->f(); // Вызов a::f()
pb->h(); // Вызов b::h()
pa = &C1;
pb = &C1;
pa->f(); // Вызов a::f()
pa->g(); // Вызов c::g()
pb->h(); // Вызов c::h()
pb->t(); // Вызов b::t()
Таблицы виртуальных функций для данного примера имеют вид:
A1
-a----¬ Таблица ВФ для "a"
¦ ------------>--------¬
+-----+ ¦a::f() ¦
L------ +-------+
¦a::g() ¦
L------- B1
-b----¬ Таблица ВФ для "b"
¦ ------------>--------¬
+-----+ ¦b::h() ¦
L------ +-------+
¦b::t() ¦
L------- C1
T --c-----¬ Таблица ВФ для "a" в "c"
¦ ¦--a---¬¦ --------¬
db ¦ ¦¦ ----------->¦a::f() ¦
¦ ¦L------¦ +-------+
+ ¦--b---¬¦ ¦c::g() ¦
¦¦ -------¬ L------- ¦L------¦ ¦ Таблица ВФ для "b" в "c"
¦ ¦ ¦
¦ ¦ L--->--------¬ "Заплата" для c::h()
L-------- ¦ xxx()----->--xxx()----------------¬
+-------+ ¦ this=(char*)this - db¦
¦b::t() ¦ ¦ goto c::h ¦
L-------- L----------------------
Другим вариантом решения проблемы является хранение необходимых смещений в самих таблицах виртуальных функций.
7.4. Виртуальные базовые классы
------------------------------
В процессе иерархического определения производных классов
может получиться, что в объект производного класса войдут
несколько объектов базового класса, например
class base {}
class a : public base {}
class b : public base {}
class c : a, b {}
В классе "c" присутствуют два объекта класса base. Для исключения такого дублирования объект базового класса должен быть