Подтвердить что ты не робот

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

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

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

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

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

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

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

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

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 один для класса 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();
    };

sizeof A равно 8

sizeof B - 16

sizeof C равен 16

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

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

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

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

4b9b3361

Ответ 1

Это все реализация определена. Я использую 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 - это подстрока соответствия структур/классов, которые вы хотите увидеть в макете. Используя это, вы можете самостоятельно изучить влияние различных схем наследования, а также почему/где добавлено заполнение и т.д.

Ответ 2

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

Пример кода # 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, нам do нужна эта модификация, и, соответственно, нам понадобится vptr в начале результирующего объекта.

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

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

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

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

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

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

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

EDIT: разъяснение - все это действительно зависит от компилятора, макет памяти, который я показал, может отличаться в разных компиляторах.

Ответ 3

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

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

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

класс C: public A, public B {}, size = 20. C может произвольно расширять vtable либо A, либо B. 2 * vtable pointer + 3 * int = 20

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

Ответ 4

Все это полностью реализовано вами. Вы не можете рассчитывать ни на что. Нет "правила".

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

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

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

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

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

Ответ 5

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

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

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

Итак, мы должны ответить на другой вопрос: когда класс нужен еще один v-table?

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

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

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

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