Размер объекта C ++ с виртуальными методами

У меня есть несколько вопросов о размере объекта с виртуальным.

1) виртуальная функция

class A {
public:
int a;
virtual void v();
}

Размер класса A составляет 8 байт …. одно целое число (4 байта) плюс один виртуальный указатель (4 байта)
Ясно!

class B: public A{
public:
int b;
virtual void w();
}

Каков размер класса B? Я проверил, используя размер B, он печатает
12

Означает ли это, что только один vptr есть, даже у класса B и класса A есть виртуальная функция? Почему там только один вптр?

class A {
public:
int a;
virtual void v();
};

class B {
public:
int b;
virtual void w();
};

class C :  public A, public B {
public:
int c;
virtual void x();
};

Размер C составляет 20 ……..

Похоже, что в этом случае два vptrs находятся в макете ….. Как это происходит? Я думаю, что два vptrs один для класса A, а другой для класса B …. так что нет vptr для виртуальной функции класса C?

У меня вопрос, каково правило о количестве vptrs в наследстве?

2) виртуальное наследование

    class A {
public:
int a;
virtual void v();
};

class B: virtual public A{                  //virtual inheritance
public:
int b;
virtual void w();
};

class C :  public A {                      //non-virtual inheritance
public:
int c;
virtual void x();
};

class D: public B, public C {
public:
int d;
virtual void y();
};

Размер A составляет 8 байтов ————— 4 (int a) + 4 (vptr) = 8

Размер B составляет 16 байт ————— Без виртуального должно быть 4 + 4 + 4 = 12. Почему здесь есть еще 4 байта? Какой макет класса B?

Размер C составляет 12 байт. ————— 4 + 4 + 4 = 12. Понятно!

Размер D составляет 32 байта ————— это должно быть 16 (класс B) + 12 (класс C) + 4 (int d) = 32. Это правильно?

    class A {
public:
int a;
virtual void v();
};

class B: virtual public A{                       //virtual inheritance here
public:
int b;
virtual void w();
};

class C :  virtual public A {                    //virtual inheritance here
public:
int c;
virtual void x();
};

class D: public B, public C {
public:
int d;
virtual void y();
};

Размер А составляет 8

размер B 16

Размер С составляет 16

размер D равен 28 Означает ли это 28 = 16 (класс B) + 16 (класс C) — 8 (класс A) + 4 (что это?)

У меня вопрос: почему при применении виртуального наследования появляется дополнительное пространство?

Какое нижнее правило для размера объекта в этом случае?

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

24

Решение

Это все реализация определена. Я использую VC10 Beta2. Ключ к пониманию этой вещи (реализация виртуальных функций), вам нужно знать о секретном переключателе в компиляторе Visual Studio, / d1reportSingleClassLayoutXXX. Я вернусь к этому через секунду.

Основное правило — vtable должен быть расположен со смещением 0 для любого указателя на объект. Это подразумевает несколько vtables для множественного наследования.

Пара вопросов здесь, я начну сверху:

Означает ли это, что только один vptr есть, даже у класса B и класса A есть виртуальная функция? Почему там только один вптр?

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

Похоже, что в этом случае два vptrs находятся в макете ….. Как это происходит? Я думаю, что два vptrs один для класса A, а другой для класса B …. так что нет vptr для виртуальной функции класса C?

Это макет класса C, как сообщает / d1reportSingleClassLayoutC:

class C size(20):
+---
| +--- (base class A)
0      | | {vfptr}
4      | | a
| +---
| +--- (base class B)
8      | | {vfptr}
12      | | b
| +---
16      | c
+---

Вы правы, есть две таблицы, по одной для каждого базового класса. Вот как это работает в множественном наследовании; если C * приведен к B *, значение указателя корректируется на 8 байтов. Для работы вызовов виртуальных функций виртуальная таблица все еще должна иметь смещение 0.

Vtable в приведенном выше макете для класса A рассматривается как vtable класса C (при вызове через C *).

Размер B составляет 16 байт ————— Без виртуального должно быть 4 + 4 + 4 = 12. Почему здесь есть еще 4 байта? Какой макет класса B?

Это макет класса B в этом примере:

class B size(20):
+---
0      | {vfptr}
4      | {vbptr}
8      | b
+---
+--- (virtual base A)
12      | {vfptr}
16      | a
+---

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

Размер D составляет 32 байта ————— это должно быть 16 (класс B) + 12 (класс C) + 4 (int d) = 32. Это правильно?

Нет, 36 байт. То же самое касается виртуального наследования. Расположение D в этом примере:

class D size(36):
+---
| +--- (base class B)
0      | | {vfptr}
4      | | {vbptr}
8      | | b
| +---
| +--- (base class C)
| | +--- (base class A)
12      | | | {vfptr}
16      | | | a
| | +---
20      | | c
| +---
24      | d
+---
+--- (virtual base A)
28      | {vfptr}
32      | a
+---

У меня вопрос: почему при применении виртуального наследования появляется дополнительное пространство?

Указатель виртуального базового класса, это сложно. Базовые классы «объединены» в виртуальном наследовании. Вместо того, чтобы базовый класс был встроен в класс, класс будет иметь указатель на объект базового класса в макете. Если у вас есть два базовых класса, использующих виртуальное наследование (иерархия классов «алмаз»), они оба будут указывать на один и тот же виртуальный базовый класс в объекте, вместо того, чтобы иметь отдельную копию этого базового класса.

