What you'll learn in this post
What a union is, why it exists, how it shares memory, how to use it in C, how it compares to struct and enum, real-world examples, and common gotchas.
🤔 What Problem Does Union Solve?
Imagine you're writing firmware for a tiny microcontroller — a chip with
very limited RAM. You need a variable that can hold either a
temperature reading (a float), a sensor ID (an int),
or a status flag (a char) — but never all three at once.
With a regular struct, the compiler reserves memory for all three fields simultaneously, even if you're only ever using one at a time. On a microcontroller with 2 KB of RAM, that kind of waste adds up fast.
📖 What Exactly Is a Union?
A union is a user-defined data type in C that lets multiple members share the same block of memory. You declare it just like a struct, but the compiler only allocates enough space for the largest member. All other members fit inside that same space.
This means at any point in time, a union variable holds exactly one value — whichever member you last wrote to. Writing to a different member overwrites whatever was there before.
✏️ Syntax & Declaration
The syntax is almost identical to a struct — just swap the keyword:
union UnionName { type1 member1; type2 member2; type3 member3; };
Let's make a real example — a union that can hold an int, a float, or a char:
// Define the union union Data { int i; float f; char c; }; int main() { union Data d; d.i = 42; printf("int : %d\n", d.i); // ✅ 42 d.f = 3.14f; // ⚠️ overwrites d.i printf("float: %.2f\n", d.f); // ✅ 3.14 d.c = 'Z'; // ⚠️ overwrites d.f printf("char : %c\n", d.c); // ✅ Z printf("Size : %zu bytes\n", sizeof(d)); // 4 (size of float/int) return 0; }
Notice how you use the dot . operator to access members —
exactly like a struct. The difference is entirely in what happens under the hood
in memory.
🧠 How Union Memory Works (The Key Insight)
This is the single most important thing to understand about unions. Let's compare a struct and a union with the exact same three members:
Memory Layout Comparison
float
char
The struct needs 12 bytes (4 for int + 4 for float + 1 for char + 3 padding). The union needs just 4 bytes — the size of the largest member. That's a 3× reduction in memory for this simple case.
// Same three members — very different sizes struct SData { int i; float f; char c; }; union UData { int i; float f; char c; }; printf("struct size: %zu\n", sizeof(struct SData)); // 12 printf("union size: %zu\n", sizeof(union UData)); // 4
🛠️ Using typedef with Union
Just like with structs, typing union before every variable declaration
gets tedious. The typedef pattern cleans this up instantly:
// Without typedef — verbose every time union Sensor { int id; float temp; char status; }; union Sensor s1; // must write "union Sensor" // With typedef — much cleaner typedef union { int id; float temp; char status; } Sensor; Sensor s1; // clean and concise s1.temp = 36.6f;
Professional C codebases almost always use typedef union.
It makes your code look and feel like a proper custom type — which is exactly what it is.
🌍 Real-World Use Cases
Unions aren't just textbook theory — they appear in real production code, especially anywhere hardware or network protocols are involved.
Embedded / IoT
Store different sensor readings (temperature, pressure, voltage) in one compact variable.
Network Packets
Parse protocol headers where the same bytes mean different things depending on the packet type.
Hardware Registers
Access the same register as a whole integer or as individual bytes/bits.
Type Punning
Reinterpret the raw bytes of one type as another — e.g., float bits as an int for fast tricks.
Here's a real-world embedded example — a sensor reading union:
typedef enum { TEMP, PRESSURE, VOLTAGE } SensorType; typedef struct { SensorType type; // what kind of reading? union { // the reading itself (anonymous union) float temperature; float pressure; int voltage_mv; } value; } SensorReading; void printReading(SensorReading r) { switch (r.type) { case TEMP: printf("Temp: %.1f°C\n", r.value.temperature); break; case PRESSURE: printf("Pres: %.2f bar\n", r.value.pressure); break; case VOLTAGE: printf("Volt: %d mV\n", r.value.voltage_mv); break; } } int main() { SensorReading s; s.type = TEMP; s.value.temperature = 36.6f; printReading(s); // Temp: 36.6°C s.type = VOLTAGE; s.value.voltage_mv = 3300; printReading(s); // Volt: 3300 mV return 0; }
This pattern — a struct that holds a type tag alongside a union — is called a tagged union (or discriminated union). It's one of the most powerful and practical patterns in C.
⚔️ Union vs Struct vs Enum — Side by Side
C has three main user-defined data types and they're often confused, especially by beginners. Here's the clearest breakdown:
- Each member has its own memory
- All values stored simultaneously
- Size = sum of all members
- Best for grouping related data
- All members share one memory block
- Only one value valid at a time
- Size = size of largest member
- Best for memory-efficient alternatives
- Named integer constants
- No runtime data storage per se
- Size = size of int
- Best for named states / flags
Here's a code-level side-by-side comparison of all three:
/* ── ENUM: named constants, no data ── */ enum Color { RED, GREEN, BLUE }; /* ── STRUCT: all members stored, 12 bytes ── */ struct Pixel_S { int x; // 4 bytes int y; // 4 bytes char label; // 1 byte (+ 3 padding) }; // total: 12 bytes /* ── UNION: only one member valid, 4 bytes ── */ union Pixel_U { int x; // 4 bytes int y; // 4 bytes (same slot as x!) char label; // 1 byte (same slot!) }; // total: 4 bytes int main() { printf("%zu\n", sizeof(struct Pixel_S)); // 12 printf("%zu\n", sizeof(union Pixel_U)); // 4 return 0; }
| Feature | union | struct |
|---|---|---|
| Memory allocation | Shared — all members overlap | Separate — each member owns space |
| Size | = Size of largest member | = Sum of all members (+ padding) |
| Active members | Only 1 at a time | All, simultaneously |
| Use when… | You need one of several types | You need all fields together |
| Memory efficiency | ✅ Excellent | ⚠️ Higher usage |
| Safety | ⚠️ Easy to misread members | ✅ All members always valid |
🐛 Common Mistakes & Gotchas
1. Reading a member you didn't write last
This is the #1 union mistake. After you write to d.f,
reading d.i gives you the raw bytes of that float
reinterpreted as an int — which is almost certainly not what you want.
union Data { int i; float f; }; union Data d; d.f = 3.14f; printf("%d\n", d.i); // ❌ garbage! You wrote f, not i printf("%.2f\n", d.f); // ✅ correct — last written member
2. Forgetting to track the active member
A union by itself has no idea which member is currently valid. Always pair your union with a type tag (an enum or int) that explicitly records what's currently stored. This is the tagged union pattern shown earlier.
3. Initializing only the first member
In C, you can only initialize the first member of a union in a brace-initializer. Trying to initialize a later member with a designated initializer may not behave as expected in older compilers.
union Data { int i; float f; }; union Data d1 = { 10 }; // ✅ initializes d1.i = 10 union Data d2 = { .f = 3.14f }; // ✅ C99 designated initializer
📌 Quick Summary
- A union is a user-defined type where all members share the same memory location.
- Its size equals the size of its largest member — not the sum.
- Only one member is valid at a time — writing to one overwrites the others.
- Use unions when you need to hold one of several types and memory is tight.
- Always combine a union with a type tag (enum) to know which member is active — this is the tagged union pattern.
- Access members with
.(dot) for variables and->for pointers, just like struct. - Use
typedef unionto avoid repeating theunionkeyword on every declaration. - Unions shine in embedded systems, network protocol parsing, and hardware register access.