выравнивание данных C++ / членский порядок и наследование

То, как элементы данных становятся выровненными / заказало, если наследование / множественное наследование используется? Действительно ли этот компилятор конкретен?

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

Спасибо!

26
задан linuxbuild 12 September 2015 в 20:44
поделиться

6 ответов

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

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

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

Например:

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

Макет памяти для vde производного будет:

void* vfptr // vderived

Макет памяти для vde производного2 будет:

void* vfptr // vbase
void* vfptr // vbase2

Есть многие люди не понимают, как работают vftables. Первое, что нужно понять, это то, что классы хранят только указатели на vftables, а не целые vftables.

Это означает, что независимо от того, сколько виртуальных функций имеет класс, у него будет только один vftable, если только он не наследует vftable откуда-то еще через множественное наследование. Практически все компиляторы помещают указатель vftable перед остальными членами класса. Это означает, что между указателем vftable и членами класса может быть некоторый отступ.

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

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

64
ответ дан 28 November 2019 в 06:26
поделиться

Это не только специфично для компилятора - на него, скорее всего, влияют опции компилятора. Я не знаю ни одного компилятора, который бы давал Вам точный контроль над тем, как члены и базы упаковываются и упорядочиваются с множественным наследованием.

Если Вы делаете что-то, что зависит от порядка и упаковки, попробуйте сохранить POD-структуру внутри своего класса и использовать ее.

.
3
ответ дан 28 November 2019 в 06:26
поделиться

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

.
0
ответ дан 28 November 2019 в 06:26
поделиться

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

так

struct foo
{
    char a;
    int b;
    char c;
}

Обычно 32-битная машина занимает более 6 байт

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

При множественном наследовании происходит смещение между адресом класса и адресом второго базового класса. >static_cast и dynamic_cast вычисляют это смещение. reinterpret_cast не вычисляет. Касы в стиле Си по возможности производят статическое приведение, в противном случае, reinterpret cast.

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

.
1
ответ дан 28 November 2019 в 06:26
поделиться
[

] Как только ваш класс не является POD (Plain old data), все ставки отменяются. Вероятно, существуют директивы, специфичные для компилятора, которые вы можете использовать для упаковки / выравнивания данных.[

].
1
ответ дан 28 November 2019 в 06:26
поделиться
[

] Это специфично для компилятора.[

] [

]Правка: в основном она сводится к тому, где находится виртуальная таблица, и может отличаться в зависимости от того, какой компилятор используется.[

].
1
ответ дан 28 November 2019 в 06:26
поделиться
Другие вопросы по тегам:

Похожие вопросы: