Макет объекта в случае виртуальных функций и множественного наследования

Недавно в интервью меня спросили о компоновке объекта с виртуальными функциями и задействованным множественным наследованием.
Я объяснил это в контексте того, как это реализовано без задействования множественного наследования (т.е. как компилятор сгенерировал виртуальную таблицу, вставил секретный указатель на виртуальная таблица в каждом объекте и так далее).
Мне показалось, что в моем объяснении чего-то не хватает.
Итак, вот вопросы (см. пример ниже)

  1. Какова точная структура памяти объекта класса C.
  2. Записи виртуальных таблиц для класса C.
  3. Размеры (возвращаемые функцией sizeof) объектов классов A, B и C. (8, 8, 16 ??)
  4. Что делать, если используется виртуальное наследование. Разумеется, это должно повлиять на размеры и записи виртуальной таблицы?

Пример кода:

class A {  
  public:   
    virtual int funA();     
  private:  
    int a;  
};

class B {  
  public:  
    virtual int funB();  
  private:  
    int b;  
};  

class C : public A, public B {  
  private:  
    int c;  
};   

Спасибо!


person Ankur    schedule 24.08.2009    source источник
comment
Мне и раньше задавали подобные вопросы. Я всегда задавался вопросом, действительно ли вам нужно знать такие вещи, чтобы понимать и использовать C ++. Т.е. есть ли какой-то аспект языка, в котором семантика зависит от макета объекта? Мне показалось, что нет, и этот материал актуален только для агрессивной оптимизации под платформу.   -  person jon-hanson    schedule 24.08.2009
comment
Обратите внимание, что если вы разместите код сразу после списка в SO, он не будет отформатирован правильно. Вам нужно поместить простой текст между ними.   -  person    schedule 24.08.2009
comment
@Jon Я использую C ++ более 20 лет (почти 5 из них потратили на это обучение), и мне никогда не нужно было знать такие вещи, кроме как отвечать случайным педантичным ученикам. Конечно, в повседневном программировании это совершенно неважно.   -  person    schedule 24.08.2009
comment
Думаю пригодится при отладке. Если вы разбираетесь в компоновке объектов с множественным наследованием, тогда вы понимаете, когда и как значение указателя будет изменено с помощью static_cast.   -  person Steve Jessop    schedule 24.08.2009


Ответы (4)


Макет памяти и макет vtable зависят от вашего компилятора. Например, используя мой gcc, они выглядят так:

sizeof(int) == 4
sizeof(A) == 8
sizeof(B) == 8
sizeof(C) == 20

Обратите внимание, что sizeof (int) и пространство, необходимое для указателя vtable, также могут варьироваться от компилятора к компилятору и от платформы к платформе. Причина, по которой sizeof (C) == 20, а не 16, заключается в том, что gcc дает ему 8 байтов для подобъекта A, 8 байтов для подобъекта B и 4 байта для его члена int c.

Vtable for C
C::_ZTV1C: 6u entries
0     (int (*)(...))0
4     (int (*)(...))(& _ZTI1C)
8     A::funA
12    (int (*)(...))-0x00000000000000008
16    (int (*)(...))(& _ZTI1C)
20    B::funB

Class C
   size=20 align=4
   base size=20 base align=4
C (0x40bd5e00) 0
    vptr=((& C::_ZTV1C) + 8u)
  A (0x40bd6080) 0
      primary-for C (0x40bd5e00)
  B (0x40bd60c0) 8
      vptr=((& C::_ZTV1C) + 20u)

Использование виртуального наследования

class C : public virtual A, public virtual B

макет меняется на

Vtable for C
C::_ZTV1C: 12u entries
0     16u
4     8u
8     (int (*)(...))0
12    (int (*)(...))(& _ZTI1C)
16    0u
20    (int (*)(...))-0x00000000000000008
24    (int (*)(...))(& _ZTI1C)
28    A::funA
32    0u
36    (int (*)(...))-0x00000000000000010
40    (int (*)(...))(& _ZTI1C)
44    B::funB

VTT for C
C::_ZTT1C: 3u entries
0     ((& C::_ZTV1C) + 16u)
4     ((& C::_ZTV1C) + 28u)
8     ((& C::_ZTV1C) + 44u)

