Перечисления, typedef и объединения

Урок собирает три полезных инструмента типов: enum (именованные целочисленные константы), typedef (псевдонимы типов для читаемости) и union (несколько полей, делящих одну память).
enum даёт имена числам, typedef даёт короткие имена типам, а union — это место, где разные поля лежат в одной и той же памяти, и активно только одно из них.

Перечисление enum заменяет «магические числа» осмысленными именами. Под капотом это обычные целые, начинающиеся с 0:

enum Direction { NORTH, EAST, SOUTH, WEST };  // 0, 1, 2, 3

enum Direction d = EAST;
if (d == EAST) {
    printf("Идём на восток\n");
}
printf("EAST = %d\n", EAST);   // 1

Ключевое слово typedef создаёт псевдоним для типа — чаще всего чтобы не писать struct каждый раз и сделать код чище:

typedef struct {
    int x, y;
} Point;          // теперь просто Point, без слова struct

Point p = {1, 2};   // вместо struct ... p

Объединение union внешне похоже на структуру, но все его поля делят одну и ту же память. Размер union равен размеру самого большого поля, и в каждый момент осмысленно лишь одно из них:

union Value {
    int    i;
    float  f;
    char   c;
};

union Value v;
v.i = 65;
printf("%d\n", v.i);   // 65
v.f = 3.14f;           // та же память — теперь там float
printf("%f\n", v.f);   // 3.14 (но v.i уже испорчен!)

Как работает под капотом

Главное отличие union от struct — раскладка в памяти. Структура кладёт поля рядом, union — поверх друг друга:

struct (поля РЯДОМ):           union (поля ПОВЕРХ друг друга):
  struct { int i; float f; }     union { int i; float f; char c; }

  +--------+--------+            +------------------+
  |  i (4) |  f (4) |            |   общая память   |
  +--------+--------+            |   4 байта        |
  размер = 8 байт                +------------------+
                                  i, f, c — все живут ЗДЕСЬ,
                                  размер = 4 байта (макс. поле)
                                  активно только одно за раз!

union экономит память, когда значение может быть одного из нескольких типов, но никогда — нескольких сразу. Чтобы знать, какое поле сейчас активно, union обычно сопровождают enum-тегом — получается «размеченное объединение».

Частые ошибки

  • Чтение «не того» поля union. Записали v.f, читаете v.i — получите мусор: память одна, но интерпретация разная.
  • Забыли отслеживать активное поле union. Без enum-тега невозможно понять, что в union лежит сейчас.
  • Полагаются на конкретные числа enum. Значения по умолчанию 0,1,2..., но их легко сбить ручным присваиванием — лучше не завязываться на них.
  • Путают union со struct. Ожидают, что поля union независимы, а они делят память.

Best practices

  • Используйте enum вместо «магических чисел» — код становится самодокументируемым.
  • Применяйте typedef для структур и сложных типов, чтобы повысить читаемость, но не злоупотребляйте им для простых типов.
  • union сопровождайте enum-тегом активного типа («размеченное объединение») — это делает его безопасным.

enum и «размеченное объединение» удобно показать на Python: имена-константы и явный тег типа, который говорит, как читать значение.

# enum как именованные константы
NORTH, EAST, SOUTH, WEST = 0, 1, 2, 3
d = EAST
print("Направление EAST =", d)

# "размеченное объединение": тег + значение в одной ячейке
def make_value(tag, value):
    return {"tag": tag, "data": value}   # tag говорит, как читать data

v = make_value("int", 65)
if v["tag"] == "int":
    print("Целое:", v["data"])

v = make_value("float", 3.14)            # перезаписали — теперь float
if v["tag"] == "float":
    print("Дробное:", v["data"])

Та же логика на Python ▶ — поле tag играет роль enum-метки, без которой нельзя понять, как трактовать значение. Именно так делают union безопасным в C.

Размеченное объединение на практике

Связка enum плюс union — это не академическая экзотика, а рабочий паттерн, на котором держатся интерпретаторы, парсеры и форматы данных. Представьте «значение», которое может быть числом, строкой или булевым: хранить под каждый вариант отдельное поле расточительно, ведь активно всегда одно. Решение — структура с двумя полями: enum-тег, говорящий «сейчас здесь число», и union, физически хранящий это число (или строку, или булево) в общей памяти. Любой код, работающий с таким значением, сперва смотрит на тег, а потом читает соответствующее поле union — так делают, например, JSON-библиотеки и движки динамических языков. Это даёт компактность union и безопасность типобезопасного доступа одновременно. Освоив этот паттерн, вы поймёте, как языки вроде Python представляют свои «универсальные» значения на уровне C.

Итоги

Три инструмента типов: enum даёт имена целочисленным константам и убирает «магические числа»; typedef создаёт псевдонимы типов ради читаемости; union размещает несколько полей в общей памяти, экономя её, но позволяя хранить лишь одно значение за раз. Безопасный union всегда сопровождают enum-тегом активного поля.

Проверьте себя
1. Чем union отличается от struct в плане памяти?
AНичем, это синонимы
BВ struct поля лежат рядом, а в union все поля делят одну память, и активно лишь одно за раз
Cunion всегда больше struct
Dunion нельзя использовать с числами
2. Для чего обычно используют enum?
AДля выделения памяти
BЧтобы заменить «магические числа» осмысленными именованными целочисленными константами
CЧтобы сравнивать строки
DЧтобы ускорить циклы