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 union:int n = 1; char *p = (char*)&n; also works, but the union form is often considered friendlier to the compiler's strict-aliasing rules.
Tagged union (variant pattern)
When a value can hold one of several types, pair a "which type" tag (enum) with the data union inside a struct. This is C's version of a sum type / 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/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 with no tag is dangerous β there's no way to know which member is current. Use a tagged union.
Type punning: reading a member different from the one written is defined behavior for unions in C11+. Punning via pointer casts, however, can violate strict-aliasing rules.
float β int via union gives you the bit pattern, not a numeric cast.
When unions shine
Saving memory when you know only one field is ever active (embedded systems)
Inspecting representation: endianness, bit patterns
Hardware register maps: the same register as bits and as a word
Modern alternatives: if you just need memory savings, a void* + size pair or C++'s std::variant / 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 β 0x3F800000).
Challenge 2: is_little_endian()
Implement the function from the text on your machine and verify. Most Mac/Linux/Windows desktops are little-endian.
Challenge 3: Tiny value type
Build a tagged union struct Value with int, float, and bool. Implement Value add(Value a, Value b): numeric types add, two bools OR; mismatched kinds print an error.
Challenge 4: RGBA color
Union of a 32-bit integer and four 8-bit bytes (R,G,B,A). Pack/unpack 0xRRGGBBAA through both views.