Виртуальные таблицы и виртуальные указатели для нескольких виртуальное наследование и преобразование типа

Я мало смущен vptr и представлением объектов в памяти и надеждой, можно помочь мне понять вопрос лучше.

  1. Рассмотреть B наследовался A и оба определяют виртуальные функции f(). Из того, что я узнал, что представление объекта класса B в памяти похоже на это:[ vptr | A | B ] и vtbl это vptr точки к содержат B::f(). Я также понял что кастинг объекта от B кому: A не делает ничего кроме игнорирования B часть в конце объекта. Действительно ли это верно? Не это поведение является неправильным? Мы хотим тот объект типа A выполниться A::f() метод и нет B::f().

  2. Есть ли много vtables в системе как количество классов?

  3. Как будет a vtable из класса, который наследовался двум или больше классам, похожи? Как объект C будет представлен в памяти?

  4. То же как вопрос 3, но с виртуальным наследованием.

10
задан P Shved 24 July 2010 в 12:13
поделиться

3 ответа

Следующее верно для GCC (и кажется верным для LLVM ссылка ), но также может быть верным для используемого вами компилятора. Все это зависит от реализации и не регулируется стандартом C ++. Однако GCC пишет свой собственный двоичный стандартный документ Itanium ABI .

Я попытался объяснить основные концепции организации виртуальных таблиц более простыми словами в своей статье о производительности виртуальных функций в C ++ , которая может оказаться для вас полезной. Вот ответы на ваши вопросы:

  1. Более правильный способ изобразить внутреннее представление объекта:

     | вптр | ======= | ======= | <- ваш объект
     | ---- A ---- | |
     | --------- B --------- |
    

    B содержит его базовый класс A , он просто добавляет пару своих собственных членов после своего конца.

    Приведение из B * в A * действительно ничего не дает, оно возвращает тот же указатель, а vptr остается прежним. Но, вкратце, виртуальные функции не всегда вызываются через vtable . Иногда они вызываются так же, как и другие функции.

    Вот более подробное объяснение. Следует различать два способа вызова функции-члена:

     A a, * aptr;
    a.func (); // вызов A :: func () предварительно скомпилирован!
    aptr-> A :: func (); // то же самое
    aptr-> func (); // вызывает виртуальную функцию через vtable.
     // Это может быть вызов A :: func () или B :: func ().
    

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

     B b, * bptr;
    static_cast  (b) :: func (); // вызывает A :: func, потому что тип
     // static_cast  (b) равно A!
    

    В этом случае он даже не смотрит внутрь vtable!

  2. Как правило, нет. Класс может иметь несколько vtables, если он наследуется от нескольких баз, каждая из которых имеет свою собственную vtable. Такой набор виртуальных таблиц образует «группу виртуальных таблиц» (см. П. 3).

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

  3. Вот пример. Предположим, что C наследуется от A и B , каждый класс определяет virtual void func () , а также a ], b или c виртуальная функция, относящаяся к ее имени.

    C будет иметь группу vtable из двух vtables. Он будет разделять одну виртуальную таблицу с A (виртуальная таблица, в которой собственные функции текущего класса go, называются «первичными»), и будет добавлена ​​виртуальная таблица для B :

     | C :: func () | а () | c () || C :: func () | б () |
    | ---- vtable для A ---- | | ---- vтаблица для B ---- |
    | --- "первичная виртуальная таблица" - || - "вторичная виртуальная таблица" - |
    | -------------- группа виртуальных таблиц для C ------------- |
    

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

  4. Виртуальные базы (некоторые из них) размещены в конце группы vtable. Это сделано потому, что у каждого класса должна быть только одна виртуальная база, и если они были смешаны с «обычными» vtables, то компилятор не мог повторно использовать части построенных vtables для создания частей производных классов. Это приведет к вычислению ненужных смещений и снизит производительность.

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

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

16
ответ дан 3 December 2019 в 21:19
поделиться
  1. Это кажется мне правильным. Это не неправильно, так как если вы используете указатель A, вам нужно только то, что предоставляет A, плюс, возможно, реализации функций B, которые доступны из таблицы A (может быть несколько таблиц, в зависимости от компилятора и сложности иерархии).
  2. Я бы сказал "да", но это зависит от реализации компилятора, так что вам не обязательно об этом знать.
  3. и 4. Читайте дальше.

Я бы рекомендовал прочитать Multiple Inheritance Considered Useful , это длинная статья, но она проясняет тему, поскольку очень подробно объясняет, как работает наследование в C++ (ссылки на рисунки не работают, но они доступны внизу страницы).

2
ответ дан 3 December 2019 в 21:19
поделиться
  1. Если объект B наследуется от A, то представление памяти для B будет следующим:

    • указатель на виртуальную таблицу A
    • A специфические переменные/функции
    • указатель на виртуальную таблицу B
    • B специфические переменные/функции/оверрайды

    Если у вас есть B* b = new B(); (A)b->f(), то:

    • если f была объявлена как виртуальная функция, то вызывается реализация B, потому что b имеет тип B
    • если f не была объявлена как виртуальная функция, то при вызове не будет поиска в таблице v правильной реализации и будет вызвана реализация A.
  2. Каждый объект будет иметь свою собственную vtable (не принимайте это как должное, так как мне придется это исследовать

  3. Посмотрите на this пример vtable layour при работе с множественным наследованием

  4. Смотрите this для обсуждения бриллиантового наследования и представления vtable

-1
ответ дан 3 December 2019 в 21:19
поделиться
Другие вопросы по тегам:

Похожие вопросы: