Ответ 1
Действительно, вы задаете здесь много разных вопросов, поэтому я собираюсь сделать все возможное, чтобы ответить на каждый из них по очереди.
Сначала вы хотите знать, как выровнены элементы данных. Выравнивание элементов определяется компилятором, но из-за того, как процессоры работают с несогласованными данными, все они имеют тенденцию следовать тем же
что структуры должны быть выровнены на основе самого ограничивающего элемента (который обычно, но не всегда, самый большой внутренний тип), и strucutres всегда выравниваются так, что элементы массива выравниваются одинаково.
Например:
struct some_object
{
char c;
double d;
int i;
};
Эта структура будет 24 байта. Поскольку класс содержит double, он будет выровнен по 8 байт, что означает, что char будет дополняться 7 байтами, а int будет дополнено 4, чтобы гарантировать, что в массиве some_object все элементы будут выровнены по 8 байт. Вообще говоря, это зависит от компилятора, хотя вы обнаружите, что для данной архитектуры процессора большинство компиляторов выравнивают данные одинаково.
Второе, о чем вы говорите, - это производные члены класса. Порядок и выравнивание производных классов - это боль. Классы индивидуально следуют правилам, описанным выше для структур, но когда вы начинаете говорить о наследовании, вы попадаете в грязный дерн. Учитывая следующие классы:
class base
{
int i;
};
class derived : public base // same for private inheritance
{
int k;
};
class derived2 : public derived
{
int l;
};
class derived3 : public derived, public derived2
{
int m;
};
class derived4 : public virtual base
{
int n;
};
class derived5 : public virtual base
{
int o;
};
class derived6 : public derived4, public derived5
{
int p;
};
Макет памяти для базы будет:
int i // base
Макет памяти для производных будет:
int i // base
int k // derived
Макет памяти для производного2 будет выглядеть следующим образом:
int i // base
int k // derived
int l // derived2
Схема памяти для производного 3 будет выглядеть следующим образом:
int i // base
int k // derived
int i // base
int k // derived
int l // derived2
int m // derived3
Вы можете заметить, что база и каждый из них появляются здесь дважды. Это чудо множественного наследования.
Чтобы обойти это, мы имеем виртуальное наследование.
Макет памяти для производного 4 будет выглядеть следующим образом:
base* base_ptr // ptr to base object
int n // derived4
int i // base
Макет памяти для производного5 будет выглядеть следующим образом:
base* base_ptr // ptr to base object
int o // derived5
int i // base
Схема памяти для производного6 будет выглядеть следующим образом:
base* base_ptr // ptr to base object
int n // derived4
int o // derived5
int i // base
Вы заметите, что производные 4, 5 и 6 имеют указатель на базовый объект. Это не обязательно, поэтому при вызове любой из базовых функций у него есть объект для передачи этих функций. Эта структура зависит от компилятора, потому что она не указана в спецификации языка, но почти все компиляторы реализуют ее одинаково.
Все становится более сложным, когда вы начинаете говорить о виртуальных функциях, но опять же, большинство компиляторов реализуют их одинаково. Возьмем следующие классы:
class vbase
{
virtual void foo() {};
};
class vbase2
{
virtual void bar() {};
};
class vderived : public vbase
{
virtual void bar() {};
virtual void bar2() {};
};
class vderived2 : public vbase, public vbase2
{
};
Каждый из этих классов содержит по крайней мере одну виртуальную функцию.
Макет памяти для vbase будет выглядеть следующим образом:
void* vfptr // vbase
Макет памяти для vbase2 будет выглядеть следующим образом:
void* vfptr // vbase2
Макет памяти для vderived будет выглядеть следующим образом:
void* vfptr // vderived
Макет памяти для vderived2 будет выглядеть следующим образом:
void* vfptr // vbase
void* vfptr // vbase2
Есть много вещей, которые люди не понимают о том, как работают vftables. Первое, что нужно понять, это то, что классы хранят только указатели на vftables, а не целые vftables.
Это означает, что независимо от того, сколько виртуальных функций имеет класс, он будет иметь только один vftable, если только он не наследует vftable из другого места через множественное наследование. Практически все компиляторы ставят указатель vftable перед остальными членами класса. Это означает, что у вас может быть некоторое дополнение между указателем vftable и членами класса.
Я также могу сказать, что почти все компиляторы реализуют возможности pragma pack, которые позволяют вручную принудительно выравнивать структуру. Как правило, вы не хотите этого делать, если вы действительно не знаете, что делаете, но оно есть, и иногда оно не обязательно.
Последнее, что вы задали, - это контроль над заказом. Вы всегда контролируете заказ. Компилятор всегда будет заказывать вещи в том порядке, в котором вы их записываете. Надеюсь, это длинное объяснение поразит все, что вам нужно знать.