|
Программирование >> Оптимизация возвращаемого значения
Объект С1 Объект С1
Объект С2 С1 vtbl
Реализация виртуальных функций С1 Реализация виртуальных функций С2 Рис. 4.4 Рассмотрим фрагмент программы: void makeACall(Cl *рС1) { pCl->fl{) ; Это вызов виртуальной функции f 1 при помощи указателя рС1. Здесь нельзя понять, какую из функций f 1 (С1: : f 1 или С2 : : f 1) необходимо вызвать, так как рС1 может указывать как на объект С1, так и на объект С2. Компилятор, тем не менее, должен сгенерировать код для вызова функции fl внутри функции makeACall, причем должна вызываться правильная функция независимо от того, на что указывает рС1. Для этого компиляторы генерируют код, выполняющий следующее: Предположим, имеется программа, содержащая несколько объектов типов С1 и С2. Если задано описанное отношение между объектами, указателями виртуальных таблиц и виртуальными таблицами, объекты в программе можно представить себе примерно так, как они изображены на рис. 4.4. 1. Проследить указатель виртуальной таблицы до соответствующей виртуальной таблицы. Это простая операция, так как компиляторы знают положение указателя виртуальной таблицы в объекте. (В конце концов, они сами его туда поместили.) В результате ресурсы системы тратятся только на вычисление смещения (для получения указателя виртуальной таблицы) и доступ по указателю (для получения виртуальной таблицы). 2. Найти указатель виртуальной таблицы, соответствующий вызываемой функции (в данном примере функции f 1). Это также несложно, поскольку компиляторы присваивают каждой виртуальной функции уникальный индекс в таблице. Затраты на этот шаг сводятся просто к вычислению смещения в массиве виртуальной таблицы. 3. Вызвать функцию, на которую ссылается указатель, найденный в шаге 2. Если представить, что каждый объект имеет невидимый элемент данных vpt г и что индекс виртуальной таблицы функции f 1 равен i, то оператор pCl->fl{); будет генерировать код (*pCl->vptr[i])(pCl); Вызывать функцию, на которую указывает i-й элемент виртуальной таблицы, заданной pCl->vptr; указатель рС1 передается функции со значением this. Данный подход почти так же эффективен, как и вызов невиртуальной функции: на большинстве компьютеров при этом выполняется всего на несколько команд больше. Виртуальные функции сами по себе обычно не влияют на производительность. Реальные затраты на виртуальные функции во время выполнения программы связаны с тем, что из практических соображений они не реализуются как встроенные. Директива inline означает в процессе компиляции подставить вместо вызова функции ее тело , но виртуальная предполагает определить вызываемую функцию во время выполнения программы . Если компилятор не знает, какая функция будет вызвана, то он не сможет сделать ее встроенной. Таким образом, надо сделать так, чтобы не реализовывать виртуальные функции как встроенные. (Можно делать виртуальные функции встроенными при их вызове с помощью объектов, но большинство виртуальных функций вызывается при помощи указателей или ссылок на объекты, а такие вызовы не бывают встроенными. А поскольку эти вызовы общеприняты, виртуальные функции обычно не делаются встроенными.) Все сказанное до сих пор относится и к одиночному, и к множественному наследованию, но во втором случае картина усложняется. Нет смысла вдаваться в детали, но при множественном наследовании вычислить смещение, чтобы найти указатели виртуальной таблицы в объектах, намного сложнее; в одном объекте содержится несколько таких указателей (по одному для каждого из базовых классов); и кроме отдельных виртуальных таблиц должны создаваться еще специальные виртуальные таблицы для базовых классов. В результате возрастают расходы памяти, class А { ... } ; class В: virtual public А { ... } ; class С: virtual public А { ... } ; class D: public В, public С { ... } ; Рис. 4.5 В этом примере А является виртуальным базовым классом, так как классы В и С виртуально наследуют от него. В некоторых компиляторах (особенно старых) объект типа D может иметь вид, представленный на рис. 4.6. Кажется немного странным помещать элементы данных базового класса в конец объекта, но зачастую это делается именно так. Конечно же, в разных реализациях память организована по-разному, поэтому предложенный рисунок Данные класса В Vuj.iiri нииртушнии G1.JR .яи>лх Данные класса С Указатель на виртуальный базовый класс Данные класса D Данные класса А Рис. 4.6 используемой классами и объектами для виртуальных функций, и немного увеличивается стоимость вызова функции во время выполнения программы. При множественном наследовании часто приходится создавать виртуальные базовые классы. В противном сл5ае, если производный класс имеет более одного пути наследования от базового класса, элементы данных этого базового класса копируются в каждый объект производного класса, по одному экземпляру для каждого пути между производным и базовым классами. Программисты обычно пытаются не допустить такого копирования, сделав базовые классы виртуальными. Но включение в код виртуальных базовых классов все равно приводит к дополнительным издержкам, поскольку их реализация часто использует указатели на части виртуального базового класса, чтобы избежать копирования, и в объектах могут храниться один или более таких указателей. Рассмотрим следующий пример, который я называю ужасным бриллиантом множественного наследования (см. рис. 4.5).
|
© 2006 - 2024 pmbk.ru. Генерация страницы: 0
При копировании материалов приветствуются ссылки. |