Почему sizeof(структуры) не равен сумме sizeof(полей)? Полный разбор

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Как такое возможно? Введение в парадокс sizeof

Представьте: вы создаёте простую структуру на C++ с двумя полями — char (1 байт) и int (4 байта). Логично предположить, что её размер будет 5 байт. Вы пишете код, вызываете sizeof и... получаете 8 байт. «Бред!» — думаете вы. «Компилятор сошёл с ума?»

Этот вопрос — один из самых популярных на Stack Overflow (более 200 000 просмотров). И он не про ошибку компилятора. Он про то, как процессор и оперативная память на самом деле работают на низком уровне. В этой статье мы разберём выравнивание структур в C++, понятие padding (заполнения), и научимся предсказывать реальный размер структур. А главное — узнаем, как этим управлять, когда это действительно нужно.

Почему 1 + 4 ≠ 5? Физика процесса

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

Чтобы этого избежать, компилятор вставляет «пустые» байты (padding) между полями структуры. Он выравнивает каждое поле по адресу, кратному его размеру. Это называется естественным выравниванием (natural alignment).

// Простой пример, который всех удивляет
#include <iostream>

struct Example {
    char c;   // 1 байт
    int  i;   // 4 байта
};

int main() {
    std::cout << "Размер char: " << sizeof(char) << std::endl;    // 1
    std::cout << "Размер int: "  << sizeof(int)  << std::endl;    // 4
    std::cout << "Размер struct: " << sizeof(Example) << std::endl; // 8 (!)
    return 0;
}

Как это выглядит в памяти? c занимает байт 0. Байт 1, 2, 3 — это padding. А i начинается с байта 4. Итого: 1 (c) + 3 (пусто) + 4 (i) = 8 байт.

Правила выравнивания: как компилятор принимает решение

Компилятор следует простым, но строгим правилам. Зная их, вы сможете предсказывать размер структуры без запуска программы.

  • Правило 1: Порядок полей сохраняется. Компилятор не переставляет поля в памяти (хотя некоторые компиляторы могут это делать при оптимизации, но стандарт это не гарантирует).
  • Правило 2: Каждое поле выравнивается по адресу, кратному его размеру. char (1 байт) может быть где угодно. short (2 байта) — по чётным адресам. int (4 байта) — по адресам, кратным 4. double (8 байт) — по адресам, кратным 8.
  • Правило 3: Размер всей структуры должен быть кратен выравниванию самого большого поля. Это нужно для того, чтобы массивы структур работали корректно. Если самая большая переменная — double (8 байт), то размер структуры будет кратен 8.

Давайте разберём более сложный пример:

struct Complex {
    char  a; // 1 байт
    int   b; // 4 байта
    short c; // 2 байта
    char  d; // 1 байт
};
// Каким будет sizeof? Считаем вместе.

Пошаговый разбор:

  • a (char) — байт 0.
  • b (int) — должен быть по адресу, кратному 4. Следующий такой адрес — 4. Значит, padding: байты 1, 2, 3. Итого после b: 8 байт (0-7).
  • c (short) — должен быть по чётному адресу. Текущая позиция — 8 (чётная). Отлично. Итого после c: 10 байт (0-9).
  • d (char) — может быть где угодно. Ставим в байт 10. Итого после d: 11 байт (0-10).
  • Финальное выравнивание: Самое большое поле — int (4 байта). Размер структуры должен быть кратен 4. Следующее число после 11, кратное 4 — это 12. Добавляем 1 байт padding в конце.

Результат: sizeof(Complex) == 12. Запустите код — убедитесь сами.

Как оптимизировать структуры? Искусство перестановки полей

Зная механику выравнивания в C++, вы можете уменьшить размер структуры, просто изменив порядок объявления полей. Сортируйте поля по убыванию размера!

Сравните два варианта:

// Плохой порядок: 12 байт
struct BadOrder {
    char   a; // 1
    double b; // 8
    char   c; // 1
};
// sizeof = 24 (1 + 7 padding + 8 + 1 + 7 padding)

// Хороший порядок: 16 байт
struct GoodOrder {
    double b; // 8
    char   a; // 1
    char   c; // 1
};
// sizeof = 16 (8 + 1 + 1 + 6 padding)

Мы сэкономили 8 байт на одной структуре! Если у вас массив из миллиона таких структур — экономия 8 мегабайт. Для встраиваемых систем или высоконагруженных серверов это критично. Это и есть оптимизация памяти структур.

А что, если мне нужно упаковать структуру плотно?

Иногда выравнивание мешает. Например, при передаче данных по сети или при работе с бинарными форматами файлов, где каждый байт на счету. В таких случаях используется директива #pragma pack.

#include <iostream>
#pragma pack(push, 1) // Устанавливаем упаковку в 1 байт

struct PackedStruct {
    char c; // 1
    int  i; // 4
};

#pragma pack(pop) // Восстанавливаем стандартное выравнивание

int main() {
    std::cout << "Размер упакованной структуры: " 
              << sizeof(PackedStruct) << std::endl; //                

Блоги

Рекомендаций по книгам