Topic 86 of 100 Structures and Unions in C Interactive Tutorial

Structure vs Union in C: Memory Layout and Behavioral Differences

11 mins read Last updated: 22 Aug 2026 27,200 views
C Compiler 0.00%

What is Structure vs Union in C? Core Concept & Direct Summary

Core Objective & Problem Statement

What are the fundamental differences in memory allocation, size, member access, and use cases between `struct` and `union`?

Direct Answer Summary

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:

main.c
Run Online
typedef struct {
    int i;
    float f;
    char c;
}
StructA;
typedef union {
    int i;
    float f;
    char c;
}
UnionA;
main.c
Run Online
printf("sizeof(StructA) = %zu\n", sizeof(StructA));
/* 12 */printf("sizeof(UnionA)  = %zu\n", sizeof(UnionA));
/* 4  */

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:

Architecture Diagram
offset:  0   1   2   3 │ 4   5   6   7 │ 8 │ 9  10  11       ┌───────────────┬───────────────┬───┬───────────┐       │    int  i     │   float  f    │ c │  padding  │       │   bytes 0-3   │   bytes 4-7   │ 8 │  9,10,11  │       └───────────────┴───────────────┴───┴───────────┘                                            ▲                              3 bytes of padding: the struct                              must be a multiple of its                              largest alignment (4)

UnionA — 4 bytes, all three members at offset 0:

Architecture Diagram
offset:  0   1   2   3       ┌───────────────┐       │ int i         │   ← all three members start here       │ float f       │      and reuse these same 4 bytes       │ char c        │       └───────────────┘

Two things to notice:

  1. The struct's members don't overlap — i owns bytes 0–3, f owns 4–7, c owns byte 8. Three values can be live simultaneously.
  2. 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

  1. Walk the members in declaration order.
  2. 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.
  3. 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

  1. Every member starts at offset 0.
  2. 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):

TypeSizeAlignment
char11
short22
int, float44
double, pointers88

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:

main.c
Run Online
typedef struct {
    char c;
    double d;
    int i;
}
StructB;
typedef union {
    char c;
    double d;
    int i;
}
UnionB;

sizeof(StructB) = 24, sizeof(UnionB) = 8.

Walking StructB with the rules — and note the offsets, which are the interesting part:

MemberOffsetSizePadding beforeWhy
char c010starts at 0
double d887d needs an 8-multiple; the next after byte 0 is 8
int i164016 is already a 4-multiple
trailing20 → 24—4total must be a multiple of 8
Architecture Diagram
offset: 0 │ 1  2  3  4  5  6  7 │ 8  9 10 11 12 13 14 15 │16 17 18 19│20 21 22 23      char│      PADDING (7)     │       double d         │   int i   │  PADDING

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:

main.c
Run Online
typedef struct {
    char a;
    double b;
    char c;
}
StructUnordered;
/* 24 bytes */typedef struct {
    double b;
    char a;
    char c;
}
StructOrdered;
/* 16 bytes */
DeclarationOffsetsSize
{ char, double, char }a@0, pad 1–7, b@8, c@16, pad 17–2324
{ double, char, char }b@0, a@8, c@9, pad 10–1516

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:

main.c
Run Online
typedef struct {
    int id;
    char name[20];
}
StructC;
/* 24 bytes */typedef union {
    int id;
    char name[20];
}
UnionC;
/* 20 bytes */
  • StructC: id at 0–3, name[20] at 4–23 → 24 bytes used, no padding needed (24 is a 4-multiple).
  • UnionC: largest member is char[20] → 20 bytes, aligned to 4 → 20.

Now scale it up:

main.c
Run Online
printf("StructC[1000] = %zu bytes\n", 1000 * sizeof(StructC));
/* 24000 */printf("UnionC[1000]  = %zu bytes\n", 1000 * sizeof(UnionC));
/* 20000 */

And for Pair A, where the members were tiny, the difference is dramatic:

main.c
Run Online
printf("StructA[1000] = %zu bytes\n", 1000 * sizeof(StructA));
/* 12000 */printf("UnionA[1000]  = %zu bytes\n", 1000 * sizeof(UnionA));
/*  4000 */

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.