Class C
   size=24 align=4
   base size=8 base align=4
C (0x40bd5e00) 0
    vptridx=0u vptr=((& C::_ZTV1C) + 16u)
  A (0x40bd6080) 8 virtual
      vptridx=4u vbaseoffset=-0x0000000000000000c vptr=((& C::_ZTV1C) + 28u)
  B (0x40bd60c0) 16 virtual
      vptridx=8u vbaseoffset=-0x00000000000000010 vptr=((& C::_ZTV1C) + 44u)

Используя gcc, вы можете добавить -fdump-class-hierarchy, чтобы получить эту информацию.

person Tobias    schedule 24.08.2009
comment
Хорошо объяснено. Спасибо. Причина, по которой sizeof (C) == 20, а не 16, заключается в том, что gcc дает ему 8 байтов для подобъекта A, 8 байтов для подобъекта B и 4 байта для его члена int c. Как насчет указателя виртуальной таблицы в объекте C? - person Ankur; 24.08.2009
comment
Таким образом, компилятор может повторно использовать указатель vtable A-подобъекта, сохраняя 4 байта на экземпляр. - person Tobias; 24.08.2009
comment
@Tobias Технический термин для переработанного vptr - это первичная база. - person curiousguy; 27.07.2015

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

person stefaanv    schedule 24.08.2009
comment
Я думаю, что статья по следующей ссылке развивает вашу точку зрения. Правильно? phpcompiler.org/articles/virtualinheritance.html - person Ankur; 24.08.2009

Во-первых, полиморфный класс имеет хотя бы одну виртуальную функцию, поэтому у него есть vptr:

struct A {
    virtual void foo();
};

компилируется в:

struct A__vtable { // vtable for objects of declared type A
    void (*foo__ptr) (A *__this); // pointer to foo() virtual function
};

void A__foo (A *__this); // A::foo ()

// vtable for objects of real (dynamic) type A
const A__vtable A__real = { // vtable is never modified
    /*foo__ptr =*/ A__foo
};

struct A {
    A__vtable const *__vptr; // ptr to const not const ptr
                             // vptr is modified at runtime
};

// default constructor for class A (implicitly declared)
void A__ctor (A *__that) { 
    __that->__vptr = &A__real;
}

Примечание: C ++ может быть скомпилирован в другой язык высокого уровня, такой как C (как это сделал cfront), или даже в подмножество C ++ (здесь C ++ без virtual). Я помещаю __ в имена, сгенерированные компилятором.

Обратите внимание, что это упрощенная модель, в которой RTTI не поддерживается; настоящие компиляторы добавят данные в таблицу vtable для поддержки typeid.

Теперь простой производный класс:

struct Der : A {
    override void foo();
    virtual void bar();
};

Невиртуальные (*) подобъекты базового класса являются подобъектами, как подобъекты-члены, но в то время как подобъекты-члены являются полными объектами, т. Е. их реальный (динамический) тип - это их объявленный тип, подобъекты базового класса не завершены, и их реальный тип изменяется во время построения.

(*) виртуальные базы очень разные, например, виртуальные функции-члены отличаются от невиртуальных членов

struct Der__vtable { // vtable for objects of declared type Der
    A__vtable __primary_base; // first position
    void (*bar__ptr) (Der *__this); 
};

// overriding of a virtual function in A:
void Der__foo (A *__this); // Der::foo ()

// new virtual function in Der:
void Der__bar (Der *__this); // Der::bar ()

// vtable for objects of real (dynamic) type Der
const Der__vtable Der__real = { 
    { /*foo__ptr =*/ Der__foo },
    /*foo__ptr =*/ Der__bar
};

struct Der { // no additional vptr
    A __primary_base; // first position
};

Здесь «первая позиция» означает, что член должен быть первым (другие элементы могут быть переупорядочены): они расположены по нулевому смещению, поэтому мы можем reinterpret_cast указатели, типы совместимы; при ненулевом смещении нам пришлось бы производить корректировку указателя с помощью арифметики на char*.

