Подтвердить что ты не робот

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

Как члены данных получают выравнивание/упорядочение, если используется наследование/множественное наследование? Является ли этот компилятор конкретным?

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

Спасибо!

4b9b3361

Ответ 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, которые позволяют вручную принудительно выравнивать структуру. Как правило, вы не хотите этого делать, если вы действительно не знаете, что делаете, но оно есть, и иногда оно не обязательно.

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

Ответ 2

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

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

Ответ 3

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

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

Ответ 4

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

Ответ 5

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

так

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

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

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

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

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

Ответ 6

Порядок объектов в множественном наследовании - это не всегда то, что вы указываете. Из того, что я испытал, компилятор будет использовать указанный порядок, если он не может. Он не может использовать указанный порядок, когда первый базовый класс не имеет виртуальных функций, а другой базовый класс имеет виртуальные функции. В этом случае первые байты класса должны быть указателем таблицы виртуальной функции, но первый базовый класс не имеет этого. Компилятор переупорядочивает базовые классы, так что первый имеет указатель таблицы виртуальной функции.

Я тестировал это как с msdev, так и с g++, и оба они меняют классы. Раздражающе, у них, похоже, есть разные правила, как они это делают. Если у вас есть 3 или более базовых класса, а у первого нет виртуальных функций, эти компиляторы будут иметь разные макеты.

Чтобы быть в безопасности, выберите два и избегайте другого.

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

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

  • Используйте 2 или меньше базовых классов (так как в этом случае компиляторы одинаково перестраиваются)

Ответ 7

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

Ответ 8

Я могу ответить на один из вопросов.

Как члены данных получают выравнивание/упорядочение, если используется наследование/множественное наследование?

Я создал инструмент для визуализации макета памяти классов, кадров стека функций и другой информации ABI (Linux, GCC). Вы можете посмотреть результат для mysqlpp:: Connection class (inherits OptionalExceptions) из библиотеки MySQL ++ здесь.

введите описание изображения здесь