Размер объекта C ++ с виртуальными методами - PullRequest
25 голосов
/ 11 января 2010

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

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 (что это?)

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

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

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

Ответы [ 6 ]

21 голосов
/ 11 января 2010

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

Основное правило - виртуальная таблица должна находиться со смещением 0 для любого указателя на объект. Это подразумевает наличие нескольких таблиц для множественного наследования.

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

Означает ли это, что существует только один 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
        +---

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

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

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

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

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

cl test.cpp /d1reportSingleClassLayoutXXX

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

3 голосов
/ 11 января 2010

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

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

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

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

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

3 голосов
/ 11 января 2010

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

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

Структура памяти следующая:

vptr | A::a | B::b

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

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

vptr | A::a | vptr | B::b | C::c

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

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

Пример кода № 4 и далее

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

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

vptr | base pointer | B::b | vptr | A::a | C::c | vptr | A::a
          \-----------------------------------------^

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

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

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

2 голосов
/ 11 января 2010

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

В примере наследования, вот как может выглядеть виртуальная таблица для классов 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.

1 голос
/ 11 января 2010

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

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

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

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

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

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

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

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

0 голосов
/ 11 января 2010

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

...