Отсутствие настройки может показаться неважным с точки зрения сгенерированного кода (просто некоторые добавляют немедленные инструкции asm), но это означает гораздо больше, это означает, что такие указатели могут рассматриваться как имеющие разные типы: объект типа A__vtable* может содержать указатель на Der__vtable и рассматриваться как Der__vtable* или A__vtable*. Тот же объект-указатель служит указателем на A__vtable в функциях, работающих с объектами типа A, и как указатель на Der__vtable в функциях, имеющих дело с объектами типа Der.

// default constructor for class Der (implicitly declared)
void Der__ctor (Der *__this) { 
    A__ctor (reinterpret_cast<A*> (__this));
    __this->__vptr = reinterpret_cast<A__vtable const*> (&Der__real);
}

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

При множественном наследовании:

struct C : A, B {};

Экземпляр C будет содержать A и B, например:

struct C {
    A base__A; // primary base
    B base__B;
};

Обратите внимание, что только один из этих подобъектов базового класса может иметь привилегию сидеть по нулевому смещению; это важно во многих отношениях:

  • преобразование указателей в другие базовые классы (апкасты) потребует корректировки; и наоборот, для повышения качества нужны противоположные корректировки;

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

Итак, следующий код:

void B::printaddr() {
    printf ("%p", this);
}

void C::printaddr () { // overrides B::printaddr()
    printf ("%p", this);
}

может быть скомпилирован в

void B__printaddr (B *__this) {
    printf ("%p", __this);
}

// proper C::printaddr taking a this of type C* (new vtable entry in C)
void C__printaddr (C *__this) {
    printf ("%p", __this);
}

// C::printaddr overrider for B::printaddr
// needed for compatibility in vtable
void C__B__printaddr (B *__this) {
    C__printaddr (reinterpret_cast<C*>(reinterpret_cast<char*> (__this) - offset__C__B));
}

Мы видим, что объявленный C__B__printaddr тип и семантика совместимы с B__printaddr, поэтому мы можем использовать &C__B__printaddr в vtable B; C__printaddr несовместим, но может использоваться для вызовов, связанных с C объектами или классами, производными от C.

Невиртуальная функция-член похожа на бесплатную функцию, которая имеет доступ к внутреннему содержимому. Виртуальная функция-член - это «точка гибкости», которую можно настроить путем переопределения. Объявление виртуальной функции-члена играет особую роль в определении класса: как и другие члены, они являются частью контракта с внешним миром, но в то же время они являются частью контракта с производным классом.

Невиртуальный базовый класс похож на объект-член, в котором мы можем улучшить поведение с помощью переопределения (также мы можем получить доступ к защищенным членам). Для внешнего мира наследование для A в Der подразумевает, что для указателей будут существовать неявные преобразования производных в базовые, что A& может быть привязано к Der lvalue и т. Д. Для других производных классов (производных от Der) он также означает, что виртуальные функции A наследуются в Der: виртуальные функции в A могут быть переопределены в последующих производных классах.

Когда класс наследуется далее, скажем, Der2 является производным от Der, неявные преобразования указателей типа Der2* в A* семантически выполняются на этапе: сначала выполняется проверка преобразования в Der* (контроль доступа к отношению наследования Der2 из Der проверено обычными общедоступными / защищенными / частными / дружескими правилами), затем контроль доступа от Der до A. Отношение не виртуального наследования не может быть уточнено или переопределено в производных классах.

Функции-члены, не являющиеся виртуальными, могут вызываться напрямую, а виртуальные члены должны вызываться косвенно через vtable (если реальный тип объекта не известен компилятору), поэтому ключевое слово virtual добавляет косвенное обращение к доступу к функциям-членам. Как и для функций-членов, ключевое слово virtual добавляет косвенное обращение к доступу к базовому объекту; так же, как и для функций, виртуальные базовые классы добавляют точку гибкости при наследовании.

При выполнении невиртуального, многократного, множественного наследования:

struct Top { int i; };
struct Left : Top { };
struct Right : Top { };
struct Bottom : Left, Right { };

В Bottom (Left::i и Right::i) всего два Top::i подобъекта, как и в случае с объектами-членами:

struct Top { int i; };
struct mLeft { Top t; };
struct mRight { mTop t; };
struct mBottom { mLeft l; mRight r; }

Никого не удивляет наличие двух int подчиненных членов (l.t.i и r.t.i).

