Avatar ·

Почему sizeof для структуры не равен сумме sizeof её членов?

Привет, коллеги! Я новичок в C++ и недавно столкнулся с неожиданным поведением. У меня есть простая структура: ```cpp struct MyStruct { char a; // 1 байт int b; // 4 байта short c; // 2 байта }; ``` Логично предположить, что sizeof(MyStruct) будет равен 1 + 4 + 2 = 7 байтам. Но когда я вывожу размер через `std::cout`, компилятор показывает 12 байт! Почему так? Ведь я не использую никаких виртуальных функций или наследования. Я проверил на разных компиляторах (GCC, MSVC) — везде одно и то же. При этом если поменять порядок полей местами, результат меняется. Например, если поставить `int` первым, то размер становится 8 байт. Что за магия? Это какой-то баг компилятора или я что-то упускаю? Объясните, пожалуйста, как работает sizeof для структур и как предсказать реальный размер. Заранее спасибо! <<>>

Почему sizeof для структуры больше суммы размеров полей?

Это не баг, а особенность работы процессора и компилятора, называемая выравниванием памяти (alignment). Компилятор добавляет "пустые" байты (padding) между полями структуры, чтобы ускорить доступ к данным.

Как это работает?

Современные процессоры читают память не побайтово, а блоками по 4 или 8 байт (в зависимости от архитектуры). Если 4-байтовое целое число (int) расположено по адресу, не кратному 4, процессору придётся сделать два обращения к памяти вместо одного. Чтобы этого избежать, компилятор выравнивает каждое поле по его естественной границе:

  • char (1 байт) — может быть по любому адресу
  • short (2 байта) — по чётному адресу (кратному 2)
  • int (4 байта) — по адресу, кратному 4
  • double (8 байт) — по адресу, кратному 8

Разберём ваш пример

Ваша структура:

struct MyStruct {
    char a;      // 1 байт, смещение 0
    int b;       // 4 байта, нужно смещение, кратное 4
    short c;     // 2 байта, нужно чётное смещение
};

Компилятор располагает поля так:

  1. a — байт 0 (смещение 0)
  2. b — не может начаться с байта 1, т.к. 1 не кратно 4. Компилятор пропускает байты 1, 2, 3 (padding). b занимает байты 4-7
  3. c — может начаться с байта 8 (чётное). Занимает байты 8-9

Итого: 10 байт данных + 2 байта padding'а = 12 байт? Нет, это не всё! Компилятор также выравнивает размер всей структуры до размера самого большого поля (int — 4 байта). Поэтому добавляет ещё 2 байта в конце (c 10 по 11), чтобы размер стал кратным 4. Итог: 12 байт.

Как изменить порядок полей, чтобы сэкономить память?

Группируйте поля по убыванию размера:

struct OptimizedStruct {
    int b;       // 4 байта, смещение 0
    short c;     // 2 байта, смещение 4 (чётное)
    char a;      // 1 байт, смещение 6
    // padding: 1 байт в конце (чтобы размер стал кратным 4)
}; // sizeof = 8 байт

Как узнать выравнивание конкретного типа?

Используйте оператор alignof (C++11):

std::cout << alignof(int) << std::endl; // 4 на x86

Можно ли отключить выравнивание?

Да, с помощью директивы #pragma pack:

#pragma pack(push, 1)  // выравнивание по 1 байту
struct PackedStruct {
    char a;
    int b;
    short c;
};
#pragma pack(pop)
// sizeof(PackedStruct) = 7 байт

Но так делать не рекомендуется — доступ к полям будет медленнее, а на некоторых архитектурах (ARM) может вызвать исключение.

Итог

sizeof учитывает padding, который добавляется для производительности. Планируйте порядок полей в структурах, чтобы минимизировать потери памяти, особенно если создаёте много объектов.

<<>>

Дополнение: как ещё влияет выравнивание?

Первый ответ отлично объяснил основы. Хочу добавить, что выравнивание может зависеть от:

  • Архитектуры процессора: на x86-64 int обычно выравнивается по 4 байтам, но компилятор может использовать 8 байт для оптимизации.
  • Настроек компилятора: флаги вроде -malign-double в GCC меняют поведение.
  • Стандарта C++: начиная с C++11, вы можете явно задать выравнивание с помощью alignas:
struct alignas(16) AlignedStruct {
    char a;
    int b;
}; // sizeof = 16 байт

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

Совет: если вы работаете с большими массивами структур или передаёте их по сети, используйте #pragma pack(1) или библиотеки сериализации (например, Protocol Buffers). Для обычного кода лучше довериться компилятору — современные процессоры прощают 3-4 байта padding'а ради скорости.