Какое нижнее правило для размера объекта в этом случае?

Важная точка; нет никаких правил: компилятор может делать все, что ему нужно.

И последняя деталь; чтобы сделать все эти схемы компоновки классов, с которыми я компилирую:

cl test.cpp /d1reportSingleClassLayoutXXX

Где XXX — это подстрока соответствия структур / классов, которые вы хотите увидеть в макете. Используя это, вы можете самостоятельно изучить влияние различных схем наследования, а также почему / где добавлено заполнение и т. Д.

21

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

Цитата> Мой вопрос, каково правило о количестве vptrs в наследстве?

Правил нет, каждому поставщику компилятора разрешается реализовывать семантику наследования так, как он считает нужным.

класс B: public A {}, size = 12. Это довольно нормально, одна таблица v для B, в которой есть оба виртуальных метода, указатель таблицы vtable + 2 * int = 12

класс C: общедоступный A, общедоступный B {}, размер = 20. C может произвольно расширять vtable таблицы A или B. 2 * указатель vtable + 3 * int = 20

Виртуальное наследование: это то, где вы действительно сталкиваетесь с недокументированным поведением. Например, в MSVC параметры компиляции #pragma vtordisp и / vd становятся актуальными. Есть некоторая справочная информация в Эта статья. Я изучил это несколько раз и решил, что аббревиатура опции компиляции является репрезентативной для того, что может случиться с моим кодом, если я когда-либо его использовал.

3

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

Пример кода № 2

Расположение памяти выглядит следующим образом:

вптр | A :: a | B :: б

Повышение указателя на B для типа A приведет к тому же адресу, с тем же VTPR используется. Вот почему здесь нет необходимости в дополнительном vptr.

Пример кода № 3

вптр | A :: a | вптр | B :: b | C :: гр

Как видите, здесь два vptr, как вы и догадались. Зачем? Потому что это правда, что если мы выполняем передачу с C на A, нам не нужно изменять адрес, и поэтому мы можем использовать тот же vptr. Но если мы сбрасываем с C на B, мы делать нужна эта модификация, и соответственно нам нужен vptr в начале результирующего объекта.

Таким образом, любой унаследованный класс, кроме первого, потребует дополнительного vptr (если только у этого унаследованного класса нет виртуальных методов, в этом случае у него нет vptr).

Пример кода № 4 и выше

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

Так как же выглядит макет памяти? Это зависит от компилятора. В вашем компиляторе это, вероятно, что-то вроде

вптр | базовый указатель | B :: b | вптр | A :: a | C :: c | вптр | A ::
\ ----------------------------------------- ^

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

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

РЕДАКТИРОВАТЬ: разъяснение — все это действительно зависит от компилятора, расположение памяти, которое я показал, может быть различным в разных компиляторах.

3

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

В примере наследования, вот как может выглядеть виртуальная таблица для классов A и B:

      class A
+-----------------+
| pointer to A::v |
+-----------------+

class B
+-----------------+
| pointer to A::v |
+-----------------+
| pointer to B::w |
+-----------------+

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

В вашем примере с классом C, если вы подумаете об этом, нет способа создать виртуальную таблицу, которая была бы действительной как таблица для класса C, класса A и класса B. Таким образом, компилятор создает два. Одна виртуальная таблица действительна для классов A и C (наиболее вероятно), а другая действительна для классов A и B.

2

Это, очевидно, зависит от реализации компилятора.
В любом случае, я думаю, что я могу суммировать следующие правила из реализации, приведенной в классической статье, ссылки на которую приведены ниже и которая дает количество байтов, которые вы получаете в своих примерах (за исключением класса D, который будет 36 байтов, а не 32 !!!) :

Размер объекта класса Т:

  • Размер его полей ПЛЮС — сумма размеров каждого объекта, от которого Т наследует ПЛЮС 4 байта для каждого объекта, от которого Т фактически наследует ПЛЮС 4 байта, ТОЛЬКО ЕСЛИ Т нужна ДРУГАЯ v-таблица
  • Обратите внимание: если класс K фактически наследуется несколько раз (на любом уровне), вы должны добавить размер K только один раз

Итак, мы должны ответить на другой вопрос: когда классу нужен ДРУГОЙ v-стол?

  • Класс, который не наследуется от других классов, нуждается в v-таблице, только если у него есть один или несколько виртуальных методов
  • Иначе, классу нужна другая v-таблица, ТОЛЬКО ЕСЛИ ни у одного из классов, от которых он не наследуется, действительно есть v-таблица

Конец правил (которые, я думаю, можно применить, чтобы соответствовать тому, что Терри Махаффи объяснил в своем ответе) 🙂

В любом случае, я предлагаю прочитать следующую статью Бьярна Страуструпа (создателя C ++), которая объясняет именно эти вещи: сколько виртуальных таблиц необходимо для виртуального или не виртуального наследования … и почему!

Это действительно хорошее чтение:
http://www.hpc.unimelb.edu.au/nec/g1af05e/chap5.html

1

Я не уверен, но я думаю, что это из-за указателя на Таблица виртуальных методов

0
По вопросам рекламы [email protected]