devnotes.

Unions, typedef, and Fixed-Underlying Enums

A union looks like a struct syntactically, but all its members share the same memory location instead of each having their own — its size equals its largest member, not the sum of all of them.

typedef union {
    int i;
    float f;
    char bytes[4];
} Variant;

int main(void) {
    Variant v;
    v.i = 65;
    printf("%d\n", v.i);        // 65
    printf("%c\n", v.bytes[0]); // 'A' — same memory, reinterpreted as a char

    printf("%zu\n", sizeof(Variant)); // 4 — size of the largest member (int or float)
    return 0;
}

Only one union member holds a "valid" value at a time — writing to v.i and then reading v.f doesn't convert the integer to a float, it reinterprets the exact same 4 bytes as if they were a float, usually producing a meaningless result. Unions are used for genuinely low-level tasks: examining the raw bytes of a value (as above), building tagged variant types (a struct pairing a union with an enum tag saying which member is currently valid), and squeezing memory in constrained environments.

typedef gives an existing type a new, often shorter or clearer, name — used constantly with structs and unions as shown, but works for any type: typedef unsigned long ulong;.

C23 also lets an enum specify its exact underlying integer type, which was previously left to the compiler:

enum Status : unsigned char { OK = 0, WARNING = 1, ERROR = 2 };

This guarantees the enum's storage size across compilers and platforms, which matters when an enum's exact byte layout is significant — for example, when it's part of a struct written directly to a file or sent over a network.

C Unions, typedef, and Fixed-Underlying-Type Enums (C23) | DevNotes