Объединения в C (union): эффективное использование памяти для разных типов данных
Что такое объединение (union)?
Объединение — пользовательский тип, позволяющий хранить разные типы данных в одной и той же области памяти. В любой момент времени активно только одно поле. Размер объединения = размер самого большого поля (с учётом выравнивания).
Объявление и определение
union Data {
int i;
float f;
char str[20];
}; // размер = max(sizeof(int), sizeof(float), 20) = 20 байт (обычно выровнено до 24)
union Data data; // переменная объединения
Доступ к полям
Синтаксис как у структур: data.i, data.f, data.str. Но писать можно только в одно поле, читать — то же поле, в которое писали последним.
Пример: демонстрация перезаписи
union Data data;
data.i = 10;
printf("i = %d\n", data.i); // 10
data.f = 3.14f;
printf("f = %f\n", data.f); // 3.140000
// data.i теперь содержит биты 3.14f как int (мусорное значение)
strcpy(data.str, "Hello");
printf("str = %s\n", data.str); // Hello
// data.i и data.f теперь содержат части строки
Правильное использование: одно поле за раз
union Data data;
data.i = 42;
printf("int: %d\n", data.i); // 42
data.f = 2.71f;
printf("float: %f\n", data.f); // 2.71
strcpy(data.str, "C union");
printf("string: %s\n", data.str); // C union
Когда использовать union?
- Экономия памяти — когда объект может быть одного из нескольких типов, но не всех сразу (вариантный тип / tagged union).
- Представление данных в разных форматах — побитовый доступ, type punning (с осторожностью).
- Низкоуровневое программирование — сетевые пакеты, файловые форматы, регистры железа.
Tagged Union (дискриминированное объединение)
Комбинация enum (тег) + union для безопасного варианта:
enum ValueType { TYPE_INT, TYPE_FLOAT, TYPE_STRING };
struct Variant {
enum ValueType type;
union {
int i;
float f;
char *s; // указатель на строку (память управляется отдельно)
} value;
};
void print_variant(struct Variant *v) {
switch (v->type) {
case TYPE_INT: printf("int: %d\n", v->value.i); break;
case TYPE_FLOAT: printf("float: %f\n", v->value.f); break;
case TYPE_STRING: printf("string: %s\n", v->value.s); break;
}
}
Анонимные объединения (C11)
Внутри структуры можно объявить union без имени — его поля становятся доступны напрямую:
struct Packet {
int header;
union {
struct { int x, y; } mouse;
struct { int key, mods; } keyboard;
}; // анонимный union
// доступ: pkt.mouse.x, pkt.keyboard.key
};
Type Punning (переинтерпретация типов) — осторожно!
Чтение из поля union, отличного от последнего записанного — implementation-defined (C99+C11 разрешают для union, но не для приведения указателей).
// Разрешено в C (не в C++!):
union { float f; uint32_t u; } converter;
converter.f = 1.0f;
uint32_t bits = converter.u; // биты IEEE 754 float как int
// НЕ ДЕЛАЙТЕ так (strict aliasing violation):
float f = 1.0f;
uint32_t bits = *(uint32_t*)&f; // UB! Компилятор может оптимизировать неправильно.
Выравнивание
Адрес union выравнивается по максимальному выравниванию его полей. _Alignof(union Data) (C11).
Сравнение struct vs union
| Характеристика | struct | union |
|---|---|---|
| Память | Сумма размеров полей + padding | Макс. размер поля + padding |
| Активные поля | Все одновременно | Только одно в данный момент |
| Использование | Агрегация связанных данных | Вариантность, экономия памяти, type punning |
| Инициализация | Все поля (или subset с нулем) | Только первое поле (или designated) |
Инициализация union
union Data d1 = { .i = 42 }; // designated (C99) — рекомендуется
union Data d2 = { 3.14f }; // инициализирует ПЕРВОЕ поле (f если float первым)
union Data d3 = { .str = "hello" }; // designated для любого поля
Пример: парсинг сетевого пакета
#pragma pack(push, 1) // без padding (GCC/Clang/MSVC)
struct PacketHeader {
uint16_t magic;
uint8_t version;
uint8_t type;
uint32_t payload_len;
};
union PacketPayload {
struct { uint32_t user_id; char name[32]; } login;
struct { uint32_t msg_id; char text[256]; } message;
struct { uint32_t code; char reason[64]; } error;
uint8_t raw[256]; // сырой доступ
};
#pragma pack(pop)
// Использование:
union PacketPayload payload;
payload.login.user_id = 12345;
strcpy(payload.login.name, "alice");
Частые ошибки
- Чтение неактивного поля (кроме type punning через union).
- Нарушение строгого алиасинга (strict aliasing) через приведение указателей вместо union.
- Забывание тега (discriminant) в tagged union → логические ошибки.
- Указатели в union — управление памятью строк/буферов ложится на программиста.
Язык C
- Микропроцессоры: эволюция вычислений с запрограммированной памятью
- Полные данные по ОУ: даташиты National Semiconductor и CA3130
- Понимание типов данных C++: основы, модификаторы и лучшие практики
- Типы данных в C: int, float, char и другие — полное руководство
- Объединения в C: определение, использование и сравнение со структурами
- Освоение типов данных в Python: практическое руководство
- Типы данных в C: полное руководство по целочисленным, вещественным и производным типам
- Структуры данных в C++: массивы, структуры и эффективное программирование
- Абстракция данных в C++: разделение интерфейса и реализации
- Инкапсуляция данных в C++: защита вашего кода