C in 100 Seconds: Unions — Shared Memory | Episode 25
Video: C in 100 Seconds: Unions — Shared Memory One Field at a Time | Episode 25 by Taught by Celeste AI - AI Coding Coach
C Unions: Shared Memory, One Field at a Time
A
unionis like a struct, but all fields share the same memory.sizeof(union)is the size of the largest field. Used for variant types and low-level binary parsing.
Unions look like structs but with a twist: only one field is "live" at a time. Reading a field other than the last one written returns garbage (or, occasionally, useful reinterpretation).
The basic shape
#include <stdio.h>
union Value {
int i;
float f;
char c;
};
int main() {
union Value v;
v.i = 42;
printf("int: %d\n", v.i);
v.f = 3.14;
printf("float: %.2f\n", v.f);
printf("int after float: %d (garbage)\n", v.i);
printf("\nsizeof union: %lu\n", sizeof(union Value)); // 4 (size of largest)
printf("sizeof int: %lu\n", sizeof(int)); // 4
printf("sizeof float: %lu\n", sizeof(float)); // 4
return 0;
}
Same memory, multiple views
union Value v;
v.i = 42; // writes 4 bytes
v.f = 3.14; // overwrites the same 4 bytes
v.i; // now reads the bytes of 3.14 as if they were an int (garbage value)
In a struct, each field has its own memory. In a union, all fields overlap — they live at the same address.
struct Both { int i; float f; }; // 8 bytes (4 + 4, possibly + padding)
union Either { int i; float f; }; // 4 bytes (max of the two)
Variant types (tagged unions)
The most common real use:
typedef enum { TYPE_INT, TYPE_FLOAT, TYPE_STRING } ValueType;
typedef struct {
ValueType type;
union {
int i;
float f;
char *s;
} as;
} Value;
void print(Value v) {
switch (v.type) {
case TYPE_INT: printf("int: %d\n", v.as.i); break;
case TYPE_FLOAT: printf("float: %.2f\n", v.as.f); break;
case TYPE_STRING: printf("string: %s\n", v.as.s); break;
}
}
Value v = {.type = TYPE_INT, .as.i = 42};
print(v);
The type tag tells you which union field is live. The union as saves space — only one of the three is allocated, not all three.
This pattern is how interpreters represent dynamically-typed values, how database row formats work, etc.
Low-level reinterpretation
union FloatBits {
float f;
uint32_t u;
};
union FloatBits b = { .f = 3.14 };
printf("0x%x\n", b.u); // bytes of 3.14 as hex
You can write a float and read it as an int — bypassing the type system. Used for:
- Reading IEEE-754 floating-point bits.
- Endian-neutral byte access.
- Hash functions on float values.
This used to violate strict-aliasing rules in some contexts; the C committee added a special exception for type-punning via unions in C11. (Older C, you'd use memcpy instead.)
Anonymous unions
C11 added anonymous unions:
struct Tagged {
int type;
union {
int i;
float f;
}; // no name — fields are accessed directly
};
struct Tagged t;
t.i = 42; // direct, no .as. prefix
t.f = 3.14; // same
Cleaner than the named-union style. Available since C11; check compiler support.
Memory layout
union {
char c; // 1 byte
int i; // 4 bytes
double d; // 8 bytes
} v;
sizeof(v) // 8 — size of largest field
The union is exactly the size of its biggest field (plus alignment padding). All fields start at the same offset (0).
For mixed-size fields, smaller writes leave the upper bytes of larger fields as whatever was there before. Reading the larger field after a smaller write gives partial garbage:
union { char c; int i; } v;
v.i = 0x12345678;
v.c = 'A'; // overwrites just the first byte
// v.i is now 0x12345641 (or 0x41345678 on big-endian)
When to use
- Tagged unions / variants. Save space when only one of N values is needed.
- Binary protocols. Parsing fixed-size records where one field is one of several types.
- Hardware register access. Memory-mapped registers viewed as struct or as raw int.
- Type-punning. Float bits, etc.
When not to use
- "I want to save space and don't need both at once." Modern compilers and big RAM make this premature optimization. A struct is clearer.
- "I want polymorphism." Better in C++ via inheritance, or via function pointers in C (episode 27).
Common mistakes
Reading the wrong field. Writing v.f = 3.14 and reading v.i gives the bit pattern of 3.14, not 3 or 0. Misuse — always know which field is "live."
No type tag. Without a tag, the union itself doesn't know which field is current. Always pair with an enum or other tracker.
Different sizes. A union of int and double — writing int only sets 4 of the 8 bytes; the other 4 keep whatever was there.
Memory copying with memcpy. memcpy(&u1, &u2, sizeof(u1)) copies all bytes — including the bytes that aren't part of the live field. May leak garbage into u1's "active" field. Usually OK for unions of equal-sized fields.
Strict-aliasing in older C. Pre-C11, type-punning via unions was technically undefined. Use memcpy for portable bit-reinterpretation.
What's next
Episode 26: typedef. Custom type names. The typedef we've been using since episode 23, in detail.
Recap
union { type1 a; type2 b; ... } — all fields share memory; size = largest field. Reading a non-live field is undefined. Use for: variant types (paired with enum tag), low-level binary access, type-punning floats. Anonymous unions (C11+) skip the field-prefix. Always know which field is live; always pair with a type tag for variants.
Next episode: typedef.