виртуальная таблица и схема хранения _vptr

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

class Base
{
public:
FunctionPointer *__vptr;
virtual void function1() {};
virtual void function2() {};
};

class D1: public Base
{
public:
virtual void function1() {};
};

class D2: public Base
{
public:
virtual void function2() {};
};
int main()
{
D1 d1;
Base *dPtr = &d1;
dPtr->function1();
}

Спасибо! заблаговременно

6

Решение

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

Тем не менее, практически все компиляторы C ++ работают в этом отношении одинаково.

Итак, начнем с не виртуальных функций. Они бывают двух классов: статические и нестатические.

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

Нестатические функции-члены немного сложнее. Это все еще обычные функции, которые вызываются напрямую, но им передается скрытый указатель на экземпляр объекта, для которого они были вызваны. Внутри функции вы можете использовать ключевое слово this ссылаться на эти данные экземпляра. Итак, когда вы звоните что-то вроде a.func(b);, сгенерированный код очень похож на код, который вы получите для func(a, b);

Теперь давайте рассмотрим виртуальные функции. Вот где мы попадаем в vtables и vtable указатели. У нас достаточно косвенных указаний на то, что, вероятно, лучше нарисовать несколько диаграмм, чтобы увидеть, как все это изложено. Вот довольно простой случай: один экземпляр одного класса с двумя виртуальными функциями:

введите описание изображения здесь

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

введите описание изображения здесь

Обратите внимание, что каждый экземпляр класса имеет свои собственные данные, но они оба используют один и тот же vtable и один и тот же код — и если бы у нас было больше экземпляров, они все равно разделяли бы один vtable среди всех экземпляров одного и того же класса.

Теперь давайте рассмотрим деривацию / наследование. В качестве примера, давайте переименуем наш существующий класс в «Base» и добавим производный класс. Поскольку я чувствую себя творчески, я назову это «Производный». Как и выше, базовый класс определяет две виртуальные функции. Производный класс переопределяет один (но не другой) из них:

введите описание изображения здесь

Конечно, мы можем объединить два, имея несколько экземпляров каждого из базового и / или производного класса:

введите описание изображения здесь

Теперь давайте углубимся в это немного подробнее. Интересно, что при деривации мы можем передать указатель / ссылку на объект производного класса в функцию, написанную для получения указателя / ссылки на базовый класс, и она все еще работает, но если вы вызываете виртуальную функцию, вы получаете версию для фактического класса, а не базовый класс. Итак, как это работает? Как мы можем обращаться с экземпляром производного класса, как если бы он был экземпляром базового класса, и при этом работать? Для этого у каждого производного объекта есть «подобъект базового класса». Например, давайте рассмотрим код следующим образом:

struct simple_base {
int a;
};

struct simple_derived : public simple_base {
int b;
};

В этом случае, когда вы создаете экземпляр simple_derived, вы получите объект, содержащий два ints: a а также b, a (часть базового класса) находится в начале объекта в памяти, и b (производная часть класса) следует за этим. Таким образом, если вы передаете адрес объекта функции, ожидающей экземпляра базового класса, он использует те части, которые существуют в базовом классе, которые компилятор размещает с теми же смещениями в объекте, что и они. находиться в объекте базового класса, поэтому функция может манипулировать ими, даже не зная, что имеет дело с объектом производного класса. Аналогично, если вы вызываете виртуальную функцию, все, что ей нужно знать, это расположение указателя vtable. Насколько это волнует, что-то вроде Base::func1 в основном просто означает, что он следует за указателем vtable, а затем использует указатель на функцию с некоторым заданным смещением оттуда (например, четвертый указатель на функцию).

По крайней мере, сейчас я собираюсь игнорировать множественное наследование. Это добавляет немного сложности к картине (особенно когда задействовано виртуальное наследование), и вы вообще об этом не упомянули, поэтому я сомневаюсь, что вас это действительно волнует.

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

6

Другие решения

Виртуальная таблица должна быть разделена между экземплярами класса. Точнее, он живет на уровне «класса», а не на уровне экземпляра. У каждого экземпляра есть накладные расходы на наличие указателя на виртуальную таблицу, если в его иерархии есть виртуальные функции и классы.

Сама таблица имеет размер, необходимый для хранения указателя для каждой виртуальной функции. Помимо этого, это детали реализации, как это на самом деле определяется. Проверьте Вот для ТАКОГО вопроса с более подробной информацией об этом.

4

Прежде всего, следующий ответ содержит почти все, что вы хотите знать о виртуальных таблицах:
https://stackoverflow.com/a/16097013/8908931

Если вы ищете что-то более конкретное (с обычной оговоркой, что это может измениться между платформами, компиляторами и архитектурами ЦП):

  1. При необходимости для класса создается виртуальная таблица. У класса будет только один экземпляр виртуальной таблицы, и у каждого объекта класса будет указатель, который будет указывать на место в памяти этой виртуальной таблицы. Сама виртуальная таблица может рассматриваться как простой массив указателей.
  2. Когда вы назначаете производный указатель на базовый указатель, он также содержит указатель на виртуальную таблицу. Это означает, что базовый указатель указывает на виртуальную таблицу производного класса. Компилятор направит этот вызов на смещение в виртуальной таблице, которая будет содержать фактический адрес функции из производного класса.
  3. На самом деле, нет. Обычно в начале объекта есть указатель на саму виртуальную таблицу. Но это вам не сильно поможет, так как это просто набор указателей, без реального указания его размера.
  4. Короткий очень длинный ответ: для точного размера вы можете найти эту информацию в исполняемом файле (или в сегментах, загруженных из него в память). Обладая достаточными знаниями о том, как работает виртуальная таблица, вы можете получить довольно точную оценку, если вы знаете код, компилятор и целевую архитектуру.

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

3

Ответ, который дал Джерри Коффин, отлично объясняет, как работают указатели виртуальных функций для достижения полиморфизма во время выполнения в C ++. Тем не менее, я считаю, что он не отвечает, где в памяти хранится vtable. Как уже отмечали другие, это не продиктовано стандартом.

Тем не менее, есть отличный Сообщения в блоге) Мартин Кизел, который подробно рассказывает о том, где хранятся виртуальные таблицы. Подводя итог сообщениям блога:

  1. Одна виртуальная таблица создается для каждого класса (не экземпляра) с виртуальными функциями. Каждый экземпляр этого класса указывает на одну и ту же таблицу в памяти
  2. Каждый vtable хранится в памяти только для чтения полученного двоичного файла
  3. Разборка для каждой функции в vtable хранится в текстовом разделе результирующего двоичного файла ELF.
  4. Попытка записи поверх виртуальной таблицы, расположенной в постоянной памяти, приводит к ошибке сегментации (как и ожидалось)
1

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

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

базы

 Base::function1()
Base::function2()

D1-х

 D1::function1()
Base::function2()

D2-х

 Base::function1()
D2::function2()

Далее производные drom D1 или D2 просто добавят свои новые виртуальные функции в список ниже 2 текущих.

При вызове виртуальной функции мы просто вызываем соответствующий индекс, function1 будет индексом 0

Итак, ваш звонок

 dPtr->function1();

на самом деле

 dPtr->_vptr[0]();
-1
По вопросам рекламы [email protected]