С виртуальными функциями:

struct Top { virtual void foo(); };
struct Left : Top { }; // could override foo
struct Right : Top { }; // could override foo
struct Bottom : Left, Right { }; // could override foo (both)

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

Семантика невиртуальных базовых классов следует из того факта, что базовое, не виртуальное, наследование является исключительным отношением: отношение наследования, установленное между Left и Top, не может быть изменено дальнейшим производным, поэтому тот факт, что подобное отношение существует между Right и Top не может повлиять на это отношение. В частности, это означает, что Left::Top::foo() можно переопределить в Left и в Bottom, но Right, который не имеет отношения наследования с Left::Top, не может установить эту точку настройки.

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

struct Top { int i; virtual void foo(); };
struct vLeft : virtual Top { }; 
struct vRight : virtual Top { };
struct vBottom : vLeft, vRight { }; 

Здесь это только один подобъект базового класса Top, только один int член.

Выполнение:

Место для не виртуальных базовых классов выделяется на основе статического макета с фиксированными смещениями в производном классе. Обратите внимание, что макет производного класса включен в макет более производного класса, поэтому точное положение подобъектов не зависит от реального (динамического) типа объекта (точно так же, как адрес невиртуальной функции является константой ). OTOH, положение подобъектов в классе с виртуальным наследованием определяется динамическим типом (точно так же, как адрес реализации виртуальной функции известен только тогда, когда известен динамический тип).

Местоположение подобъекта будет определяться во время выполнения с помощью vptr и vtable (повторное использование существующего vptr подразумевает меньшие накладные расходы на пространство) или прямого внутреннего указателя на подобъект (больше накладных расходов, требуется меньше косвенных обращений).

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

В части возможного перевода:

struct vLeft__vtable { 
    int Top__offset; // relative vLeft-Top offset
    void (*foo__ptr) (vLeft *__this); 
    // additional virtual member function go here
};

// this is what a subobject of type vLeft looks like
struct vLeft__subobject { 
    vLeft__vtable const *__vptr;
    // data members go here
};

void vLeft__subobject__ctor (vLeft__subobject *__this) { 
    // initialise data members
}

// this is a complete object of type vLeft 
struct vLeft__complete {
    vLeft__subobject __sub;
    Top Top__base;
}; 

// non virtual calls to vLeft::foo
void vLeft__real__foo (vLeft__complete *__this);

// virtual function implementation: call via base class
// layout is vLeft__complete 
void Top__in__vLeft__foo (Top *__this) {
    // inverse .Top__base member access 
    char *cp = reinterpret_cast<char*> (__this);
    cp -= offsetof (vLeft__complete,Top__base);
    vLeft__complete *__real = reinterpret_cast<vLeft__complete*> (cp);
    vLeft__real__foo (__real);
}

void vLeft__foo (vLeft *__this) {
    vLeft__real__foo (reinterpret_cast<vLeft__complete*> (__this));
}

// Top vtable for objects of real type vLeft
const Top__vtable Top__in__vLeft__real = { 
    /*foo__ptr =*/ Top__in__vLeft__foo 
};

// vLeft vtable for objects of real type vLeft
const vLeft__vtable vLeft__real = { 
    /*Top__offset=*/ offsetof(vLeft__complete, Top__base),
    /*foo__ptr =*/ vLeft__foo 
};

void vLeft__complete__ctor (vLeft__complete *__this) { 
    // construct virtual bases first
    Top__ctor (&__this->Top__base); 

    // construct non virtual bases: 
    // change dynamic type to vLeft
    // adjust both virtual base class vptr and current vptr
    __this->Top__base.__vptr = &Top__in__vLeft__real;
    __this->__vptr = &vLeft__real;

    vLeft__subobject__ctor (&__this->__sub);
}

Для объекта известного типа доступ к базовому классу осуществляется через vLeft__complete:

struct a_vLeft {
    vLeft m;
};

void f(a_vLeft &r) {
    Top &t = r.m; // upcast
    printf ("%p", &t);
}

переводится на:

struct a_vLeft {
    vLeft__complete m;
};

void f(a_vLeft &r) {
    Top &t = r.m.Top__base;
    printf ("%p", &t);
}

