Главная Обратная связь

Дисциплины:

Архитектура (936)
Биология (6393)
География (744)
История (25)
Компьютеры (1497)
Кулинария (2184)
Культура (3938)
Литература (5778)
Математика (5918)
Медицина (9278)
Механика (2776)
Образование (13883)
Политика (26404)
Правоведение (321)
Психология (56518)
Религия (1833)
Социология (23400)
Спорт (2350)
Строительство (17942)
Технология (5741)
Транспорт (14634)
Физика (1043)
Философия (440)
Финансы (17336)
Химия (4931)
Экология (6055)
Экономика (9200)
Электроника (7621)


 

 

 

 



Виртуальные классы. Порядок вызова конструкторов и деструкторов



Мы продолжаем модификацию последнего варианта нашей программы, добавляя к прототипу функции int A::Fun1(int); спецификатор virtual.

class A { public: int x0; virtual int Fun1(int key); };

Результат выполнения программы можно предугадать. Функция-член класса A становится виртуальной, а значит, замещается в соответствии с таблицей виртуальных функций в ходе сборки объекта-представителя производного класса D. В состав этого объекта включены два независимых друг от друга базовых фрагмента-представителя базового класса A. Количество таблиц виртуальных функций соответствует количеству базовых фрагментов, представителей базового класса, содержащего объявления виртуальных функций.

Как бы мы ни старались, вызвать функцию-член класса A из любого фрагмента объекта-представителя производного класса D невозможно. Сначала конструкторы строят объект, настраивают таблицы виртуальных функций, а потом уже мы сами начинаем его "перекраивать", создавая на основе базового фрагмента видимость самостоятельного объекта. Напрасно. Объект построен, таблицы виртуальных функций также настроены. До конца жизни объекта виртуальные функции остаются недоступны.

Следует обратить особое внимание на то обстоятельство, что независимо от места вызова виртуальной функции (а мы её вызываем непосредственно из базовых фрагментов объекта), замещающей функции в качестве параметра передаётся корректное значение this указателя.

Очевидно, что соответствующая корректировка значения этого указателя производится в процессе вызова замещающей функции. При этом возможны, по крайней мере, два различных подхода к реализации алгоритма корректировки.

Соответствующее корректирующее значение может определяться в момент создания объекта конструкторами и храниться в виде константной величины вместе с таблицами виртуальных функций, либо this указатель может динамически настраиваться в момент вызова виртуальной функции благодаря специальному программному коду настройки этого указателя. Но это всё уже зависит от конкретной реализации языка.

В этом разделе нам осталось обсудить понятие виртуального базового класса. Согласно соответствующей БНФ, спецификатор virtual может быть включён в описатель базы:

ОписательБазы ::= ПолноеИмяКласса ::= [virtual] [СпецификаторДоступа] ПолноеИмяКласса ::= [СпецификаторДоступа] [virtual]ПолноеИмяКласса

Модифицируем нашу программу. Мы добавим в описатели баз производных классов B и C спецификатор virtual:

class A { public: int x0; int Fun1(int key); }; class B: virtual public A { public: int x0; int Fun1(int key); int Fun2(int key); }; class C: public virtual A { public: int x0; int Fun2(int key); }; class D: public B, public C { public: int x0; int Fun1(int key); };

Вот как выглядит после модификации граф производного класса D:

A B C D

А вот как меняется структура класса D, представляемая в виде неполной схемы. Спецификатор virtual способствует минимизации структуры производного класса. Виртуальные базовые классы не тиражируются.

A B C D

А вот и схема объекта-представителя класса D.

D MyD; MyD ::= A (int)x0; (int)xA; B (int)xB; C (int)x0; D (int)x0; (int)xD;

Спецификатор virtual в описании базы позволяет минимизировать структуру объекта. Различные варианты обращения к данным-членам базового фрагмента приводят к модификации одних и тех же переменных.

Базовый фрагмент объекта связан со своими производными фрагментами множеством путей, по которым с одинаковым успехом может быть обеспечен доступ к данным-членам базового фрагмента.

В C++ допускаются такие варианты объявления производных классов, при которых одни и те же классы одновременно выступают в роли виртуальных и невиртуальных базовых классов. Например, сам класс D может быть использован в качестве базового класса:

class F: public A { ::::: } class G: public A, public D { ::::: } ::::: G MyG;

Множество одноименных виртуальных и невиртуальных базовых фрагментов, данных-членов, простых и виртуальных функций. Можно до бесконечности рисовать направленные ациклические графы, диаграммы классов и объектов… Искусство объявления классов и навигации по фрагментам объектов совершенствуется в результате напряжённой длительной практики.

Введём новое понятие, связанное с доступом к данным и функциям-членам производных классов.

Имя x (имя переменной, класса, или функции), объявленное в классе X, обозначается как X::x. Имя B::f доминирует над именем A::f, если объявление класса B содержит в списке баз имя класса A.

На понятии доминирования имён основывается правило доминирования, определяющее корректность доступа к членам производного класса, объявленного на основе виртуальных базовых классов.

Это правило гласит, что возможность достижения по пути направленного ациклического графа нескольких одноименных функций либо данных-членов приводит к неоднозначности, за исключением случая, когда между именами существует отношение доминирования.

 

 

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

Виртуальный метод (виртуальная функция) — в объектно-ориентированном программировании метод (функция) класса, который может быть переопределён в классах-наследниках так, что конкретная реализация метода для вызова будет определяться во время исполнения. Таким образом, программисту необязательно знать точный тип объекта для работы с ним через виртуальные методы: достаточно лишь знать, что объект принадлежит классу или наследнику класса, в котором метод объявлен.

Виртуальные методы — один из важнейших приёмов реализации полиморфизма. Они позволяют создавать общий код, который может работать как с объектами базового класса, так и с объектами любого его класса-наследника. При этом базовый класс определяет способ работы с объектами и любые его наследники могут предоставлять конкретную реализацию этого способа.

Базовый класс может и не предоставлять реализации виртуального метода, а только декларировать его существование. Такие методы без реализации называются «чисто виртуальными» (калька с англ. pure virtual) или абстрактными. Класс, содержащий хотя бы один такой метод, тоже будет абстрактным. Объект такого класса создать нельзя (в некоторых языках допускается, но вызов абстрактного метода приведёт к ошибке). Наследники абстрактного класса должны предоставить реализацию для всех его абстрактных методов, иначе они, в свою очередь, будут абстрактными классами.

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

 

 

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

 

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

 

 



Просмотров 902

Эта страница нарушает авторские права




allrefrs.su - 2024 год. Все права принадлежат их авторам!