main.c
Run Online
typedef struct {
    int type;
    int a;
    double b;
}
BothDoubles;
/* 16 */typedef struct {
    int type;
    union {
        int a;
        double b;
    }
    v;
}
SwapDoubles;
/* 16 */
LayoutSize
BothDoublestype@0(4), a@4(4), b@8(8)16
SwapDoublestype@0(4), pad 4–7, v@8(8)16

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.

main.c
Run Online
typedef struct {
    int type;
    int a;
    int b;
}
BothInts;
/* 12 */typedef struct {
    int type;
    union {
        int a;
        int b;
    }
    v;
}
SwapInts;
/*  8 */

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.

Behaviorstructunion
Members valid simultaneouslyAll of themOne — the last written
Writing a memberLeaves others untouchedOverwrites the others
Reading a member you didn't writeN/A — it's validReinterprets the same bytes (implementation-defined; possible trap representation)
Address of each memberAll differentAll identical
sizeofSum of members + paddingLargest member + padding
Reordering members changes sizeYes — often by a lotNo
Copy assignment =Copies all membersCopies the same bytes (one value)
== comparisonNot allowedNot allowed
Passed to a function by valueCopies the whole structCopies the whole union
Taking a member's addressNormal, always validValid, but can't tell which is active
Needs a companion "which one?" fieldNoYes — a tag, in practice
Safe to write to a file/socketOnly with explicit serialization (padding!)Only with explicit serialization
Typical useGrouping data needed togetherRepresenting alternatives

Demonstration 1: all-at-once vs one-at-a-time

main.c
Run Online
StructA s = {
    0
}
;
s.i = 10;
s.f = 3.5f;
s.c = 'X';
printf("struct: i=%d f=%.1f c=%c\n", s.i, s.f, s.c);
/* 10  3.5  X  — all three live */
UnionA u;
u.i = 10;
printf("union after u.i = 10  : u.i = %d\n", u.i);
/* 10 */u.f = 3.5f;
printf("union after u.f = 3.5f: u.i = %d   <- overwritten\n", u.i);
printf("                         u.f = %.1f\n", u.f);

Output:

main.c
Run Online
struct: i=10 f=3.5 c=Xunion after u.i = 10 : u.i = 10union after u.f = 3.5f: u.i = 1080033280 <- overwritten u.f = 3.5

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

main.c
Run Online
printf("&s.i=%p &s.f=%p &s.c=%p\n",(void*)&s.i,(void*)&s.f,(void*)&s.c);
printf("&u.i=%p &u.f=%p &u.c=%p\n",(void*)&u.i,(void*)&u.f,(void*)&u.c);
main.c
Run Online
&s.i=0000009B0A3FF9A0 &s.f=0000009B0A3FF9A4 &s.c=0000009B0A3FF9A8 <- all differ&u.i=0000009B0A3FF9B0 &u.f=0000009B0A3FF9B0 &u.c=0000009B0A3FF9B0 <- identical

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.

main.c
Run Online
typedef struct {
    ValueType type;
    /* the tag: which alternative is real */ union {
        int intValue;
        double doubleValue;
        char stringValue[32];
    }
    value;
}
ConfigValue;

The union stores the alternatives; the tag records which. Never use a bare union for variable data — the tag is what makes it correct:

main.c
Run Online
switch (v->type) {
    case VAL_INT: printf("%d\n", v->value.intValue);
    break;
    case VAL_DOUBLE: printf("%.3f\n", v->value.doubleValue);
    break;
    case VAL_STRING: printf("%s\n", v->value.stringValue);
    break;
}

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:

main.c
Run Online
typedef union {
    struct {
        uint32_t low, high;
    }
    halves;
    /* two 32-bit views        */ uint64_t whole;
    /* one 64-bit view         */ uint8_t bytes[8];
    /* eight byte-level views  */
}
DoubleWord;

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:

main.c
Run Online
typedef union {
    uint16_t raw;
    struct {
        uint16_t mode : 3;
        uint16_t enable : 1;
        uint16_t channel : 4;
        uint16_t code : 8;
    }
    bits;
}
ControlRegister;

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:

main.c
Run Online
fwrite(&record, sizeof(record), 1, file);
/* ⚠ writes garbage padding bytes */

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:

main.c
Run Online
/* Write field by field, in a fixed order, with fixed widths */fwrite(&rec.id, sizeof rec.id, 1, file);
fwrite(rec.name, 1, sizeof rec.name, file);

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.