Здесь известен реальный (динамический) тип r.m, так же как и относительное положение подобъекта во время компиляции. Но здесь:

void f(vLeft &r) {
    Top &t = r; // upcast
    printf ("%p", &t);
}

реальный (динамический) тип r неизвестен, поэтому доступ осуществляется через vptr:

void f(vLeft &r) {
    int off = r.__vptr->Top__offset;
    char *p = reinterpret_cast<char*> (&r) + off;
    printf ("%p", p);
}

Эта функция может принимать любой производный класс с другим макетом:

// this is what a subobject of type vBottom looks like
struct vBottom__subobject { 
    vLeft__subobject vLeft__base; // primary base
    vRight__subobject vRight__base; 
    // data members go here
};

// this is a complete object of type vBottom 
struct vBottom__complete {
    vBottom__subobject __sub; 
    // virtual base classes follow:
    Top Top__base;
}; 

Обратите внимание, что базовый класс vLeft находится в фиксированном месте в vBottom__subobject, поэтому vBottom__subobject.__ptr используется как vptr для всего vBottom.

Семантика:

Отношение наследования используется всеми производными классами; это означает, что право на переопределение является общим, поэтому vRight может переопределить vLeft::foo. Это создает разделение ответственности: vLeft и vRight должны договориться о том, как они настраивают Top:

struct Top { virtual void foo(); };
struct vLeft : virtual Top { 
    override void foo(); // I want to customise Top
}; 
struct vRight : virtual Top { 
    override void foo(); // I want to customise Top
}; 
struct vBottom : vLeft, vRight { };  // error

Здесь мы видим конфликт: vLeft и vRight стремятся определить поведение единственной виртуальной функции foo, а определение vBottom ошибочно из-за отсутствия общего переопределителя.

struct vBottom : vLeft, vRight  { 
    override void foo(); // reconcile vLeft and vRight 
                         // with a common overrider
};

Выполнение:

Создание класса с не виртуальными базовыми классами с не виртуальными базовыми классами включает вызов конструкторов базовых классов в том же порядке, что и для переменных-членов, с изменением динамического типа каждый раз, когда мы вводим ctor. Во время построения подобъекты базового класса действительно действуют так, как если бы они были законченными объектами (это верно даже для невозможных полных абстрактных подобъектов базового класса: это объекты с неопределенными (чистыми) виртуальными функциями). Виртуальные функции и RTTI могут быть вызваны во время построения (за исключением, конечно, чистых виртуальных функций).

Создание класса с не виртуальными базовыми классами с виртуальными базами более сложное: во время создания динамический тип является типом базового класса, но макет виртуальной базы по-прежнему является макетом наиболее производных тип, который еще не создан, поэтому нам нужно больше vtables для описания этого состояния:

// vtable for construction of vLeft subobject of future type vBottom
const vLeft__vtable vLeft__ctor__vBottom = { 
    /*Top__offset=*/ offsetof(vBottom__complete, Top__base),
    /*foo__ptr =*/ vLeft__foo 
};

Виртуальные функции принадлежат vLeft (во время конструирования время жизни объекта vBottom еще не началось), а виртуальные базовые местоположения - это те же функции, что и vBottom (как определено в переведенном объекте vBottom__complete).

Семантика:

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

int foo (int *p) { return *pi; }
int i = foo(&i); 

или указателем this в конструкторе:

struct silly { 
    int i;
    std::string s;
    static int foo (bad *p) { 
        p->s.empty(); // s is not even constructed!
        return p->i; // i is not set!
    }
    silly () : i(foo(this)) { }
};

Совершенно очевидно, что любое использование this в ctor-init-list должно быть тщательно проверено. После инициализации всех членов this может быть передан другим функциям и зарегистрирован в некотором наборе (до начала уничтожения).

Менее очевидно то, что при конструировании класса, включающего общие виртуальные базы, подобъекты перестают конструироваться: во время конструирования vBottom:

  • сначала создаются виртуальные базы: когда Top создается, он строится как обычный субъект (Top даже не знает, что это виртуальная база)

  • затем базовые классы создаются в порядке слева направо: создается подобъект vLeft и становится функциональным как обычный vLeft (но с макетом vBottom), поэтому подобъект базового класса Top теперь имеет динамический тип vLeft;

  • начинается построение vRight подобъекта, и динамический тип базового класса меняется на vRight; но vRight не является производным от vLeft, ничего не знает о vLeft, поэтому база vLeft теперь сломана;

  • когда начинается тело конструктора Bottom, типы всех подобъектов стабилизируются, и vLeft снова работает.

