Unlike a struct, where every member gets its own dedicated memory and the struct's total size is roughly the sum of all members, a union's members all overlap the SAME memory — the union's size is just large enough to hold its biggest member, and writing to one member overwrites whatever was in any other member sharing that space. This makes a union useful specifically when only one of several possible interpretations of a value is needed at a time, and it's the classic mechanism for type punning — reinterpreting the same raw bytes as a different type.
All members share one address
sizeof(Value) equals sizeof(double) (its largest member) — not the sum of int + float + double the way a struct would be. Setting data.d then reading data.i would give nonsense, since they're literally the same bytes interpreted as two different types.
union Value {
int i;
float f;
double d;
};
union Value data;
data.i = 42;
printf("%d\n", data.i); // 42
data.d = 3.14; // OVERWRITES the same memory data.i used
printf("%d\n", data.i); // garbage — not 42 anymore, i's bytes were reinterpreted
printf("%zu\n", sizeof(union Value)); // size of the LARGEST member (double), not the sumTagged unions: tracking which member is actually valid
Since a union alone can't tell you which member was last written, pairing it with a separate "tag" field (often inside a wrapping struct) is the idiomatic way to safely know which interpretation is currently meaningful — this is C's rough equivalent of a discriminated union or a Rust enum with data.
enum ValueType { TYPE_INT, TYPE_FLOAT };
struct TaggedValue {
enum ValueType type; // the tag: tells you which member is currently valid
union {
int i;
float f;
} value;
};
struct TaggedValue v = { .type = TYPE_INT, .value.i = 42 };
if (v.type == TYPE_INT) {
printf("%d\n", v.value.i); // safe — we KNOW it's the int member
}Type punning: examining a value's raw bytes
This lets low-level code inspect a float's underlying bit pattern directly — a common technique in numerical and embedded code, though the C standard technically only fully guarantees this specific pattern (reading a different member than was last written) works portably as of more recent standard clarifications; it has long been common practice regardless.
union FloatBits {
float f;
unsigned int bits;
};
union FloatBits fb;
fb.f = 1.0f;
printf("%x\n", fb.bits); // the raw IEEE 754 bit pattern of 1.0f, as a hex integer