Урок №167. Виртуальные таблицы

  Юрий Ворон  | 

    | 

  Обновлено 20 Окт 2018  | 

 528

Для реализации виртуальных функций C++ использует специальную форму позднего связывания —  виртуальные таблицы. Виртуальная таблица – это таблица поиска функций для выполнения вызовов функций в режиме позднего (динамического) связывания. Виртуальную таблицу еще называют «vtable», «таблица виртуальных функций» или «таблица виртуальных методов».

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

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

Во-вторых, компилятор также добавляет скрытый указатель на родительский класс, который мы будем называть *__vptr. Этот указатель автоматически создается при создании объекта класса и указывает на виртуальную таблицу этого класса. В отличие от скрытого указателя *this, который фактически является параметром функции, используемый компилятором для «указания на самого себя», *__vptr является реальным указателем. Следовательно, размер каждого объекта увеличивается на размер этого указателя. *__vptr также наследуется дочерними классами.

Сейчас вы можете быть немного сконфужены, как это всё работает вместе, поэтому рассмотрим следующий простой пример:

У нас здесь 3 класса, соответственно, компилятор создаст 3 виртуальные таблицы: одна для Parent, одна для C1 и одна для C2.

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

При создании объектов классов Parent, C1 или C2, *__vptr будет указывать на виртуальную таблицу класса Parent, C1 или C2 соответственно.

Как заполняются виртуальные таблицы?

Из примера выше у нас есть только две виртуальные функции, поэтому каждая виртуальная таблица будет иметь две записи (одна для function1() и одна для function2()). Помните, что при заполнении виртуальных таблиц выбираются наиболее дочерние методы, доступ к которым имеют объекты.

Виртуальная таблица для объектов класса Parent проста. Объект класса Parent имеет доступ только к членам класса Parent, он не имеет доступа к членам классов C1 и C2. Следовательно, запись function1 будет указывать на Parent::function1(), а запись function2 будет указывать на Parent::function2().

Виртуальная таблица для C1 уже немного сложнее. Объект класса C1 имеет доступ как к членам C1, так и к членам Parent. Однако C1 имеет переопределение function1(), что делает C1::function1() более дочерним методом, нежели Parent::function1(). Следовательно, запись function1 будет указывать на C1::function1(). C1 не переопределяет function2(), поэтому запись function2 остается указывать на Parent::function2().



В виртуальной таблице для C2 запись function1 будет указывать на Parent::function1(), а запись function2 будет указывать на C2::function2().

Смотрим:

Хотя здесь уже можно сконфузиться во второй раз, всё, на самом деле, очень просто: *__vptr каждого класса указывает на виртуальную таблицу этого же класса. Записи в виртуальной таблице указывают на наиболее дочерние методы (переопределения), доступ к которым имеют объекты.

Рассмотрим, что произойдет при создании объекта класса C1:

Поскольку c1 является объектом класса C1, то он имеет свой *__vptr, который указывает на виртуальную таблицу класса C1.

Теперь создадим указатель класса Parent на объект c1:

Поскольку cPtr является указателем класса Parent, то он указывает только на часть Parent объекта c1. Однако *__vptr находится в части Parent также, поэтому cPtr имеет доступ к этому указателю. Наконец, cPtr->__vptr будет указывать на виртуальную таблицу C1, поскольку cPtr указывает на объект класса C1! Даже если cPtr является указателем класса Parent, он все равно имеет доступ к виртуальной таблице C1 (через __vptr).

Поэтому, что произойдет, если мы попытаемся вызвать cPtr->function1()?

Во-первых, компилятор распознает, что function1() является виртуальной функцией. Во-вторых, он будет использовать cPtr->__vptr для перехода к виртуальной таблице C1. В-третьих, он будет искать, какую версию function1() вызывать в виртуальной таблице C1. Он найдет C1::function1(). Следовательно, cPtr->function1() будет вызывать C1::function1()!

Теперь вы можете спросить: «А если бы cPtr указывал бы на объект класса Parent вместо объекта класса C1? Вызывал бы ли он по-прежнему C1::function1()?». Ответ: «Нет».

В этом случае, при создании объекта p, *__vptr указывает на виртуальную таблицу класса Parent вместо C1. Следовательно, pPtr->__vptr также будет указывать на виртуальную таблицу класса Parent. Запись function1() в виртуальной таблице класса Parent будет указывать на Parent::function1(). Таким образом, pPtr->function1() будет вызывать Parent::function1(), который является наиболее дочерним методом, доступ к которому имеет объект p.

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

Вызов виртуальной функции происходит медленнее, чем вызов не виртуальной функции из-за следующего:

   Во-первых, мы должны использовать *__vptr для перехода к соответствующей виртуальной таблице.

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

   И только тогда мы сможем вызвать функцию.

В результате мы делаем 3 операции, чтобы вызвать функцию, в отличие от двух операций для обычного непрямого вызова функции или одной операции для прямого вызова функции. Однако с современными компьютерами затраченное дополнительное время не является значительным.

Итого

Любой класс, который использует виртуальные функции, имеет свой *__vptr, и размер каждого объекта этого класса увеличивается на размер этого указателя. Виртуальные функции мощные, но цена этому — производительность.

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (9 оценок, среднее: 5,00 из 5)
Загрузка...

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

ВОЛШЕБНАЯ ТАБЛЕТКА ПО С++