person curiousguy    schedule 27.07.2015

Я не уверен, как этот ответ можно рассматривать как полный ответ без упоминания битов выравнивания или заполнения.

Позвольте мне рассказать немного о выравнивании:

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

Альтернативная формулировка с выравниванием по b-битам обозначает адрес с выравниванием по b / 8 байтов (например, с выравниванием по 64-битам с выравниванием по 8 байтам).

Считается, что доступ к памяти выровнен, если длина объекта данных, к которому осуществляется доступ, составляет n байтов, а адрес базы данных выровнен по n байтам. Когда доступ к памяти не выровнен, говорят, что он не выровнен. Обратите внимание, что по определению обращения к байтовой памяти всегда выровнены.

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

Обратите внимание, что приведенные выше определения предполагают, что каждый элемент данных имеет длину в два байта. Когда это не так (как в случае с 80-битной плавающей точкой на x86), контекст влияет на условия, при которых датум считается выровненным или нет.

Структуры данных могут храниться в памяти в стеке со статическим размером, известным как ограниченный, или в куче с динамическим размером, известным как неограниченный. "- из Wiki ...

Чтобы сохранить выравнивание, компилятор вставляет биты заполнения в скомпилированный код объекта структуры / класса. «Хотя компилятор (или интерпретатор) обычно выделяет отдельные элементы данных на выровненных границах, структуры данных часто имеют элементы с различными требованиями к выравниванию. Для поддержания надлежащего выравнивания транслятор обычно вставляет дополнительные неименованные элементы данных, чтобы каждый член был правильно выровнен. структура данных в целом может быть дополнена последним безымянным членом. Это позволяет правильно выровнять каждый член массива структур .... ....

Заполнение вставляется только тогда, когда за членом структуры следует член с более высокими требованиями к выравниванию или в конце структуры »- Wiki

Чтобы получить дополнительную информацию о том, как это делает GCC, см.

http://www.delorie.com/gnu/docs/gcc/gccint_111.html

и найдите текст "basic-align"

Теперь перейдем к этой проблеме:

Используя пример класса, я создал эту программу для компилятора GCC, работающего на 64-битной Ubuntu.

int main() {
    cout << "!!!Hello World!!!" << endl; // prints !!!Hello World!!!
    A objA;
    C objC;
    cout<<__alignof__(objA.a)<<endl;
    cout<<sizeof(void*)<<endl;
    cout<<sizeof(int)<<endl;
    cout<<sizeof(A)<<endl;
    cout<<sizeof(B)<<endl;
    cout<<sizeof(C)<<endl;
    cout<<__alignof__(objC.a)<<endl;
    cout<<__alignof__(A)<<endl;
    cout<<__alignof__(C)<<endl;
    return 0;
}

И результат для этой программы следующий:

4
8
4
16
16
32
4
8
8

Теперь позвольте мне объяснить это. Поскольку оба A и B имеют виртуальные функции, они будут создавать отдельные VTABLE, и VPTR будет добавлен в начало их объектов соответственно.

Следовательно, объект класса A будет иметь VPTR (указывающий на VTABLE для A) и int. Указатель будет иметь длину 8 байт, а длина int - 4 байта. Следовательно, перед компиляцией размер составляет 12 байт. Но компилятор добавит дополнительные 4 байта в конец int a в качестве битов заполнения. Следовательно, после компиляции размер объектов A будет 12 + 4 = 16.

Аналогично для объектов класса B.

Теперь объект C будет иметь два VPTR (по одному для каждого класса A и класса B) и 3 целых (a, b, c). Таким образом, размер должен быть 8 (VPTR A) + 4 (int a) + 4 (байты заполнения) + 8 (VPTR B) + 4 (int b) + 4 (int c) = 32 байта. Таким образом, общий размер C будет 32 байта.

person somenath mukhopadhyay    schedule 20.07.2015