This page is the drill ground. The parent note built the ideas — one shared room, size = largest member, writing clobbers the rest. Here we grind through every kind of situation a union can throw at you, so no exam question or bug can surprise you.
Before we start, one reminder of the vocabulary, so nothing is used before it is earned:
Every union problem falls into one of these cells. The examples below are labelled with the cell they cover — together they fill the whole grid.
Cell
Case class
What makes it tricky
A
Sizing — plain
largest member sets size
B
Sizing — alignment padding
largest member is not the strictest-aligned one
C
Sizing — degenerate (array/struct member)
a member that is itself big or awkward
D
Overwrite — same-size members
write one, read another (garbage / UB)
E
Type punning — read raw bytes
little-endian byte order matters
F
Endianness flip
big-endian gives a different answer
G
Zero / default init
= {0} sets only the first member
H
Real-world word problem
tagged union bookkeeping
I
Exam-style twist
nested union / char-array reinterpretation
J
Bitfield union
bits, not bytes, share the same word
Figure 1 below draws the shared "room" of bytes with each member laid over it, and lists all matrix cells so you can see at a glance what each example varies. Notice how the biggest bar (the double) is the one that sets the room's width — that is Cell A in one picture.
Figure 1 — One shared room of bytes; the largest member fixes its width. The labels A–J tag the case classes worked below.
List the member sizes.char=1, short=2, int=4.
Why this step? The union must fit the worst case, so we need to know the biggest member first — see sizeof operator.
Take the maximum.max(1,2,4)=4.
Why this step? All members start at offset 0 and overlap, so the room only needs to be as wide as the widest lodger — not the sum.
Check alignment.int needs 4-byte alignment; 4 is already a multiple of 4, no padding added.
Why this step? The final size must be a multiple of the strictest member's alignment so arrays of unions stay lined up (see Memory alignment and padding).
Answer: sizeof(union Small) == 4.
Verify: If it were a struct instead, size would be at least 1+2+4=7 (padded to 8). The union being smaller than that sum confirms overlap. ✓
Find the largest member.buf = 10 bytes, int = 4 bytes. Largest = 10.
Why this step? Same rule: reserve room for the biggest lodger.
Find the strictest alignment.int requires alignment 4. char[10] requires alignment 1. Strictest = 4.
Why this step? The whole union must be aligned to its strictest member, because the compiler may put unions back-to-back in an array.
Round the size up to a multiple of 4. 10 is not a multiple of 4. Round up: ⌈10/4⌉×4=12.
Why this step? This is the align(⋅) step from the parent's formula size(U)=align(maxmsize(m)). Without it, the next union in an array would start at a misaligned address for its int.
Answer: sizeof(union Pad) == 12, not 10.
Verify:12 is the smallest multiple of 4 that is ≥10. 12mod4=0 ✓, and 12≥10 ✓.
Size the struct member. Three shorts of 2 bytes each, all 2-aligned, pack with no gaps: 2+2+2=6. See Structs — separate memory for each member.
Why this step? A member can itself be a compound type — we must know its full size before comparing.
Compare members.int=4, struct t=6. Largest = 6.
Why this step? The struct is the worst case here, so it sets the width.
Alignment. Strictest alignment among all members: int wants 4, each short wants 2 → strictest is 4. Round 6 up to a multiple of 4 → 8.
Why this step? Same padding rule as Example 2; 6 is not a multiple of 4.
Answer: sizeof(union Wrap) == 8.
Verify:8 is the least multiple of 4 that is ≥6. 8mod4=0 ✓.
Both members share the same 4 bytes. Writing u.f = 2.0f overwrites those bytes with the float bit pattern of 2.0.
Why this step? This is the whole point of a union — one room. The int is gone.
Note the standard caveat. Reading u.i after writing u.f is undefined behavior per the C standard (the last written member is f, and int is not a character type). In practice mainstream compilers reinterpret the bytes as shown, but do not rely on it in portable code — use memcpy if you truly want the bits.
Why this step? The reviewer-critical nuance: type punning through a non-character member is UB, not guaranteed. See Type punning and bit-level reinterpretation.
Find the float bit pattern of 2.0f (IEEE-754 single precision).2.0=1.0×21. Sign=0, exponent field =1+127=128=100000002, mantissa = 0. Bits: 0 10000000 00000000000000000000=0x40000000.
Why this step? On typical compilers, reading u.i reads these exact bits as a signed integer — no arithmetic conversion.
Interpret 0x40000000 as an int.0x40000000=4×167=1073741824.
Why this step? The high nibble 4 sits in the top byte; converting the hex gives the decimal int.
Answer: on a typical little-endian compiler u.i prints 1073741824, NOT 1 and NOT 2 — but formally this read is UB.
Verify:0x40000000=230=1073741824. This is the raw reinterpretation, exactly the mistake the parent note warns about — a union is not a converter.
Figure 2 lays the four bytes into slots b[0]…b[3] with their offsets, and the yellow arrow marks where the least significant byte (0x44) lands. Read the steps against that picture — the arrow is the whole rule.
Figure 2 — Little-endian layout of 0x11223344: the least significant byte 0x44 sits at offset 0 (b[0]), the most significant 0x11 at offset 3.
Split the int into bytes by value. Most significant byte 0x11, then 0x22, 0x33, least significant byte 0x44.
Why this step? Each unsigned char slot holds exactly one byte; we need to know which value lands where.
Apply little-endian: least significant byte → lowest offset. So b[0] (offset 0) gets 0x44, and b[3] gets 0x11. Because we read through unsigned char, this is fully defined by the standard — the legal way to inspect bytes.
Why this step? Little-endian, by definition, puts the least significant byte first. This is the arrow in Figure 2.
Big-endian puts the most significant byte at the lowest offset.Why this step? Big-endian is the mirror of little-endian; the highest-weight byte goes first.
Therefore b[0] = 0x11, b[3] = 0x44.Why this step? Direct consequence of the byte ordering — this is why type punning is non-portable (see Endianness — byte order).
Answer: big-endian b[0]=0x11 (vs little-endian 0x44 in Example 5).
Verify: Big-endian read of bytes 11 22 33 44 = 0x11⋅2563+0x22⋅2562+0x33⋅256+0x44=0x11223344 ✓. The value is identical; only the byte layout differs.
{0} initializes only the first member d.i to 0.Why this step? By the rule restated above, a union's brace-initializer touches only the first-listed member d.i.
But all members share the same 4 bytes. Setting d.i = 0 writes four zero bytes 00 00 00 00.
Why this step? Because they overlap, zeroing the int does zero the shared storage — so here the "only first member" rule happens to zero the bytes seen by every member.
Read d.f: the bit pattern 0x00000000 as a float is exactly +0.0.Why this step? IEEE-754: all-zero bits = positive zero. So here reading d.f happens to give 0.0 (still formally UB as a non-char read, but the value is deterministic on IEEE machines).
Answer:d.i prints 0, d.f prints 0.000000.
Verify: float bits 0x00000000 = +0.0 ✓. Caution: this only works because 0 has all-zero bits — with = {1}, d.f would be garbage (0x00000001 = a tiny denormal ≈ 1.4×10−45, not 1.0).
Why this step? The union has no memory of which member is live — the enum tag is your bookkeeping.
Read only the member the tag names.
if (s.tag == COUNT) printf("%d\n", s.v.n); // prints 42else printf("%s\n", s.v.text);
Why this step? Reading s.v.text when n is live would treat the integer 42 (0x2A) as a pointer address — undefined behavior and almost certainly a crash. The tag prevents that by ensuring you only read the last-written member.
Answer: with tag == COUNT, the program safely prints 42; reading .text here is undefined and likely segfaults.
Verify:s.v.n = 42 stores bytes for 42; the tag routes us to the int branch → 42. The bookkeeping is what makes this safe, exactly as the parent's tagged-union pattern requires.
Lay out the bytes.c[0]=0x41 at offset 0, c[1]=0x42 at offset 1. (We wrote through char, so those byte values are guaranteed.)
Why this step? We wrote each byte individually through the char array, so we know exactly which byte value sits at each offset before we reinterpret the pair as a short. Reading raw bytes through a character type is always defined, so this layout is trustworthy.
Read as unsigned short (2 bytes) on little-endian. The byte at the lowest offset (offset 0, value 0x41) is the least significant byte of the short; the byte at offset 1 (0x42) is the most significant. So the short's value is 0x42×256+0x41.
Why this step? Little-endian, by definition, places the least significant byte at the lowest address — so offset 0 supplies the low half and offset 1 the high half when the two bytes are assembled into one 16-bit number.
Compute the number.0x42=66 and 0x41=65, so 66×256+65=16896+65=16961. In hex that is 0x4241.
Why this step? Reassembling high-byte ×256 plus low-byte turns the two stored bytes into the single value the %u read prints.
Answer:u.s prints 16961 (0x4241) on little-endian. (On big-endian, offset 0 would be the high byte, giving 0x4142 = 16706.)
Understand the layout.raw is 1 byte (8 bits). The struct packs ready(1) + error(1) + mode(2) + unused(4) = 8 bits into the same byte. sizeof(union Status) == 1.
Why this step? Both members overlap the same storage — the union rule still holds, but now the bits line up, not whole bytes. Size = largest member = 1 byte.
Write the raw byte.st.raw = 0x0D = binary 0000 1101. On a typical little-endian compiler, bitfields fill from the least significant bit upward.
Why this step? So bit 0 (the 20 place) is ready, bit 1 is error, bits 2–3 are mode. Bit ordering of bitfields is implementation-defined — this is the common layout, and the reason bitfield-punning is non-portable.
Extract each field from 0000 1101.
bit 0 = 1 → ready = 1
bit 1 = 0 → error = 0
bits 2–3 = 11 = 3 → mode = 3
bits 4–7 = 0000 → unused = 0Why this step? Each field pulls out only its own bits; mode is a 2-bit field so it can hold 0–3.
Answer:ready = 1, error = 0, mode = 3.
Verify: Reassemble: ready⋅20+error⋅21+mode⋅22=1+0+3⋅4=13=0x0D ✓. The bits reproduce the raw byte, confirming the overlap.
Caveat: bitfield bit-order (which end fills first) and straddling of storage units are implementation-defined — treat this pattern as platform-specific, exactly like byte-level type punning.
A → Ex 1 ::: plain largest-member sizing (4)
B → Ex 2 ::: alignment padding, 10 rounds to 12
C → Ex 3 ::: struct member inside union, 6 rounds to 8
D → Ex 4 ::: same-size overwrite gives 1073741824 (formally UB)
E → Ex 5 ::: little-endian byte order of 0x11223344 (char read = legal)
F → Ex 6 ::: big-endian flips b[0] to 0x11
G → Ex 7 ::: {0} zeros first member → 0.0 float
H → Ex 8 ::: tagged union safe read = 42
I → Ex 9 ::: nested char/short reinterpret = 16961
J → Ex 10 ::: bitfield union, raw 0x0D → ready 1, error 0, mode 3