union Data { int i; /* ┐ */ float f; /* ├─ all three start at the same address */ char str[20]; /* ┘ */};Unions in C: Shared Memory Members and Practical Examples
What is Unions in C? Core Concept & Direct Summary
What is a `union` in C, how do all union members share the exact same memory location, and how does union sizing work?
A structure gives every member its own storage. Add five members and you get five separate places in memory, all alive at once.
A union does the opposite. Every member starts at the same address and shares the same bytes. It holds exactly one value at a time — whichever member you last wrote. The union is as large as its largest member, not the sum of them all.
That property sounds like a limitation, and it is — but it's the limitation that makes unions the right tool for a specific and genuinely common job: representing data that could be one of several types, where you only ever need one of them at a time. Configuration values, protocol packets, hardware registers, and variant records all have that shape.
This page covers how unions share memory, the rules that govern reading them (including the ones that bite), and four practical patterns where unions are the correct answer rather than a clever trick.
(Suggested image: the hero graphic — a struct's five separate boxes beside a union's one box with three labels pointing at it and "same address" marked)
How Does Unions in C Work? In-Depth Mechanics & Architecture
Declaration and the Core Idea
Output:
(Address values vary per run — that's your machine's stack layout.)
All four addresses are identical. That's the entire definition of a union in one observation. The members aren't laid out one after another; they're all anchored to the same starting byte and simply interpret what's there differently.
And sizeof is 20 — the size of str[20], the largest member. Not 4 + 4 + 20 = 28.
(Suggested image: the union drawn as a single byte-range with i, f, and str all overlapping it, against the struct drawn as three sequential non-overlapping ranges)
Unions vs Structs: The Comparison That Matters
The one-sentence distinction:a struct says "and" (this and that); a union says "or" (this or that).
That's the mental test to apply when choosing. Do you need the width and the height at the same time? Struct. Is this value either an int or a float, never both? Union.
(Suggested image: the side-by-side memory map — struct showing two adjacent boxes, union showing one box with two labels — with "and" vs "or" annotated)
Writing One Member Overwrites the Others
This is the behavior that surprises people, so let's make it concrete:
Output:
Assigning d.f = 3.14f wrote the 4 bytes of the float 3.14on top of the integer. The integer is gone. Reading d.i now reinterprets those same 4 bytes as an int — which yields a large, meaningless number. That particular value (1085069459 = 0x40A3D70A-ish reinterpreted) isn't a bug in your code; it's simply the float's bit pattern read as an integer.
The rule:a union holds one value. The last member you wrote is the only one that means anything.
(Suggested image: two frames of one byte-range — first showing int 10 written, then showing float 3.14 overwriting those bytes, with the int label now crossed out)
Reading a Member You Didn't Write
Here's the precise rule, because the details matter.
When you read a union member other than the last one stored, the standard says the bytes are reinterpreted as the new type — a process it calls type punning — and adds a caveat: the result might be a trap representation (a bit pattern the type isn't allowed to hold), which is undefined behavior.
So:
- Reading a different member is defined by your compiler's behavior, not guaranteed portably.
- GCC and Clang explicitly document that type punning through unions works as expected.
- The result depends on byte order (endianness) and the sizeof relationship between the members.
Where it produces a meaningful answer — a small integer read as a character on a little-endian machine:
On a little-endian machine (x86, ARM in its usual mode), the low byte of 65 is 65, so d.str[0] is 'A'. On a big-endian machine, the first byte of the four-byte int 65 is 0, so you'd get an empty string instead. Same code, different answer, no warning.
Practical guidance:
- For records you tagged yourself (the pattern below), punning never happens — you always read the member that matches the tag. That's the safe, idiomatic use.
- For deliberate byte inspection, prefer
memcpy— it's fully defined in standard C and immune to aliasing surprises:
(Suggested image: the same 4 bytes shown twice — read as an int and read as a float — with an endianness note marking which byte each member touches)
Initialization Rules
Unions have a few specific rules worth knowing.
Only the first member can be brace-initialized positionally:
Anything else needs a designated initializer (C99):
That last error is the compiler enforcing the whole point: a union holds one value. Initializing two members would be contradictory.
Zeroing everything works as usual:
Assignment is a raw copy:
Unlike arrays, unions are assignable, so d2 = d1 copies the union wholesale.
But you can't compare them:
Compare the active member explicitly instead.
(Suggested image: the union with the "only one member may be initialized" rule illustrated — two designated initializers with one crossed out)
The Main Use: Tagged Unions (Variant Types)
This is why unions exist in application code: a record that can hold one of several different types, with a separate field recording which one.
The union stores the alternatives; a tag (usually an enum) records the truth.
Every operation then follows one discipline: switch on the tag, touch the matching member.
Why the tag is mandatory: nothing inside the union tells you what it contains. s->data is just bytes; you have to know whether to read it as a circle, a rectangle, or a triangle. The type field is that knowledge, stored alongside the data.
And the payoff — one array holding different kinds of records:
Four elements of one array type, holding circles, rectangles, and triangles. Without a union you'd need four separate arrays (and lose the ordering); with void * you'd lose all type safety. The tagged union gives you both.
(Suggested image: one array of 4 Shape records where each record shows the same tag slot but different payload layouts — a circle payload, rectangle payload, triangle payload)
📋 Complete Program: A Mixed Shape List
A verified, complete implementation of the tagged union.
Sample Run (numeric output verified)
Reading the design:
ShapeDatais 16 bytes — the size ofRectangleandTriangle(two doubles each).Circleis only 8 bytes, but all three share the same 16, so that's the union's size.Shapeis 24 bytes — 4 for theenumtag, 4 bytes of alignment padding so the union can start on an 8-byte boundary, then 16 for the union. The tag costs 8 bytes, which is the price of knowing what's inside.- The
switchinshapeAreais the dispatch. Every alternative is handled, and thedefaultcase guards against an uninitialized tag. This pattern — switch on tag, read the matching member — is the only correct way to read a tagged union. - The maker functions set the tag and one alternative. They deliberately leave the other alternatives untouched, because those bytes don't belong to this shape.
- One array, mixed types, one loop.
for (int i = 0; i < 4; i++) { total += shapeArea(&shapes[i]); }— the loop doesn't know or care which kind each record is. That polymorphism is what the tag buys you.
(Suggested image: the four Shape records with their tags highlighted, showing that the payload area holds a circle payload in rows 0 and 3 and different layouts in rows 1 and 2)
Use 2: Hardware Registers and Protocol Fields
Unions combine naturally with bit fields — a raw access view and a named-field view of the same register:
Why this is the right shape: a hardware register is one 16-bit word that a datasheet describes field by field. The union lets you write reg.bits.enable = 1 (readable) and also reg.raw = incoming_word (one instruction, whole register) — the same memory, two views.
⚠️ Bit-field layout is implementation-defined (bit ordering within the storage unit, padding). So this pattern is excellent for on one fixed platform — embedded targets, device drivers — and unsuitable for wire formats read by another compiler. For cross-platform formats, use explicit shifts and masks.
(Suggested image: one 16-bit register drawn twice — as a raw hex value and as four labeled bit fields — with an arrow labeled "same memory")
Use 3: Detecting Byte Order (Endianness)
A clean, portable-ish use of byte-overlapping:
Output on a typical x86-64 machine:
The uint32_t and the uint8_t[4] occupy the same 4 bytes. Writing the word and reading the bytes reveals the machine's byte order — useful when you're serializing data for a network (which is big-endian, by convention) and need to know whether to swap.
(Suggested image: the 4-byte range with the word label showing 0x01020304 and the byte labels showing 04 03 02 01, illustrating little-endian storage)
Use 4: Memory Saving in Large Arrays
When every record has a field that's sometimes a small number and sometimes a big one, a union saves real memory across a big table.
Each Measurement costs 4 (id) + 4 (isLarge) + 8 (union) = 16 bytes, instead of 4 + 4 + 4 + 8 = 20 bytes for two separate fields. Over a million records, that's ~4 MB saved.
The honest caveat: a union only saves memory when the alternatives would otherwise sit unused. If you find yourself needing two members at once, you've picked the wrong tool — that's a struct. And a few bytes per record usually isn't worth added complexity unless the table is genuinely large or the memory genuinely tight (embedded systems, huge datasets, fixed-size buffers).
Anonymous Unions (C11)
C11 lets you declare a union inside a struct without a name, which promotes its members to the struct's own namespace:
Now the members are reachable directly:
Less typing, flatter paths — at the cost of clarity about which fields belong together. Most codebases keep the named data member for exactly that reason: s.data.circle.radius spells out the hierarchy, and the compiler-error messages are clearer. Use whichever your team finds readable. (GCC and Clang accept this in C99 mode as an extension; MSVC supports it too — but for strict portability, specify C11: -std=c11.)
When to Use a Union — and When Not To
Good fits:
Poor fits:
That fourth row is worth internalizing from the verified example above: Shape is 24 bytes, while a plain struct of all three alternatives would be 4 + 8 + 16 + 16 = 44 bytes. So the union does win — combining the tag with shared storage still saves 20 bytes per record. But it's a design calculation, not an automatic win.
(Suggested image: the memory budget comparison — plain struct with all fields (44 bytes) vs tag + union (24 bytes), with the saved bytes highlighted)
Common Mistakes
- Writing one member and reading another — the #1 union bug. The bytes get reinterpreted, giving meaningless values. Always pair reads with the tag.
- Treating a union like a struct — expecting all members to hold values simultaneously. A union holds one value.
- Skipping the tag — using a bare union and "remembering" which member is active. It works until the code grows, then fails silently. Always add a tag.
- Assuming
sizeof(union)is the sum of members — it's the largest member, plus alignment padding. - Forgetting that the union is sized by the largest member — a
char[20]member makes every union 20+ bytes even if you only ever use theint. - Initializing two members —
{ .i = 1, .f = 2.0 }is a compile error. Only one may be initialized. - Positional initialization of a non-first member —
{ 3.14 }initializes the first member, whatever its type. Use designated initializers. - Comparing unions with
==— not allowed. Compare the tag, then the relevant member. - Relying on punning portably — endianness and trap representations make it non-portable. Use
memcpyfor defined byte copying. - Writing unions to files or sockets directly — padding, alignment, and bit order vary between compilers and platforms. Serialize field by field.
- Forgetting to zero a union before use — then the tag or bytes may be garbage. Start with
= {0}. - A union member of its own type — not allowed (incomplete type). A pointer to its own type is fine — that's how self-referential structures work.
- Using anonymous unions without C11 — works as an extension in GCC/Clang, but compile with
-std=c11if you want it guaranteed. - Reading the union's "other" member to test the type — there's no way to tell from the bytes. That's what the tag is for.
(Suggested image: "14 Union Mistakes" checklist — with #1, #3, and #10 highlighted as the three that produce silent wrong answers)
Final Thoughts
A union is C's "one of these" type. Where a struct allocates space for everything, a union allocates space for the largest alternative and lets the members share it — which means it holds exactly one value at a time, and the last member written is the only one that means anything.
That constraint defines both the danger and the value. The danger is reading a member you didn't write: the bytes get reinterpreted, and you get plausible-looking nonsense with no warning. The value is that alternatives become expressible — a record that's a circle or a rectangle or a triangle, a register that's a raw word or four named fields, a value that's an int or a double or a string.
The discipline that makes unions safe fits in one line: whenever you use a union for data that can vary, pair it with a tag and switch on that tag before you touch a member. Add a = {0} before you start, keep sizeof out of your mental arithmetic (the compiler knows), and reach for memcpy instead of punning when portability matters. Do that, and unions stop being a risky feature and become a precise, well-fitting tool.
Next in this cluster: typedef and type naming, enum in depth, or structure padding and layout — where the alignment rules that shaped this page's sizes get their full treatment.
Runnable C Code Example & Practical Implementation
#include <stdio.h>
union DataValue {
int intVal;
float floatVal;
char charVal;
};
int main() {
union DataValue data;
data.intVal = 100;
printf("Stored intVal: %d\
", data.intVal);
data.floatVal = 3.1415f;
// Overwrites intVal in the same memory location
printf("Stored floatVal: %.4f\
", data.floatVal);
printf("Corrupted intVal:%d (Memory was overwritten)\
", data.intVal);
printf("Size of union: %zu bytes\
", sizeof(union DataValue));
return 0;
}Key Takeaways, Best Practices & Memory Safety Rules
enum type_tag to track which member is currently active.Frequently Asked Questions & Technical Interview Insights
Technical Interview & Viva Questions
Frequently asked C programming interview questionsQ: What is the primary purpose of a union in systems programming?
Q: How is `sizeof(union)` determined?
Related Tools, Tutorials & Authoritative References
Recommended Interactive Tools
- Online C Compiler & Runner — Interactive online GCC sandbox to execute C code in real time.
- Online C Code Editor & IDE — Multi-file C programming environment with code formatting.
- C++ Programming Tutorial — Transition from procedural C to Object-Oriented C++ with STL.
- JavaScript Programming Tutorial — Master modern JavaScript fundamentals, DOM APIs, and async patterns.
Authoritative Standards & Citations
- ISO/IEC 9899 (C Language Standard) — Official international C specification by ISO/IEC JTC 1/SC 22/WG 14.
- GNU GCC Documentation — Official reference manual for the GNU Compiler Collection and C libraries.
- The C Programming Language (K&R) — Definitive classic work by Brian Kernighan & Dennis Ritchie at Bell Labs.
- C Reference Documentation (cppreference.com) — Verified reference covering C89 through C23 language features.