typedef struct {
int i;
float f;
char c;
}
StructA;
typedef union {
int i;
float f;
char c;
}
UnionA;Structure vs Union in C: Memory Layout and Behavioral Differences
What is Structure vs Union in C? Core Concept & Direct Summary
What are the fundamental differences in memory allocation, size, member access, and use cases between `struct` and `union`?
The textbook answer is short: a struct gives every member its own memory; a union makes all members share the same memory. A struct says "this and that"; a union says "this or that."
That one sentence is correct — and it's also where most explanations stop, which is a shame, because the interesting part is what the difference costs and buys you in bytes. The same members can produce a 12-byte struct and a 4-byte union. A union that looks like an obvious memory saving can save nothing at all once alignment padding is accounted for. And a struct's size can change by 50% just from reordering the members you already have.
This page works through the layout arithmetic with concrete, runnable numbers, then covers the behavioral differences that follow from it — and ends with a decision framework for choosing between them.
(Suggested image: the hero graphic — the same three members drawn twice: stacked sequentially as a struct, overlapping at one address as a union, with the two sizes labeled)
How Does Structure vs Union in C Work? In-Depth Mechanics & Architecture
Same Members, Two Very Different Sizes
Start with the cleanest possible comparison — three small members, declared identically except for the keyword:
The struct is 12 bytes; the union is 4. Three times the size, for the identical member list. Here's why, byte by byte (offsets verified with offsetof):
StructA — 12 bytes, members laid out in sequence:
UnionA — 4 bytes, all three members at offset 0:
Two things to notice:
- The struct's members don't overlap —
iowns bytes 0–3,fowns 4–7,cowns byte 8. Three values can be live simultaneously. - The union's members do overlap — all three start at byte 0 and reuse the same storage. Only one can be meaningful at a time. And the union is sized to its largest member (4 bytes), not the sum.
The struct's 3 bytes of trailing padding are the first appearance of the second theme on this page: struct sizes are rarely the sum of their parts.
(Suggested image: the two byte maps side by side, with the struct's padding bytes shaded and the union's overlap marked with braces)
The Layout Rules, Stated Precisely
Both layouts follow deterministic rules. Knowing them means you can compute any size yourself.
For a structure
- Walk the members in declaration order.
- Each member goes at the next offset that is a multiple of its own alignment requirement. If the current offset isn't a multiple, insert padding bytes to reach the next one.
- After the last member, pad the total up to a multiple of the struct's alignment — which is the largest alignment among all members.
For a union
- Every member starts at offset 0.
- The size is the largest member's size, padded up to a multiple of the largest alignment among the members.
Alignment requirements in practice (typical 32/64-bit targets):
So a struct's size is at least the sum of its members (plus padding), while a union's size is at least the largest member (plus padding).
(Suggested image: the rule as a flowchart — for each member, "offset a multiple of alignment? if no → insert padding, then place member")
Where Padding Gets Dramatic
The first example only wasted 3 bytes. Move a char next to a double and the waste becomes structural:
sizeof(StructB) = 24, sizeof(UnionB) = 8.
Walking StructB with the rules — and note the offsets, which are the interesting part:
Eleven bytes of padding — 7 internal, 4 trailing — to hold 13 bytes of actual data, in a 24-byte box. Only 1 + 8 + 4 = 13 bytes carry information.
The union needs none of that — 8 bytes, all members at offset 0, sized to the double.
Why padding exists at all: CPUs read memory most efficiently when values sit on addresses that are multiples of their size. A double at offset 8 can typically be fetched in one operation; a double at offset 1 would straddle cache-line and register boundaries. The compiler trades memory for speed, and alignment is the standard technique. (You can force tight packing with #pragma pack or __attribute__((packed)) — at the cost of slower access and undefined behavior on some platforms. The dedicated padding tutorial covers that trade.)
(Suggested image: the byte maps of StructB and UnionB side by side, with the 11 padding bytes shaded and the 13 data bytes highlighted)
The One You Can Fix: Member Order in Structs
Padding is often avoidable — by reordering the very same members:
Identical data, identical member types, 8 bytes recovered — by sorting the members from largest alignment to smallest.
That's the practical rule worth memorizing:
Declare struct members from largest to smallest. Same information, less padding.
Over a million records, that 8-byte difference is 8 MB.
A union doesn't have this problem at all — members are all at offset 0, so their order is irrelevant to size (though it does affect which member a positional initializer fills). Reordering a union changes nothing about its layout; reordering a struct can change everything.
(Suggested image: the two struct byte maps stacked, with the "unordered" one visibly longer and the reordered version compacted, "8 bytes saved" annotated)
Arrays Inside: The Size Gap Compounds
Add a typical fixed-size field — a name buffer — and the gap between the two forms widens in a specific way:
StructC:idat 0–3,name[20]at 4–23 → 24 bytes used, no padding needed (24 is a 4-multiple).UnionC: largest member ischar[20]→ 20 bytes, aligned to 4 → 20.
Now scale it up:
And for Pair A, where the members were tiny, the difference is dramatic:
Three times smaller — but only if you genuinely never need two members at once. For StructA, the union version means you can't store an int and a float in the same record, which is usually exactly what you wanted to do. Which brings us to the honest question:
Does a Union Actually Save Memory? Often, No
This is the section most comparisons skip, and it's the one that changes how you choose.
Unions don't drop padding — they inherit alignment requirements from their members. So the bytes a union saves can be exactly the bytes the surrounding struct needed for padding anyway.
Case 1: the union saves nothing.
Backing two fields with a union removed the 4-byte a... and then the union's 8-byte alignment reintroduced 4 bytes of padding in precisely the same place. Net saving: zero. You'd have paid all the risk of union aliasing for no memory benefit whatsoever.
Case 2: the union saves real bytes.
Here both alternatives are 4 bytes with 4-byte alignment, so the union slots in with no padding — and the record drops from 12 bytes to 8. Over 100,000 records that's 400,000 bytes.
The lesson: a union saves memory only when its alignment doesn't reintroduce padding. Small, same-sized alternatives save cleanly; large alternatives (especially double) frequently save nothing.
And the sanity check that overrides everything: if you cut a field and the union saves nothing, you've made the code harder to read for no gain — use a struct.
(Suggested image: the two paired byte maps — "saves nothing" showing padding reappearing where the union sits, "saves 4 bytes" showing a clean fit)
The Behavioral Differences
Layout is half the story. The rest is what you can do with each.
Demonstration 1: all-at-once vs one-at-a-time
Output:
That 1080033280 isn't random — it's the bit pattern 0x40600000, which is how IEEE-754 single precision encodes 3.5, read as an integer. Writing the float replaced the integer's bytes; reading the integer interprets those same bytes. One value, two interpretations.
Worth noting: because float and int are the same width, that particular number would be identical on a big-endian machine too — punning between same-sized types is byte-order-independent. Byte order only changes the answer when the types differ in size (as with a char reading the first byte of an int).
Demonstration 2: the addresses
The struct's member addresses are 4 bytes apart (each its own slot). The union's are all the same address — the single shared slot. That's the difference, demonstrated rather than described.
Notice also that taking the address of a union member is legal (it always points at offset 0). What you can't do is ask the union which member is active — it doesn't know.
(Suggested image: two address-listing panels, the struct's three distinct addresses color-coded separately and the union's three identical addresses all in one color)
Choosing Between Them: A Decision Framework
Work down this list:
1. Do you need more than one member at the same time? → Struct. If two fields must be live together, a union is simply wrong. This question settles most cases.
2. Is the value exactly one of several alternatives, with only one meaningful at a time? → Union — plus a tag.
The union stores the alternatives; the tag records which. Never use a bare union for variable data — the tag is what makes it correct:
3. Is this raw bytes that need two views — a register, a packet header? → Union, usually a union of a raw integer and a struct of bit fields. Excellent on a fixed platform; not portable for wire formats.
4. Are you reaching for a union purely to save memory? → Check the arithmetic first. As shown above, alignment padding often erases the saving entirely. Compute both sizeof values before committing.
5. Unsure? → Struct. It's the safer default: no aliasing hazards, no tag to maintain, no risk of reading the "wrong" member. Reach for a union when you have a specific reason, not by default.
(Suggested image: the framework as a decision flowchart with the five questions and their struct/union outcomes)
Combining Them: Both Directions Are Valid
The two compose freely, and the combinations are where most real designs live.
Struct containing a union — the tagged-variant pattern (shown above). This is by far the most common pairing.
Union containing structs — two or more structured views of the same bytes:
Three ways to look at 8 bytes: as a 64-bit quantity, as two 32-bit halves, or as 8 individual bytes. That's exactly what protocol parsers, checksum routines, and register access need.
Union containing a bit-field struct plus a raw integer — the classic register pattern:
One register, addressable by name or as a whole word.
(Suggested image: the three-way union — one 8-byte range labeled simultaneously as whole / halves / bytes)
File and Network I/O: A Critical Difference
Both types share one hazard, and it's worth stating plainly because it silently breaks portable code:
fwrite on a struct writes the padding too. Padding bytes contain whatever happened to be there — uninitialized memory, leftover values, address fragments. So:
The file is larger than needed, and byte-for-byte comparisons between two logically identical records can fail — because their padding differs. Worse, memcmp on two logically equal structs may return "not equal" for the same reason.
And struct layouts differ between compilers and platforms — same source, different member offsets, different padding. So dumping a struct isn't a portable file or wire format.
With unions the problem changes shape: writing the union writes only the active value's bytes (plus padding), but the tag isn't in the union — so a file containing only the union loses the information about what the bytes mean.
The fix in both cases is the same — explicit serialization:
Each field, in a defined order, at a defined width. No padding, no layout assumptions, works everywhere. The cost is a little code; the benefit is a format that doesn't silently change when you reorder members or switch compilers.
(Suggested image: the file-write comparison — raw fwrite dumping padding bytes vs serialized field-by-field output)
📋 Complete Program: Measuring Both
A full program that prints the sizes, offsets, and behavioral differences for every pair on this page.
Sample Run (all sizeof/offset values verified against the standard layout algorithm)
(Address values vary per run; sizeof/offsetof values are typical for common 32/64-bit ABIs — see the note below.)
The four headline results:
- 12 vs 4 (Pair A) — the struct stores three values; the union stores one, so it's sized to the largest member.
- 24 vs 8 (Pair B) — the struct pays 11 bytes of padding (7 internal + 4 trailing) to hold 13 bytes of data. The union pays none.
- 24 vs 16 (member order) — the same members reordered from largest-alignment-first to smallest recover 8 bytes. Same data, same behavior, less memory.
- 16 vs 16 (union saving) — the union removed a field and saved nothing, because the union's 8-byte alignment padding landed exactly where the removed field had been. Then 12 vs 8 shows a case where it saves 4 bytes per record.
(Suggested image: the results table rendered as a bar chart — struct vs union sizes for each pair, with padding bytes shown in a contrasting shade)
Common Mistakes
- Using a union when you need all the members — the fundamental error. Check question 1 of the framework first.
- Using a bare union for variable data with no tag — you'll read the wrong member eventually. Always pair a union with a tag field.
- Assuming
sizeof(struct)equals the sum of members — padding. Pair B is 24, not 13. - Assuming
sizeof(union)equals the sum of members — it's the largest member. Pair A's union is 4, not 9. - Assuming a union always saves memory — alignment padding frequently cancels the saving entirely (the 16 vs 16 case).
- Ignoring member order in structs — largest-to-smallest can shrink a struct by 50%.
- Reading a union member other than the last written — implementation-defined reinterpretation, possible trap representation. Switch on the tag instead.
- Comparing structs or unions with
==— not allowed in C. Compare member by member (andstrcmpfor strings). memcmpon structs — padding bytes may differ even when every field matches, giving false "not equal". Serialize or compare fields explicitly.fwrite-ing structs to files or sockets — writes garbage padding and isn't portable between compilers. Serialize field by field.fwrite-ing a union without its tag — the recipient can't tell what the bytes mean.- Taking the address of a member and passing it across a union write — the address stays valid, but the value at it changes if another member is written. Same class of bug as a dangling-but-not-freed pointer.
- Expecting
offsetofto be 0 for all struct members — only unions have that property. Struct offsets differ by design. - Reaching for
#pragma packto "fix" sizes — it removes padding but can cause slow or faulting unaligned access, and some platforms trap. Use it only with a specific, understood requirement.
(Suggested image: "14 struct-vs-union mistakes" checklist — with #5, #9, and #10 highlighted as the silent-wrong-answer cases)
Final Thoughts
The difference between a structure and a union comes down to one design decision with two consequences: members get their own storage (struct) or share one (union).
From that single fact everything else follows. Members of a struct are all simultaneously valid, live at different addresses, and make the type as large as all of them together — plus padding to satisfy alignment. Members of a union are mutually exclusive, all live at offset 0, and make the type as large as the biggest one — also plus padding. Reorder a struct's members and the size changes; reorder a union's and nothing does.
Two habits make the choice reliable:
- Ask "and" or "or" first. Need both fields at once → struct. Have one-of-several alternatives, tracked by a tag → union.
- Do the arithmetic before believing a union saves memory. Compute both
sizeofvalues; alignment padding frequently cancels the gain, and a union that saves nothing has bought you a real aliasing hazard for free.
And in both cases, remember that a sizeof is an implementation detail you query, never one you calculate by hand — and that neither type is safe to dump to a file without explicit serialization. Master the layout rules and both types stop being magic and become arithmetic you control.
Next in this cluster: structure padding and memory layout in depth, typedef, enum, or bit fields — all of which build directly on the alignment rules established here.
Runnable C Code Example & Practical Implementation
#include <stdio.h>
struct PointStruct {
int x;
// 4 bytes
int y;
// 4 bytes
};
// Total = 8 bytes
union PointUnion {
int x;
// 4 bytes
int y;
// 4 bytes (shared)
};
// Total = 4 bytes
int main() {
printf("Size of Struct: %zu bytes (Both x and y coexist)\
", sizeof(struct PointStruct));
printf("Size of Union: %zu bytes (x and y share memory)\
", sizeof(union PointUnion));
return 0;
}Key Takeaways, Best Practices & Memory Safety Rules
Frequently Asked Questions & Technical Interview Insights
Technical Interview & Viva Questions
Frequently asked C programming interview questionsQ: When should you choose a `union` over a `struct`?
Q: Can a struct contain a union, and can a union contain a struct?
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.