На самом деле вы задаете здесь много разных вопросов, поэтому я постараюсь ответить на каждый из них по очереди.
Сначала вы хотите узнать, как выровнены элементы данных. Выравнивание элементов определяется компилятором, но из-за того, как центральные процессоры работают с неверно выровненными данными, все они, как правило, следуют одному и тому же
указывает, что структуры должны быть выровнены на основе наиболее ограничивающего элемента (который обычно, но не всегда, самого большого внутреннего типа), а структуры всегда выровнены так, что элементы массива выровнены одинаково.
Например:
struct some_object
{
char c;
double d;
int i;
};
Эта структура будет 24 байта. Поскольку класс содержит double, он будет выровнен на 8 байтов, то есть символ будет дополнен на 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 и членами класса может быть некоторое отступление.
Я также могу сказать вам, что почти все компиляторы реализуют возможности пакета прагмы, которые позволяют вам вручную выравнивать структуру. Как правило, вы не хотите этого делать, если вы действительно не знаете, что делаете, но это есть, а иногда и необходимо.
Последнее, что вы спросили, можете ли вы контролировать порядок. Вы всегда контролируете порядок. Компилятор всегда упорядочивает вещи в том порядке, в котором вы их записываете. Надеюсь, это многословное объяснение затронет все, что вам нужно знать.