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

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

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 находятся в макете.....Как это происходит? Я думаю, что два vptrs-для класса А и класса Б....так нет 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();
};

в размер в 8 байт -------------- 4 (int a) + 4 (vptr) = 8

размер B составляет 16 байт -------------- без virtual это должно быть 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

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

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

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

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

6 ответов


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

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

пара вопросов вот, я начну сверху:

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

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

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

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

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

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

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

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


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

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

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

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

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


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

образец кода #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: clarification-все действительно зависит от компилятора, макета памяти I showed может быть разным в разных компиляторах.


все это полностью определено реализацией, которую вы понимаете. Ты не можешь на это рассчитывать. Нет никакого "правила".

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


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

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

  • размер его полей плюс сумма размера каждого объекта, от которого T наследует плюс 4 байта для каждого объекта из которого T фактически наследует плюс 4 байта, только если T нуждается в другой v-таблице
  • обратите внимание: если класс K фактически наследуется несколько раз (на любом уровне), вы должны добавить размер K только один раз

поэтому мы должны ответить на другой вопрос: когда классу нужна другая v-таблица?

  • класс, который не наследует от других классов должен по таблице, только если он имеет один или несколько виртуальных методов
  • в противном случае класс должен другая v-таблица, только если ни один из классов, от которых она фактически не наследуется, не имеет V-таблицы

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

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

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


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