Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
OOP_otvety_k_ekzamenu.doc
Скачиваний:
55
Добавлен:
13.04.2015
Размер:
786.94 Кб
Скачать

38. Виртуальные базовые классы. Синтаксис, структура в памяти, особенности применения и реализации. Понятие “самого производного” класса и его роль в организации работы виртуальных базовых классов.

Внутренняя структура объектов в данной иерархии достаточно сложна. Поскольку только самый производный класс (т.е. класс, объект которого фактически создается в данный момент) может знать полный набор всех междуклассовых отношений, то именно самый производный класс решает в каком месте в результирующем объекте расположить содержимое виртуального базового. Кроме того, базовые классы, не имея возможности определить местоположение виртуального базового класса самостоятельно, должны опираться на некоторые дополнительные данные, позволяющие узнать отступ от начала объекта до начала содержимого виртуального базового класса. Т.е., если взять, например, класс Printer, и создать объект Printer, то местоположение содержимого класса PoweredDevice в таком объекте будет отличаться от местоположения в объекте Copier, несмотря на то, что Copier - это подвид Printer.

Существует 2 типовые схемы реализации виртуального наследования, различные между собой - одна из реализаций применяется в компиляторе компании Microsoft, другая - в компиляторе GCC. Начнем с компилятора Microsoft.

Предположим, создается конкретный объект класса Printer. Отладчик показывает его размер как 24 байта (VisualStudio 2010, 32-битный режим, настройки по умолчанию). Что же входит в этот размер? Считаем размер информационных полей:

  • m_nominalPower (4 байта);

  • m_turned (1 байт);

  • m_pagesPerMinute (4 байта).

Из этого расчета выходит, что объект должен содержать 9 байт, а ни как не 24. На что же уходит еще 15 байт?

Во-первых, компилятор применяет схемы выравнивания структур по границе (настройка по умолчанию = 4 байта), поэтому поле m_turned занимает не 1 байт, а 4. Остается 12 байт.

Во-вторых, абстрактный класс PoweredDevice содержит виртуальный деструктор. Это резервирует в объекте место под указатель vptr. Остается 8 байт.

В-третьих, класс Printer вводит собственную виртуальную функцию printDocument. В такой сложной иерархии классов не получается обходиться только одной таблицей виртуальных функций из-за сложных смещений, потому существует второй указатель vptr.. Остается 4 байта.

Наконец, последние 4 байта расходуются на еще один служебный указатель, который называется vbptr (virtual base class pointer), и представляет собой адрес массива, содержащего смещения (число байт) виртуальных базовых классов относительно местоположения указателя vbptr. В нашем случае, виртуальный базовый класс только один, потому массив будет содержать одну полезную запись. Если добавить в иерархию еще несколько виртуальных базовых классов, это дополнит данный массив, не вводя новых указателей в память объектов.

Графически это можно представить следующим образом:

В начале объекта Printer находится собственный указатель на таблицу виртуальных функций, добавленных данным классом (vptr_1). Далее следует указатель на массив смещений базовых классов (vbptr) в объекте относительно этого же указателя. Элемент с индексом 0 - это смещение самого класса Printer, т.е. на 4 байта назад. Элемент с индексом 1 - это уже смещение виртуального базового класса PoweredDevice относительно vbptr. Далее следует поле m_pagesPerMinute, за которым размещаются члены класса PoweredDevice. Первым из них является еще один указатель vptr для соответствующих классу PoweredDevice виртуальных функций.

Соответственно, любой код, который имея указатель или ссылку на объект класса Printer, при этом обращающийся к членам унаследованного виртуального базового класса PoweredDevice, делает следующие действия:

Printer * p = new Printer( 300, 15 );

std::cout << p->getNominalPower();

  • из адреса p извлекается указатель vbptr;

  • из указателя vbptr извлекается 2 смещения по индексу 1 и по индексу 0;

  • извлеченный смещения складываются, формируя смещение объекта PoweredDevice относительно начала объекта Printer;

  • выполняется вызов метода getNominalPower (если, конечно, он не будет встраиваться в месте вызова), и ему передается адрес this, равный адресу в указателе p + нужное смещение.

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

Внутренняя структура объекта Scanner в целом формируется аналогичным образом. Интерес представляет объект Copier, с размером 36 байт. Внутренняя структура данного объекта выглядит следующим образом:

Такой сложный объект состоит из 4 информационных полей (16 байт), 3 указателей vptr по каждому из базовых классов (12 байт) и 2 указателей vbptr на виртуальный базовый класс из промежуточных базовых классов (8 байт). Итого, 36 байт.

При внимательном чтении кода примера должен вызвать удивление выделенный код в списке инициализации конструктора Copier:

// Реализация конструктора

Copier::Copier ( int _nominalPower, int _scanDPI, int _pagesPerMinute )

// Вызов конструктора виртуального базового класса через всю иерархию!

: PoweredDevice( _nominalPower )

// Вызов конструкторов обычных базовых

, Scanner( _nominalPower, _scanDPI )

, Printer( _nominalPower, _pagesPerMinute )

{

}

Т.е., конструктор класса Copier, состоящих в двух уровнях иерархии наследования от класса PoweredDevice осуществляет вызов данного конструктора. При чем, это действие выполняется раньше, чем произойдут вызовы конструкторов Scanner и Printer, для которых общая часть виртуального базового класса должна быть уже инициализированной.

