A union in C stores multiple members at the same memory address. Only one member holds a valid value at a time. The size of the union equals the size of its largest member. This sounds simple, but the subtleties around reading from a union member you didn’t write to are where most misuse happens.
Basic Syntax and Layout
#include <stdio.h>
union Value {
int i;
float f;
double d;
};
int main() {
union Value v;
v.i = 42;
printf("i = %dn", v.i); /* 42 */
printf("f = %fn", v.f); /* garbage — wrote i, reading f */
printf("d = %fn", v.d); /* garbage — different size */
printf("sizeof(Value) = %zun", sizeof(union Value)); /* 8 — size of double */
return 0;
}All members share the same starting address. Writing to v.i and then reading from v.f gives you the same bytes interpreted as a float. The result is whatever the 4-byte integer representation of 42 looks like when read as a float — not 42.0.
Tagged Unions — The Right Pattern
#include <stdio.h>
#include <string.h>
typedef enum { TYPE_INT, TYPE_FLOAT, TYPE_STRING } ValueType;
typedef struct {
ValueType type;
union {
int i;
float f;
char s[32];
} data;
} Value;
void print_value(const Value *v) {
switch (v->type) {
case TYPE_INT: printf("int: %dn", v->data.i); break;
case TYPE_FLOAT: printf("float: %fn", v->data.f); break;
case TYPE_STRING: printf("string: %sn", v->data.s); break;
}
}
int main() {
Value a = { .type = TYPE_INT, .data.i = 42 };
Value b = { .type = TYPE_FLOAT, .data.f = 3.14f };
Value c = { .type = TYPE_STRING };
strncpy(c.data.s, "hello", sizeof(c.data.s) - 1);
c.type = TYPE_STRING;
print_value(&a);
print_value(&b);
print_value(&c);
return 0;
}A tagged union (also called a discriminated union) pairs a union with an enum tag that tracks which member is currently valid. This is the correct way to implement a variant type in C — the foundation of JSON parsers, AST nodes, and configuration systems.
Type Punning — The Common (and Risky) Use Case
#include <stdio.h>
#include <stdint.h>
union FloatBits {
float f;
uint32_t bits;
};
int main() {
union FloatBits fb;
fb.f = 3.14f;
printf("3.14f in hex: 0x%08Xn", fb.bits); /* 0x4048F5C3 */
return 0;
}Reading a union member other than the one last written is undefined behavior in C++ but explicitly allowed in C (C99 and later, via TC3 and C11 footnote 95). In C, you can write to one union member and read from another — the result is the raw bytes of the written value interpreted as the other type.
This is safer than pointer casting for type punning:
/* WRONG in both C and C++ (strict aliasing violation) */
float f = 3.14f;
uint32_t bits = *(uint32_t*)&f;
/* RIGHT in C (defined behavior since C99 TC3) */
union { float f; uint32_t bits; } u = { .f = 3.14f };
uint32_t bits = u.bits;
/* Also RIGHT and portable to C++ */
float f = 3.14f;
uint32_t bits;
memcpy(&bits, &f, sizeof(bits));Union in Network Protocol Parsing
#include <stdio.h>
#include <stdint.h>
union IPAddress {
uint32_t raw;
uint8_t octet[4];
};
int main() {
union IPAddress ip;
ip.raw = 0xC0A80101; /* 192.168.1.1 in big-endian */
/* On little-endian (x86): octets are reversed */
printf("%u.%u.%u.%un",
ip.octet[3], ip.octet[2], ip.octet[1], ip.octet[0]);
return 0;
}This pattern is common in embedded systems and network code. Note the endianness: on a little-endian system, the bytes are stored low-byte first, so octet[0] is the least significant byte. Use ntohl() for portable network byte order conversion.
Union Size and Alignment
#include <stdio.h>
union Mix {
char c; /* 1 byte */
short s; /* 2 bytes */
int i; /* 4 bytes */
double d; /* 8 bytes */
};
int main() {
printf("sizeof(Mix) = %zun", sizeof(union Mix)); /* 8 */
printf("alignof(Mix) = %zun", _Alignof(union Mix)); /* 8 */
return 0;
}A union is as large as its largest member and aligned to the alignment requirement of its most strictly aligned member. This ensures any member can be stored correctly regardless of which one you write to. Understanding how bitwise operators interact with the raw bytes stored in a union helps when writing low-level parsing code.
TL;DR
- All union members share the same memory — the size equals the largest member
- Only one member holds valid data at a time — track which one with an enum tag
- Tagged unions (struct + enum + union) are the correct pattern for variant types
- Reading a different member than you wrote to is defined behavior in C (type punning) but not in C++
- For portable type punning across C and C++, use
memcpy - Unions are useful for protocol parsing, AST nodes, JSON values, and memory-efficient variant types
- Check the active struct member before reading from a tagged union — reading the wrong variant is a logic bug, not a crash