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.