Подытожим обнаруженные нестыковки с ранее изученным материалом. Если в иерархии наследования имеется виртуальный базовый класс, то его местоположение в объекте определяется непосредственным конкретным классом, к которому он относится. Только самый производный класс иерархии (“most derived” class) знает точное местоположение виртуального базового. Местоположение может отличаться для каждого конкретного класса в иерархии. Чтобы промежуточные базовые классы могли работать с членами виртуального базового, самый производный класс должен обеспечить корректную инициализацию указателей vbptr в собственном конструкторе. Это можно увидеть при дизассебмлировании. Конструктор класса Copier содержит такой код, который устанавливает два указателя vbptr в нужное значение:

01378E72 mov eax,dword ptr [ebp-14h]

01378E75 mov dword ptr [eax+4],offset Copier::`vbtable' (139BE8Ch)

01378E7C mov eax,dword ptr [ebp-14h]

01378E7F mov dword ptr [eax+10h],offset Copier::`vbtable' (139BE80h)

01378E86 mov eax,dword ptr [ebp+8]

В то же время, класс Printer (и Scanner аналогично), конструкторы которых вызываются раньше Copier, содержат такой код:

01388923 mov eax,dword ptr [this]

01388926 mov dword ptr [eax+4],offset Printer::`vbtable' (139D840h)

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

PoweredDevice::PoweredDevice

Printer::Printer

Scanner::Scanner

Copier::Copier

Если будет создан объект класса Printer, его порядок конструирования такой:

PoweredDevice::PoweredDevice

Printer::Printer

Если будет создан объект класса Scanner, его порядок конструирования такой:

PoweredDevice::PoweredDevice

Scanner::Scanner

Возникает вопрос - если конструкторы классов Printer и Scanner вызывают конструктор класса PoweredDevice, то каким же образом достигается единственность его вызова при конструировании объекта Copier? Хитрость состоит в передаче в конструкторы классы иерархии еще одного неявного целого аргумента, означающего является ли класс текущего конструктора “самым производным”. Этот аргумент не увидеть нигде, кроме дизассебмлера. Предположим создается объект Copier. Код, который инициирует создание объекта, всегда знает конкретный тип, поскольку фактически его указывает, соответственно в этом случае передается 1:

Copier c( 500, 300, 15 );

01302285 push 1

01302287 push 0Fh

01302289 push 12Ch

0130228E push 1F4h

01302293 lea ecx,[ebp-34h]

01302296 call Copier::Copier (12E3311h)

Аналогично, если создать объект класса Printer, подобная единица будет неявно передана его конструктору:

Printer p( 300, 15 );

013E223D push 1

013E223F push 0Fh

013E2241 push 12Ch

013E2246 lea ecx,[ebp-28h]

013E2249 call Printer::Printer (13C349Ch)

В то же время, когда объект Copier в собственном конструкторе вызывает конструктор класса Printer, вместо единицы передается 0, поскольку в таком случае Printer не является “самым производным” классом:

01378EC4 push 0

01378EC6 mov eax,dword ptr [ebp+10h]

01378EC9 push eax

01378ECA mov ecx,dword ptr [ebp+8]

01378ECD push ecx

01378ECE mov ecx,dword ptr [ebp-14h]

01378ED1 add ecx,0Ch

01378ED4 call Printer::Printer (137349Ch)

Этот аргумент используется в конструкторах Printer, Scanner и Copier таким образом, чтобы вызывать конструктор виртуального базового класса только при переданном значении 1 (т.е., когда текущий класс является “самым производным”). Это можно выразить следующим псевдокодом конструктора Printer:

Printer::Printer ( Printer * this,

int _nominalpower,

int _pagesPerMinute,

int mostDerived )

{

if ( mostDerived )

PoweredDevice::PoweredDevice( this + vbptr[ 0 ] + vbptr[ 1 ], _nominalPower );

this->m_pagesPerMinute = _pagesPerMinute;

}

Аналогично, конструктор класса Copier содержит нечто подобное:

Copier::Copier ( Copier * this,

int _nominalpower,

int _pagesPerMinute,

int _scanDPI,

int mostDerived )

{

if ( mostDerived )

PoweredDevice::PoweredDevice( this + offset3, _nominalPower );

Printer::Printer( this + offset1, _nominalPower, _pagesPerMinute, 0 );

Scanner::Scanner( this + offset2, _nominalPower, _scanDPI, 0 );

}

Похожим образом обеспечивается обратный порядок вызова деструкторов:

Copier::~Copier

Scanner::~Scanner

Printer::~Printer

PoweredDevice::~PoweredDevice

Реализация виртуального базового наследования в других компиляторах, в частности в компиляторе GCC - основном компиляторе на платформе Linux, отличается иным размещением смещений виртуальных базовых классов. Указатели vbptr и дополнительные таблицы vbtable здесь вообще не используются. Вместо этого, смещения размещаются в таблице виртуальных функций по отрицательным индексам. Если словосочетание “отрицательные индексы” в массиве вызывает недоумение, следует вспомнить, что vptr является указателем, а значит может указывать на любой из элементов массива. Разумеется, нумерация ячеек в самой таблице начинается с 0, однако в начале таблицы помещают смещения виртуальных базовых классов, а только за ними - адреса виртуальных функций. Указатель vptr устанавливается таким образом, чтобы по нулевому смещению размещался адрес первой виртуальной функции. Соответственно, все предыдущие данные становятся доступны по отрицательным индексам.

Такая структура является более компактной по сравнению с реализацией в компиляторе Microsoft, несмотря на более сложную для восприятия схему реализации. Для объектов Printer и Scanner экономия составляет 1 указатель на каждом объекта, а для Copier - целых 2 указателя.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]