Структуры и объединения
Структуры и объединения
Структура (struct) — способ собрать несколько полей разных типов под одним именем. Объединение (union) — те же поля, но в памяти они делят одно и то же место: в каждый момент времени активно по смыслу одно поле.
Аналогия — структура — анкета с графами "имя", "возраст", "рост"; объединение — один конверт, куда кладут либо паспорт, либо водительские права, но не оба сразу с разным содержимым.
Структуры моделируют сущности — пользователь, пакет в сети, запись в файле. Объединения — экономия памяти и разбор форматов "либо int, либо float" (с осторожностью к типам).
Объявление и определение структуры
Объявление структуры начинается с ключевого слова struct, за которым следует имя структуры. Это имя становится новым идентификатором типа. После имени в фигурных скобках перечисляются поля — переменные различных типов, которые будут входить в состав структуры. Завершается определение точкой с запятой.
Пример:
struct Book {
char title[100];
char author[50];
int year;
float price;
};
Разбор:
struct Bookобъявляет новый пользовательский тип с четырьмя полями разных типов.- Поля
titleиauthor— фиксированные символьные массивы для строковых данных. yearиpriceхранят числовые свойства книги (целое и вещественное).- Определение структуры задаёт формат объекта, но не выделяет память под конкретный экземпляр.
В этом примере создана структура Book, содержащая четыре поля — название книги, имя автора, год издания и цена. Каждое поле имеет свой тип и имя. Память для этих полей выделяется только при создании переменной этого типа, а не при объявлении самой структуры.
Существует также возможность объявить структуру без её определения:
struct Person;
Разбор:
- Это предварительное объявление типа (forward declaration), создающее неполный тип.
- Компилятор узнаёт имя структуры, но пока не знает её размер и состав полей.
- Такой приём нужен, когда достаточно работать с указателями на тип до полного определения.
- Часто применяется для разрыва циклических зависимостей между заголовочными файлами.
Такое объявление создаёт неполный тип. Компилятор знает, что такой тип существует, но не знает его размера и состава. Неполный тип нельзя использовать для создания переменных, но можно объявлять указатели на него. Это полезно при работе с рекурсивными структурами или при разделении интерфейса и реализации.
Полное определение структуры должно быть доступно в том месте программы, где происходит обращение к её полям или создание экземпляров.
Создание переменных структурного типа
После определения структуры можно объявлять переменные этого типа:
struct Book my_book;
Разбор:
- Создаётся переменная
my_bookтипаstruct Book. - Память под все поля структуры выделяется целиком в месте объявления (например, в стеке для локальной переменной).
- Значения полей без явной инициализации остаются неопределёнными для автоматических переменных.
- После создания к полям обращаются через оператор
..
Эта строка резервирует в памяти место, достаточное для хранения всех полей структуры Book. Размер структуры не всегда равен сумме размеров её полей — компилятор может вставлять дополнительные байты между полями для выравнивания по границам памяти, чтобы ускорить доступ к данным. Это поведение зависит от архитектуры процессора и настроек компилятора.
Переменные структурного типа можно инициализировать сразу при объявлении. Существуют два способа инициализации:
- По позиции: значения указываются в том же порядке, в котором объявлены поля.
struct Book novel = {"1984", "George Orwell", 1949, 29.99};
Разбор:
-
Это позиционная инициализация: значения сопоставляются полям строго по порядку их объявления.
-
Строковые литералы копируются в массивы
titleиauthorструктуры. -
1949и29.99инициализируют числовые поляyearиprice. -
Такой стиль короткий, но чувствителен к порядку полей, поэтому при изменении структуры может стать хрупким.
-
По имени: каждому полю явно присваивается значение, независимо от порядка.
struct Book novel = {.title = "1984", .author = "George Orwell", .year = 1949, .price = 29.99};
Разбор:
- Именованная инициализация (
.field = value) явно связывает значение с конкретным полем. - Порядок перечисления полей в таком стиле не критичен для корректности.
- Повышается читаемость: сразу видно, что именно инициализируется.
- Этот подход снижает риск ошибок при добавлении новых полей в структуру.
Именованная инициализация повышает читаемость кода и снижает вероятность ошибок, особенно когда структура содержит много полей.
Доступ к полям структуры
Для обращения к отдельным полям используется оператор точки (.):
my_book.year = 2025;
printf("Книга: %s, автор: %s\n", my_book.title, my_book.author);
Разбор:
my_book.year = 2025;записывает значение в конкретное поле структуры через оператор..printfчитает строковые поля структуры и подставляет их в формат%s.- Доступ по имени поля делает код самодокументируемым: видно, какие части объекта используются.
- Такой стиль подходит для работы со структурами, передаваемыми по значению.
Оператор точки применяется к переменной структурного типа и позволяет читать или изменять значение любого её поля. Все операции над полями выполняются так же, как над обычными переменными соответствующего типа.
Если переменная является указателем на структуру, используется оператор стрелки (->), который совмещает разыменование указателя и доступ к полю:
struct Book *ptr = &my_book;
ptr->price = 34.50;
Разбор:
ptr— указатель на структуруBook, получающий адресmy_book.- Оператор
->объединяет разыменование и доступ к полю в одной записи. ptr->price = 34.50;изменяет полеpriceв исходном объекте, а не в копии.- Это базовый паттерн для функций, которые должны менять структуру "снаружи".
Это эквивалентно записи (*ptr).price = 34.50;, но более кратко и удобно.
Область видимости структур
Структуры подчиняются тем же правилам области видимости, что и переменные и функции. Если структура объявлена вне функций — на уровне файла, — она доступна во всех функциях, расположенных ниже по тексту программы. Такие структуры обычно размещают в заголовочных файлах (.h), чтобы использовать их в нескольких исходных файлах проекта.
Если структура объявлена внутри функции или даже внутри блока кода (например, внутри фигурных скобок в цикле или условии), она существует только в пределах этой области. Вне блока структура недоступна, и попытка использовать её вызовет ошибку компиляции.
Вложенные объявления структур с одинаковыми именами не приводят к конфликту — внутреннее объявление скрывает внешнее в своей области видимости. Однако переменные, созданные до переопределения, сохраняют свою принадлежность к исходному типу.
Копирование и присваивание структур
Структуры одного типа можно присваивать друг другу напрямую:
struct Book original = {"Dune", "Frank Herbert", 1965, 25.0};
struct Book copy = original;
Разбор:
copy = originalвыполняет побитовое копирование всех полей структуры.- Для встроенных типов и статических массивов это обычно безопасно и ожидаемо.
- Если бы внутри структуры были указатели на динамические данные, копировались бы только адреса (поверхностная копия).
- Такой фрагмент полезен для понимания различия между значимыми типами и ссылочной семантикой.
При таком присваивании выполняется побайтовое копирование содержимого. Все поля получают копии значений из исходной структуры. Это работает корректно для простых типов и массивов фиксированного размера. Однако если структура содержит указатели, копируется только адрес, а не данные, на которые он указывает. Такое поведение называется поверхностным копированием и требует осторожности при работе с динамической памятью.
Упрощение синтаксиса с помощью typedef
Часто используется конструкция typedef для создания псевдонима типа структуры:
typedef struct {
char model[30];
int power;
float weight;
} Car;
Разбор:
typedefсоздаёт псевдонимCarдля анонимной структуры, убирая необходимость писатьstructв каждом объявлении.- Поля
model,power,weightописывают свойства автомобиля в одном типе. - Конструкция особенно удобна в публичных API, где важна компактность и читаемость сигнатур.
- Анонимная структура здесь уместна, так как отдельное имя
structне требуется.
Теперь вместо struct Car можно писать просто Car:
Car my_car = {"Tesla Model S", 670, 2100.5};
printf("%s, %d л.с.\n", my_car.model, my_car.power);
Разбор:
- Объявляется переменная
my_carтипаCar, созданного черезtypedef. - Используется позиционная инициализация по порядку полей структуры.
my_car.modelиmy_car.powerчитаются через оператор.как обычные поля.printfс%sи%dвыводит строку модели и мощность в лошадиных силах.- Код короче, потому что не нужно писать
struct Carв каждом объявлении.
Это делает код короче и чище, особенно в больших проектах. При использовании typedef имя структуры можно опустить, если оно не требуется для рекурсивных ссылок или других целей.
Альтернативные способы определения
Хотя основной способ — использование struct и, при необходимости, typedef, существуют и другие подходы. Например, препроцессорная директива #define позволяет создать макрос, раскрывающийся в определение структуры:
#define POINT struct { int x; int y; }
POINT p1 = {10, 20};
Разбор:
- Макрос
POINTподставляет текст структуры на этапе препроцессора. POINT p1 = {10, 20};после раскрытия макроса превращается в объявление переменной анонимной структуры.- Такой приём усложняет отладку, потому что ошибки возникают уже в раскрытом тексте, а не в исходном виде.
- Для обычного кода предпочтительнее
typedef, так как он типобезопаснее и читабельнее.
Такой метод редко используется в современной практике, так как усложняет отладку и снижает читаемость. Он может быть полезен в специфических случаях метапрограммирования или генерации кода, но не рекомендуется для повседневного применения.
Практическое значение структур
Структуры лежат в основе многих концепций системного программирования. Они используются для представления записей в файлах, пакетов в сетевых протоколах, элементов графического интерфейса, узлов деревьев и списков. Без структур невозможно было бы эффективно организовать сложные данные в языке, где отсутствуют встроенные классы или объекты.
Структуры позволяют передавать связанные данные в функции как единый аргумент, а не как множество отдельных параметров. Это упрощает сигнатуры функций и делает код более модульным. Функции могут принимать структуры по значению (копируя их) или по указателю (работая с оригиналом), что даёт контроль над производительностью и семантикой изменений.
Размер структуры и выравнивание (padding)
Компилятор может вставлять пустые байты между полями, чтобы адреса совпадали с требованиями процессора (быстрый доступ к int и double).
struct Example {
char c; /* 1 байт */
/* возможно 3 байта "padding" */
int i; /* 4 байта */
};
Разбор:
- Поле
char cзанимает 1 байт, но следующееint iобычно требует выравнивания по границе 4 байт. - Компилятор может вставить padding между полями, чтобы доступ к
intбыл эффективным на архитектуре. - Из-за padding фактический размер структуры часто больше арифметической суммы размеров полей.
- Это важно учитывать при бинарной сериализации и работе с аппаратными/сетевыми форматами.
sizeof(struct Example) часто равен 8, а не 5. Узнать размер и смещения:
printf("sizeof = %zu\n", sizeof(struct Example));
printf("offset i = %zu\n", offsetof(struct Example, i));
Разбор:
sizeof(struct Example)возвращает полный размер структуры в байтах с учётом выравнивания.offsetof(struct Example, i)показывает смещение поляiот начала структуры.%zu— корректный формат для типаsize_t, который возвращаютsizeofиoffsetof.- Такой диагностический код помогает проверять ABI-предположения и искать скрытый padding.
Заголовок <stddef.h> даёт макрос offsetof. Для сетевых протоколов и файлов на диске иногда задают упакованное выравнивание (#pragma pack — расширение компилятора) или выкладывают поля вручную в uint8_t буфер.
Вложенные структуры и массивы полей
struct Point { int x; int y; };
struct Line {
struct Point start;
struct Point end;
};
Разбор:
Lineиспользует композицию: её поля сами являются структурамиPoint.- Такой подход моделирует иерархические данные без объектной системы.
- Доступ к вложенным данным выполняется цепочкой полей, например
line.start.x. - Память под вложенные структуры размещается внутри внешней структуры как единый блок.
Доступ: line.start.x. Массив внутри структуры:
struct Buffer {
size_t len;
char data[128];
};
Разбор:
lenхранит текущую длину полезных данных, аdata[128]— фиксированный внутренний буфер.- Размер такого объекта известен на этапе компиляции, что упрощает выделение и копирование.
- Поле
dataвстроено в структуру, поэтому отдельныйmallocдля него не нужен. - Этот шаблон часто используется для небольших сообщений и stack-friendly буферов.
data занимает 128 байт внутри структуры (фиксированный размер). Для переменной длины используют указатель char *data и отдельный malloc — см. Основы.
Объединения (union)
Объявление похоже на структуру, но ключевое слово union:
union Value {
int i;
float f;
char bytes[4];
};
union Value v;
v.i = 42;
printf("%d\n", v.i);
Разбор:
- Объявляется
union, гдеi,fиbytesделят одну и ту же память. v.i = 42;записывает биты целого значения в общий блок объединения.printf("%d\n", v.i);читает те же биты в том же представлении, поэтому вывод предсказуем.- Если после этого записать
v.f, интерпретацияv.iуже не будет логически валидной.
Все поля начинаются с одного адреса. sizeof(union Value) равен размеру самого большого поля (с учётом выравнивания). Запись в v.f перезаписывает биты, которые интерпретировались как v.i — читать другое поле без явного соглашения в коде опасно.
Типичное применение — разбор бинарного формата:
union {
uint32_t word;
uint8_t byte[4];
} u;
u.word = 0x01020304;
/* порядок byte[0]..byte[3] зависит от endianness машины */
Разбор:
- В объединении
wordиbyteсмотрят на одни и те же 4 байта разными "линзами". - Запись
u.word = 0x01020304;позволяет затем прочитать отдельные байты черезu.byte[index]. - Фактический порядок байтов зависит от endianness платформы, поэтому результат может отличаться между архитектурами.
- Пример полезен для диагностики, но в переносимом протокольном коде безопаснее использовать маски и сдвиги.
Для переносимости между платформами предпочтительны явные сдвиги и маски с uint32_t, а не "смотреть в union".
Объединение с дискриминантом (идиома tagged union): рядом хранят enum "какой вариант активен" и union с полями; переключатель switch по enum задаёт, какое поле читать.
Код ITЗагрузка примера кода…
Разбор:
ValueKind kind— "тег", который говорит, какое поле union сейчас активно.unionэкономит память:iиfне хранятся одновременно в разных ячейках.switch (v->kind)выбирает корректное поле и предотвращает чтение "не того" варианта.const Value *vпередаёт объект только для чтения через указатель без копирования структуры.- Это базовая идиома для безопасной работы с вариантами данных в чистом C.
Пример использования узла связного списка:
Код ITЗагрузка примера кода…
Разбор:
typedef struct Node { ... } Node;описывает самоссылочный узел списка.head— указатель на первый узел;NULLозначает пустой список.malloc(sizeof *n)выделяет память ровно под одинNode(размер берётся из типа указателя).n->dataиn->next— доступ к полям структуры через указатель.- Вставка "в голову" (
n->next = head; head = n;) — самый простой способ добавить элемент. - Память под узлы нужно освобождать (
free) при удалении списка, иначе будет утечка.
Память из malloc берётся из кучи процесса; каждому успешному malloc соответствует ровно один free на том же указателе (идиомы). Размер выделяйте через sizeof *ptr, а не sizeof (тип) вручную — так проще менять тип позже.
Битовые поля
Внутри структуры можно объявить поля по несколько бит:
struct Flags {
unsigned ready : 1;
unsigned mode : 3;
};
Разбор:
- Объявляются битовые поля:
readyзанимает 1 бит,mode— 3 бита. - Такой синтаксис позволяет компактно хранить флаги в структуре.
- Реальное расположение бит и итоговый размер структуры зависят от компилятора и ABI.
- Для строго переносимого сетевого/дискового формата обычно предпочитают ручные маски (
&,|,<<,>>).
Компилятор упаковывает биты в целые слова. Размер и порядок бит зависят от реализации — для переносимых протоколов чаще используют маски и сдвиги вручную.
Структура и указатель — разбор выражений
| Выражение | Что происходит |
|---|---|
s.year | поле year у переменной s |
p->year | то же, если p указывает на структуру ((*p).year) |
&s | адрес всей структуры |
sizeof(s) | размер всей структуры в байтах |
Передача в функцию:
void print_book(struct Book b); /* копия всей структуры */
void update_book(struct Book *b); /* адрес; изменения видны снаружи */
Разбор:
- Первая сигнатура передаёт структуру по значению, поэтому внутри функции создаётся копия
b. - Вторая принимает указатель, то есть работает с исходным объектом и может менять его поля "на месте".
- Передача по указателю обычно дешевле для больших структур, потому что избегает полного копирования.
- Выбор между этими вариантами задаёт семантику API: "читать копию" или "изменять оригинал".
Для больших структур в API почти всегда используют указатель, чтобы не копировать сотни байт на каждый вызов.
См. также: Хеш-таблица, Память процесса.