main.c
Run Online
#include <stdio.h>
#include <stddef.h>
/* ---------- Pair A: same members, small types ---------- */typedef struct {
    int i;
    float f;
    char c;
}
StructA;
typedef union {
    int i;
    float f;
    char c;
}
UnionA;
/* ---------- Pair B: mixed sizes (forces padding) ---------- */typedef struct {
    char c;
    double d;
    int i;
}
StructB;
typedef union {
    char c;
    double d;
    int i;
}
UnionB;
/* ---------- Pair C: with an array member ---------- */typedef struct {
    int id;
    char name[20];
}
StructC;
typedef union {
    int id;
    char name[20];
}
UnionC;
/* ---------- Member ORDER changes struct size ---------- */typedef struct {
    char a;
    double b;
    char c;
}
StructUnordered;
typedef struct {
    double b;
    char a;
    char c;
}
StructOrdered;
/* ---------- Union savings depend on alignment ---------- */typedef struct {
    int type;
    int a;
    double b;
}
BothDoubles;
typedef struct {
    int type;
    union {
        int a;
        double b;
    }
    v;
}
SwapDoubles;
typedef struct {
    int type;
    int a;
    int b;
}
BothInts;
typedef struct {
    int type;
    union {
        int a;
        int b;
    }
    v;
}
SwapInts;
#define OFF(T, m) printf("    offsetof(%s, %s) = %2zu\n", #T, #m, offsetof(T, m))
int main(void) {
    /* ================= PAIR A ================= */ puts("=== PAIR A: struct/union { int; float; char; } ===");
    printf("  sizeof(StructA) = %2zu    sizeof(UnionA) = %2zu\n", sizeof(StructA), sizeof(UnionA));
    OFF(StructA, i);
    OFF(StructA, f);
    OFF(StructA, c);
    StructA s = {
        0
    };
    UnionA u;
    s.i = 10;
    s.f = 3.5f;
    s.c = 'X';
    printf("  struct holds all three : i=%d f=%.1f c=%c\n", s.i, s.f, s.c);
    u.i = 10;
    printf("  union after u.i = 10   : u.i = %d\n", u.i);
    u.f = 3.5f;
    printf("  union after u.f = 3.5f : u.i = %d   <- OVERWRITTEN\n", u.i);
    printf("                           u.f = %.1f\n", u.f);
    printf("  &s.i=%p &s.f=%p &s.c=%p  <- all differ\n",(void *)&s.i,(void *)&s.f,(void *)&s.c);
    printf("  &u.i=%p &u.f=%p &u.c=%p  <- identical\n",(void *)&u.i,(void *)&u.f,(void *)&u.c);
    /* ================= PAIR B ================= */ puts("\n=== PAIR B: struct/union { char; double; int; } ===");
    printf("  sizeof(StructB) = %2zu    sizeof(UnionB) = %2zu\n", sizeof(StructB), sizeof(UnionB));
    OFF(StructB, c);
    OFF(StructB, d);
    OFF(StructB, i);
    /* ================= PAIR C ================= */ puts("\n=== PAIR C: struct/union { int; char[20]; } ===");
    printf("  sizeof(StructC) = %2zu    sizeof(UnionC) = %2zu\n", sizeof(StructC), sizeof(UnionC));
    OFF(StructC, id);
    OFF(StructC, name);
    /* ================= MEMBER ORDER ================= */ puts("\n=== MEMBER ORDER (structs only) ===");
    printf("  sizeof(StructUnordered) {char,double,char} = %2zu\n", sizeof(StructUnordered));
    printf("  sizeof(StructOrdered)   {double,char,char} = %2zu\n", sizeof(StructOrdered));
    /* ================= DOES A UNION SAVE MEMORY? ================= */ puts("\n=== DOES A UNION ACTUALLY SAVE MEMORY? ===");
    printf("  struct{int,int,double}         = %2zu\n", sizeof(BothDoubles));
    printf("  struct{int,union{int,double}}  = %2zu   <- NO saving (padding eats it)\n", sizeof(SwapDoubles));
    printf("  struct{int,int,int}            = %2zu\n", sizeof(BothInts));
    printf("  struct{int,union{int,int}}     = %2zu   <- 4 bytes saved per record\n", sizeof(SwapInts));
    /* ================= ARRAYS ================= */ puts("\n=== ARRAYS OF 1000 RECORDS ===");
    printf("  StructA[1000] = %6zu bytes\n", 1000 * sizeof(StructA));
    printf("  UnionA[1000]  = %6zu bytes\n", 1000 * sizeof(UnionA));
    return 0;
}

