5.1.24 · D2C Programming

Visual walkthrough — Unions — overlapping memory

1,819 words8 min readBack to topic

We start with the most literal thing a computer has: a row of numbered boxes.


Step 1 — Memory is a numbered row of 1-byte boxes

WHAT. Before we talk about unions at all, we need to agree on what "memory" even is. In C, memory is a long line of tiny boxes. Each box holds exactly one byte (8 bits, a number from 0 to 255). Each box has an address — box 0, box 1, box 2, and so on.

WHY start here. Every claim about unions ("they overlap", "size = largest") is really a claim about which boxes a member occupies. If we don't have the boxes drawn, the rest is hand-waving.

PICTURE. Below, the row of white boxes is memory. The cyan numbers under them are addresses. Notice the little bracket labelled "offset 0" — that is where our story begins for every union member.

Figure — Unions — overlapping memory

Step 2 — A variable claims a run of boxes; its type says HOW MANY

WHAT. When you declare a variable, C reserves a run of consecutive boxes for it. How many boxes? That is what the sizeof operator reports. A char grabs 1 box, an int grabs 4 boxes, a double grabs 8 boxes (on the common 64-bit setup we'll assume throughout).

WHY this matters. A union is about sharing boxes between members. To see sharing, we first must see each member's own footprint — its length in boxes.

PICTURE. Three separate runs, drawn to scale. Read each equation right on the drawing:

Each symbol here is just "number of boxes this type occupies." The char fills 1 box; the int fills 4; the double fills 8.

Figure — Unions — overlapping memory

Step 3 — A struct lines the members up SIDE BY SIDE (the thing a union is NOT)

WHAT. To appreciate a union, look first at its opposite, the struct. A struct gives each member its own run of boxes, placed one after another. Nothing overlaps.

WHY show the contrast. The single most common mistake (from the parent note's "Mistake 2") is expecting a union to behave like a struct — summing sizes. Seeing them side by side kills that reflex.

PICTURE. The char sits in box 0, then the int starts later, then the double later still. The total width is the sum (plus padding, shown as amber gaps):

Here means "size of member in boxes," and is the padding C inserts so each member lands on a legal address.

Figure — Unions — overlapping memory

Step 4 — A union STACKS all members onto the SAME boxes (offset 0)

WHAT. Now the union. Instead of side by side, every member starts at the same first box — offset 0. They are drawn stacked on top of each other, all pointing at the identical boxes.

WHY. This is the defining property. There is no separate room per member; there is one room, and each member is just a different interpretation of the boxes in that room.

PICTURE. Look at the three coloured bars: the char covers box 0 only; the int covers boxes 0–3; the double covers boxes 0–7. They all start at the same left edge (the amber "offset 0" arrow). They are transparent overlays on the same memory, not neighbours.

That single equation — "everyone starts at 0" — is the whole idea of a union.

Figure — Unions — overlapping memory

Step 5 — Because everyone starts at 0, the union must be as wide as the WIDEST member

WHAT. Here is the parent's central result, derived. Since member occupies boxes through , the union must reserve enough boxes to satisfy the worst case — the member that reaches furthest right.

WHY this exact formula. We take a max, not a sum, precisely because the members overlap. Where a struct pays for every member, a union pays only once, for the greediest one. Everything smaller fits inside that reserved space.

PICTURE. The double (8 boxes) is the tallest bar; the int and char fit entirely within its width. The union's outline is drawn to hug the widest bar:

  • — scan all members, keep the largest box-count (here , the double).
  • — round that number up until it is a multiple of the strictest member's alignment requirement, so an array of these unions keeps every element correctly aligned.

For union { char c; int i; double d; }: , and is already a multiple of , so sizeof = 8.

Figure — Unions — overlapping memory

Step 6 — Writing one member OVERWRITES the shared boxes

WHAT. Since all members share boxes 0…, storing into member A changes the very boxes member B reads. There is no separate copy to fall back on.

WHY. This follows directly from Step 4. If two members point at box 0, then whoever wrote last owns box 0. Reading the other member now reads the last writer's bits — not its own.

PICTURE. Two frames. Frame 1: we do d.i = 65, so boxes 0–3 hold the integer pattern for 65. Frame 2: we do d.f = 3.14f, and the same boxes 0–3 are painted with the float's bit pattern. The old integer is simply gone.

union Data d;
d.i = 65;            // boxes 0..3 = int bits of 65
d.f = 3.14f;         // SAME boxes 0..3 = float bits of 3.14
printf("%d\n", d.i); // garbage — reads float bits AS an int
Figure — Unions — overlapping memory

Step 7 — The degenerate & edge cases (nothing left unshown)

WHAT. We handle the corners the earlier steps glossed over.

WHY. The contract: the reader must never meet a scenario we didn't draw.

Case A — all members the same size. union { int i; float f; }: both are 4 boxes, so and every member fills the entire union. Overlap is total.

Case B — a member needs padding to align. union { char c; double d; }: from the double. Even though char is 1 box, the union is 8 boxes, so an array of these unions keeps every double on an 8-aligned address. The extra boxes past the char are unused when c is active — but reserved.

Case C — default initialization. union Data d = {65}; sets only the first member (d.i = 65). Reading any other member afterward reads whatever bit pattern 65 makes — meaningless as a float. (Parent's Mistake 3.)

Case D — reading the lowest byte (endianness). With union { int i; char c[4]; } u; u.i = 65; on a little-endian machine, the lowest-order byte 0x41 lands in box 0, so u.c[0] == 'A' (ASCII 65). On a big-endian machine that byte would land in box 3 instead.

PICTURE. Each case as its own labelled memory strip, amber marking the "active/used" boxes and cyan marking "reserved but dead" boxes.

Figure — Unions — overlapping memory
Recall Check yourself on the edge cases

union { char c; double d; } — what is sizeof? ::: 8, the size of the largest member double (already 8-aligned). union Data d = {65}; initializes which member? ::: Only the first one (d.i); other members are meaningless to read. Little-endian, u.i = 65, what is u.c[0]? ::: 'A' (0x41), the lowest-order byte of 65. Big-endian, same code, where does 0x41 live? ::: In box 3 (u.c[3]), the highest-address byte.


The one-picture summary

Everything above compressed into a single diagram: memory as boxes → members stacked at offset 0 → size = the widest bar → writing repaints shared boxes.

Figure — Unions — overlapping memory
Recall Feynman: the whole walkthrough in plain words

Picture a strip of numbered mailboxes. A struct gives each thing its own mailbox further down the row, so it needs as many boxes as everything added up. A union does the opposite: it forces every member to use the same first mailboxes, starting at box 0. Because a double needs 8 boxes and a char needs 1, the union just reserves 8 — enough for the greediest tenant — and everyone smaller squeezes inside that same space. Now the twist: since they truly share the boxes, whatever you write last owns them. Write an int, then a float, and the int is painted over — reading it back gives nonsense, because you're reading a float's bits as if they were an int. That "reading the same boxes through different glasses" is type punning, and which byte you see first depends on endianness. The union never remembers who's living there, so in real code you keep a little tag beside it. That's the entire idea: one shared room, sized for its biggest lodger, remembering only its last occupant.


Connections

  • Parent: Unions — overlapping memory
  • Structs — separate memory for each member
  • Memory alignment and padding
  • Endianness — byte order
  • Type punning and bit-level reinterpretation
  • Enums
  • sizeof operator