Avatar ·

Почему sizeof(структура) не равен сумме sizeof всех её полей в C++?

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

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

Нет, это не баг компилятора. Это особенность работы с памятью, называемая выравниванием (alignment) и заполнением (padding). Процессор не читает память побайтово — он работает с данными блоками, размер которых обычно равен 4 или 8 байтам (в зависимости от архитектуры). Если данные не выровнены по границе их размера, процессору приходится делать два обращения к памяти вместо одного, что замедляет программу.

Рассмотрим ваш пример:

struct MyStruct {
    char c;    // 1 байт, выравнивание 1
    int i;     // 4 байта, выравнивание 4
    short s;   // 2 байта, выравнивание 2
};

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

  • c занимает байт 0.
  • i должно начинаться с адреса, кратного 4. После c остались байты 1, 2, 3 — они не подходят. Компилятор пропускает их (это и есть padding) и размещает i начиная с байта 4.
  • s должно начинаться с чётного адреса. После i (байты 4-7) идёт байт 8 — он чётный, поэтому s размещается на байтах 8-9.
  • Теперь структура занимает байты 0-9 (10 байт). Но! Размер всей структуры должен быть кратен максимальному выравниванию среди её полей (здесь это 4 — от int). 10 не кратно 4, поэтому компилятор добавляет ещё 2 байта padding в конец (байты 10-11). Итог: 12 байт.

Вот как это выглядит в памяти (X — padding):

| c | X | X | X | i | i | i | i | s | s | X | X |
  0   1   2   3   4   5   6   7   8   9  10  11

Почему порядок полей важен?

Если переставить поля, размер может измениться. Попробуйте:

struct Optimized {
    int i;    // байты 0-3
    short s;  // байты 4-5
    char c;   // байт 6
}; // Макс. выравнивание = 4, размер 7 → округляем до 8

Теперь padding нужен только в конце (1 байт), и структура занимает 8 байт вместо 12. Это называется оптимизацией размера структуры — группировка полей по убыванию размера.

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

Используйте оператор alignof:

#include <iostream>
struct Test {
    char c;
    double d;
};
int main() {
    std::cout << "Выравнивание double: " << alignof(double) << "\n";
    std::cout << "Выравнивание структуры: " << alignof(Test) << "\n";
    std::cout << "Размер структуры: " << sizeof(Test) << "\n";
    return 0;
}

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

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

#pragma pack(push, 1) // упаковка по 1 байту
struct Packed {
    char c;
    int i;
    short s;
};
#pragma pack(pop)

int main() {
    std::cout << sizeof(Packed); // 7 — но это медленнее!
}

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

Итог

  • Размер структуры всегда ≥ суммы размеров полей из-за padding.
  • Padding добавляется для выравнивания полей по границам, кратным их размеру.
  • Размер структуры округляется до кратного максимальному выравниванию её полей.
  • Правильная сортировка полей (от большего к меньшему) экономит память.
<<>>

Практический совет: как минимизировать размер структуры

В дополнение к основному ответу — вот несколько правил, которые помогут вам писать компактные структуры:

  1. Сортируйте поля по убыванию размера: сначала идут типы с большим выравниванием (double, long long, указатели), затем int, short, char. Это минимизирует количество "дырок" между полями.
  2. Группируйте мелкие поля: если у вас несколько char или bool, ставьте их рядом — они займут один блок.
  3. Используйте битовые поля для флагов:
struct Flags {
    unsigned int a : 1; // 1 бит
    unsigned int b : 3; // 3 бита
    unsigned int c : 4; // 4 бита
    // Итого: 8 бит = 1 байт (но может быть больше из-за выравнивания)
};

Если вам критична экономия памяти (например, в embedded-системах), используйте #pragma pack(1), но помните о возможном замедлении. В 99% случаев современные компиляторы сами хорошо оптимизируют расположение полей, если вы не вмешиваетесь.

Также полезно знать, что порядок полей в памяти гарантированно совпадает с порядком их объявления (до C++11 это было нестрого, но сейчас работает). А вот между классами с наследованием могут быть дополнительные padding'ы для выравнивания базовых классов.