Синтаксис си/с++

Структуры (struct): объявление, вложенность, выравнивание полей

1 просмотров

Введение

После базового синтаксиса (блок 1) переходим к организации данных — struct объединяет несколько связанных значений разных типов в единую сущность, и в этой статье разберём объявление структур, их вложенность, и менее очевидный, но практически важный аспект — выравнивание полей в памяти.

Концепция

struct объявляет составной тип, группирующий несколько полей (членов) произвольных типов под одним именем — в C структуры могут содержать только данные (без методов, в отличие от C++, где struct практически эквивалентен class с публичным доступом по умолчанию). Структуры могут быть вложенными — поле одной структуры может само быть структурой другого типа, что позволяет естественно моделировать иерархические данные. Выравнивание (alignment) — требование компилятора размещать каждое поле структуры по адресу, кратному определённому значению (обычно зависящему от размера самого поля), что может приводить к «дырам» неиспользуемого пространства (padding) между полями структуры для соблюдения этих требований, и итоговый размер структуры может оказаться больше суммы размеров отдельных полей.

Пример кода

#include <iostream>

struct Point {
    int x;
    int y;
};

struct Rectangle {
    Point topLeft;     // вложенная структура — поле само является структурой
    Point bottomRight;
    const char *label;
};

int main()
{
    Point p1 = {10, 20}; // инициализация через список значений в порядке объявления полей
    Rectangle rect = {{0, 0}, {100, 50}, "Главный прямоугольник"};

    std::cout << "Точка: " << p1.x << ", " << p1.y << std::endl;
    std::cout << "Прямоугольник от (" << rect.topLeft.x << "," << rect.topLeft.y
               << ") до (" << rect.bottomRight.x << "," << rect.bottomRight.y << ")" << std::endl;

    return 0;
}
// Выравнивание и padding — реальный размер структуры может превышать сумму размеров полей
#include <iostream>

struct Inefficient {
    char flag;      // 1 байт
    int value;       // 4 байта — но должен начинаться с адреса, кратного 4!
    char anotherFlag; // 1 байт
};
// Реальный размер скорее всего 12 байт (1 + 3 padding + 4 + 1 + 3 padding), а не 6 (1+4+1)!

struct Efficient {
    int value;        // 4 байта
    char flag;          // 1 байт
    char anotherFlag;   // 1 байт
    // компилятор может добавить 2 байта padding в конце для выравнивания РАЗМЕРА всей структуры
};
// Реальный размер скорее всего 8 байт — заметно меньше, чем у Inefficient, при тех же данных!

int main()
{
    std::cout << "sizeof(Inefficient): " << sizeof(Inefficient) << std::endl;
    std::cout << "sizeof(Efficient): " << sizeof(Efficient) << std::endl;
    return 0;
}

Пояснения к коду

Rectangle с полями topLeft/bottomRight типа Point показывает вложенность структур — доступ к полям вложенной структуры выполняется через цепочку точек (rect.topLeft.x), естественно отражающую иерархическую природу данных. Сравнение Inefficient/Efficient показывает практическое следствие выравнивания — обе структуры содержат идентичный набор полей (int + два char), но порядок их объявления существенно влияет на итоговый размер структуры из-за того, как компилятор должен вставлять padding для соблюдения требований выравнивания каждого поля; группировка полей от больших к меньшим по размеру (как в Efficient) обычно минимизирует количество необходимого padding по сравнению с произвольным, не оптимизированным порядком.

Подводные камни

  • Игнорирование выравнивания при проектировании структур, для которых размер критичен (большие массивы экземпляров структуры, передача данных по сети или сохранение в файл, embedded-системы с ограниченной памятью) — неоптимальный порядок полей может существенно увеличить реальный размер структуры по сравнению с теоретическим минимумом, и для таких критичных случаев стоит явно продумать порядок полей (от больших к меньшим) или использовать директивы компилятора для управления выравниванием (#pragma pack, хотя это снижает производительность доступа к невыровненным полям и должно применяться осознанно).
  • Предположение об одинаковом размере и выравнивании одной и той же структуры на разных платформах/компиляторах без явной проверки — поскольку точные правила выравнивания (и размеры базовых типов) могут отличаться между платформами, структура, чей размер критичен для совместимости (бинарный формат файла, сетевой протокол), должна либо использовать явно фиксированные по размеру типы и явное управление выравниванием, либо использовать более портативный формат сериализации, не зависящий от точного бинарного представления структуры в памяти конкретной платформы.
  • Сравнение структур через == без явно определённого оператора сравнения (для чистого C, не поддерживающего перегрузку операторов) — попытка побайтового сравнения структур через memcmp() может дать неверный результат из-за неопределённого содержимого padding-байтов (которые не гарантированно равны нулю или одинаковы между двумя в остальном идентичными по содержащимся данным структурами), и корректное сравнение структур в чистом C требует явного, поэлементного сравнения каждого значимого поля, а не побайтового сравнения всей структуры целиком.
  • Копирование структур с указателями внутри через простое присваивание, ожидая глубокого копирования данных, на которые указывают эти указатели — присваивание структуры (rect2 = rect1;) копирует поля поверхностно (включая сами значения указателей, но не данные, на которые они указывают), и для структур с указателями, требующих реального, глубокого копирования содержимого, нужна явная, написанная отдельно логика такого копирования (что в C++ естественно решается через конструктор копирования, недоступный для чистых C-структур).