Verify with sizeof that the size matches the largest member
The endianness check is the most practical takeaway
If it doesn't click, move on β come back when you hit a real use case
π‘ Tip: Real-world uses are tagged unions and type punning. For beginners, mastering struct is enough.
What is a union
A union is a type whose members all share the same memory. Its size equals that of its largest member, and only one member holds a meaningful value at a time.
#include<stdio.h>union Data {
int i;
float f;
char s[8];
};
intmain(void) {
union Data d;
d.i = 42;
printf("d.i = %d\n", d.i); // 42
d.f = 3.14f; // i becomes invalid nowprintf("d.f = %f\n", d.f);
printf("d.i = %d\n", d.i); // bit pattern of the floatprintf("sizeof = %zu\n", sizeof(d));// 8 (largest member)return0;
}
How it differs from struct: A struct places members side by side (the total is the sum of their sizes); a union overlays them (the total is the size of the largest).
struct vs union (memory picture)
struct S { int a; int b; }
a
a
a
a
a
0-3
b
b
b
b
b
4-7
sizeof = 8 bytes. a and b are stored independently.
union U { int a; int b; }
a = b
a/b
a/b
a/b
a/b
0-3
sizeof = 4 bytes. Writing a also changes b β they share the same location.
Experiment
#include<stdio.h>struct S { int a; int b; };
union U { int a; int b; };
intmain(void) {
struct S s = {10, 20};
printf("struct: a=%d b=%d size=%zu\n", s.a, s.b, sizeof(s));
// β a=10 b=20 size=8union U u;
u.a = 10;
u.b = 20;
printf("union: a=%d b=%d size=%zu\n", u.a, u.b, sizeof(u));
// β a=20 b=20 size=4 (same slot!)
}
Size & alignment
A union is at least as large as its biggest member, and alignment requirements may add trailing padding.
union Mix {
char c; // 1 byteint i; // 4 bytesdouble d; // 8 bytes
};
// sizeof(union Mix) is at least 8, aligned to double
The 4 bytes of int i = 1: little-endian (x86 etc.): 01 00 00 00 β c[0] == 1 big-endian (some ARM etc.): 00 00 00 01 β c[0] == 0
Without a union:int n = 1; char *p = (char*)&n; works too, but the union version is generally friendlier to the compiler's strict-aliasing rules.
Tagged union (variant pattern)
When a value can be one of several types, pair a type tag (an enum) with a data union inside a struct. It's C's take on a sum type, similar to Rust's enum.
#include<stdio.h>enum ValueKind { V_INT, V_FLOAT, V_STRING };
struct Value {
enum ValueKind kind; // "which is active"union {
int i;
float f;
char s[32];
} data;
};
voidprint_value(struct Value v) {
switch (v.kind) {
case V_INT: printf("int: %d\n", v.data.i); break;
case V_FLOAT: printf("float: %f\n", v.data.f); break;
case V_STRING: printf("str: %s\n", v.data.s); break;
}
}
intmain(void) {
struct Value a = {V_INT, .data.i = 42};
struct Value b = {V_FLOAT, .data.f = 3.14f};
struct Value c;
c.kind = V_STRING;
snprintf(c.data.s, sizeof(c.data.s), "hello");
print_value(a); // int: 42print_value(b); // float: 3.14print_value(c); // str: hello
}
Common in JSON parsers, ASTs, and protocol messages. Wrap reads and writes behind functions so callers can't forget to check kind.
Size considerations
// sizeof(struct Value) = enum(4) + largest member(32) + padding// even a small int value pays the 32-byte string slot
Pitfalls & when to use
Gotchas
Only one member is valid at a time β writing another member wipes out the previous value.
A union without a tag is dangerous β there's no way to tell which member is active. Always use a tagged union.
Type punning: reading a member different from the one that was written is defined behavior for unions in C11 and later. Punning through pointer casts, however, can violate strict-aliasing rules.
float β int via a union gives you the bit pattern, not a numeric conversion.
When unions shine
Saving memory when you know only one field is ever active (common in embedded systems)
Variant types: JSON values, AST nodes, and protocol messages (paired with a tag)
Inspecting the bit representation: endianness checks and bit patterns
Hardware register maps: the same register seen both as bits and as a word
Modern alternatives: If you just need to save memory, a void* + size pair, C++'s std::variant, or Rust's enum is safer. In C, a tagged union with wrapper functions is the pragmatic choice.
Challenges
Challenge 1: float bit pattern
Use union { float f; uint32_t u; } to print the 32-bit hex representation of f = 1.0f (IEEE-754 gives 0x3F800000).
Challenge 2: is_little_endian()
Implement the function from the text on your own machine and verify the result. Most Mac, Linux, and Windows desktops are little-endian.
Challenge 3: Tiny value type
Build a tagged union struct Value that holds int, float, or bool. Implement Value add(Value a, Value b) so that numeric types add, two bools OR together, and mismatched kinds print an error.
Challenge 4: RGBA color
Create a union of a 32-bit integer and four 8-bit bytes (R, G, B, A). Pack and unpack 0xRRGGBBAA through both views.