Sample Run (all sizeof/offset values verified against the standard layout algorithm)

main.c
Run Online
=== PAIR A: struct/union {
    int;
    float;
    char;
}
=== sizeof(StructA) = 12 sizeof(UnionA) = 4 offsetof(StructA, i) = 0 offsetof(StructA, f) = 4 offsetof(StructA, c) = 8 struct holds all three : i=10 f=3.5 c=X union after u.i = 10 : u.i = 10 union after u.f = 3.5f : u.i = 1080033280 <- OVERWRITTEN u.f = 3.5 &s.i=0000009B0A3FF9A0 &s.f=0000009B0A3FF9A4 &s.c=0000009B0A3FF9A8 <- all differ &u.i=0000009B0A3FF9B0 &u.f=0000009B0A3FF9B0 &u.c=0000009B0A3FF9B0 <- identical
=== PAIR B: struct/union {
    char;
    double;
    int;
}
=== sizeof(StructB) = 24 sizeof(UnionB) = 8 offsetof(StructB, c) = 0 offsetof(StructB, d) = 8 offsetof(StructB, i) = 16
=== PAIR C: struct/union {
    int;
    char[20];
}
=== sizeof(StructC) = 24 sizeof(UnionC) = 20 offsetof(StructC, id) = 0 offsetof(StructC, name) = 4
=== MEMBER ORDER(structs only) === sizeof(StructUnordered) {
    char,double,char
}
= 24 sizeof(StructOrdered) {
    double,char,char
}
= 16
=== DOES A UNION ACTUALLY SAVE MEMORY? === struct {
    int,int,double
}
= 16 struct {
    int,union {
        int,double
    }
}
= 16 <- NO saving(padding eats it) struct {
    int,int,int
}
= 12 struct {
    int,union {
        int,int
    }
}
= 8 <- 4 bytes saved per record
=== ARRAYS OF 1000 RECORDS === StructA[1000] = 12000 bytes UnionA[1000] = 4000 bytes

(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

  1. Using a union when you need all the members — the fundamental error. Check question 1 of the framework first.
  2. 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.
  3. Assuming sizeof(struct) equals the sum of members — padding. Pair B is 24, not 13.
  4. Assuming sizeof(union) equals the sum of members — it's the largest member. Pair A's union is 4, not 9.
  5. Assuming a union always saves memory — alignment padding frequently cancels the saving entirely (the 16 vs 16 case).
  6. Ignoring member order in structs — largest-to-smallest can shrink a struct by 50%.
  7. Reading a union member other than the last written — implementation-defined reinterpretation, possible trap representation. Switch on the tag instead.
  8. Comparing structs or unions with == — not allowed in C. Compare member by member (and strcmp for strings).
  9. memcmp on structs — padding bytes may differ even when every field matches, giving false "not equal". Serialize or compare fields explicitly.
  10. fwrite-ing structs to files or sockets — writes garbage padding and isn't portable between compilers. Serialize field by field.
  11. fwrite-ing a union without its tag — the recipient can't tell what the bytes mean.
  12. 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.
  13. Expecting offsetof to be 0 for all struct members — only unions have that property. Struct offsets differ by design.
  14. Reaching for #pragma pack to "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:

  1. Ask "and" or "or" first. Need both fields at once → struct. Have one-of-several alternatives, tracked by a tag → union.
  2. Do the arithmetic before believing a union saves memory. Compute both sizeof values; 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

main.c
Run Online
#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

Best Practice & Key Takeaways
Use structs for storing composite records and unions for storing mutually exclusive alternative states.

Frequently Asked Questions & Technical Interview Insights

Technical Interview & Viva Questions

Frequently asked C programming interview questions
Q

Q: When should you choose a `union` over a `struct`?

A: When you have multiple mutually exclusive attributes where an object can only be one type at a time (e.g., an event payload that is either a MouseEvent, KeyboardEvent, or TouchEvent).
Q

Q: Can a struct contain a union, and can a union contain a struct?

A: Yes! Combining them creates Tagged Unions and complex protocols.

Related Tools, Tutorials & Authoritative References

Recommended Interactive Tools

Authoritative Standards & Citations