Unions
A union looks like a struct syntactically, but its memory layout is completely different. Where a struct gives every member its own separate space, a union gives all of its members the exact same space — enough to hold its largest member, and no more. Every member of a union is really just a different way of interpreting the same bytes.
struct vs. union at a glance
struct | union | |
|---|---|---|
Memory layout | Each member gets its own space; total size is (roughly) the sum of all members. | All members share one space; total size is the size of the largest member. |
Values stored | All members hold independent values simultaneously. | Only one member holds a meaningful value at a time. |
Typical use | Grouping several related values together. | Representing one value that can be one of several types (a variant). |
Writing one member, reading another
#include <stdio.h>
union Data {
int i;
float f;
char bytes[4];
};
int main(void) {
union Data d;
d.i = 1078530011; // write through the int member
printf("as int: %d\n", d.i);
printf("as float: %f\n", d.f); // reading through a different member
printf("size of union Data: %zu bytes\n", sizeof(union Data));
return 0;
}as int: 1078530011 as float: 3.141593 size of union Data: 4 bytes
d.i and d.f occupy the exact same 4 bytes. Writing 1078530011 through d.i stores a particular bit pattern; reading that same memory back through d.f reinterprets those same bits as an IEEE 754 float, which happens to be approximately 3.141593. Nothing was "converted" — the bits never changed, only how they were read did.
Practical use: variant data with a tag
In real code, a union is almost always paired with a separate "tag" field that records which member is currently valid — this pattern is called a tagged union, and it is how many languages implement variant/sum types under the hood.
#include <stdio.h>
typedef enum { TYPE_INT, TYPE_FLOAT, TYPE_STRING } ValueType;
typedef struct {
ValueType type; // the tag: which member is currently valid
union {
int i;
float f;
char str[32];
} value;
} Variant;
void print_variant(const Variant *v) {
switch (v->type) {
case TYPE_INT:
printf("int: %d\n", v->value.i);
break;
case TYPE_FLOAT:
printf("float: %f\n", v->value.f);
break;
case TYPE_STRING:
printf("string: %s\n", v->value.str);
break;
}
}
int main(void) {
Variant a = {.type = TYPE_INT, .value.i = 42};
Variant b = {.type = TYPE_STRING, .value.str = "hello"};
print_variant(&a);
print_variant(&b);
return 0;
}int: 42 string: hello
All members of a union share the same memory; its size is the size of its largest member.
Writing one member and reading another reinterprets the same underlying bytes.
Pair a union with a tag field (often an enum) so code always knows which member is currently valid.
Reading a member other than the one last written is undefined behavior in the standard, even though it is widely supported in practice — prefer the tagged pattern.