🎯 Pointers & Memory
Unions & bit-fields: one space, many shapes
▶ Open the interactive lesson — free, no signupOne slot, several possible shapes: a spreadsheet cell holds a number or text — never both at once — and a game inventory slot holds a sword or a potion. Unions are C's way of storing "one of several things" without paying for the space of all of them at once. There's a party trick thrown in: using one to expose the raw bits hiding inside a float — the very bits you toggled back in Part 0.
A struct gives each member its own bytes, side by side. A union does the opposite: every member starts at the same address, overlapping in the same bytes. Its size is (roughly) the size of the largest member. Why would you want that? Storage where a value is only ever one of several things at a time.
Members that overlap
#include <stdio.h>
union Word {
unsigned int u; /* 4 bytes */
unsigned char b[4]; /* the SAME 4 bytes */
};
int main(void) {
union Word w;
w.u = 0x11223344;
printf("sizeof(union Word) = %zu\n", sizeof w);
for (int i = 0; i < 4; i++)
printf("b[%d] = 0x%02x\n", i, w.b[i]);
return 0;
}$ gcc word.c -o word && ./word sizeof(union Word) = 4 b[0] = 0x44 b[1] = 0x33 b[2] = 0x22 b[3] = 0x11 # one write to u changed all four b[i] — same bytes! # (and the 0x44 came FIRST… hold that thought)
▶ This spot has an interactive memgrid widget — open the interactive lesson to play with it.
🧠 Checkpoint: What is sizeof a union whose members are an int (4) and a double (8)?
- 12
- 8 — the largest member (plus any alignment padding)
- 4 — the first member
- Implementation-defined, could be 1
Show answer
8 — the largest member (plus any alignment padding) — All members share one storage area, so it only needs to fit the biggest: 8 bytes. A struct with the same members would need at least 12 (usually 16, with alignment).
Writing w.u then reading w.b — reading a different member than you last wrote — is called type punning. In C it's allowed: you get the stored bytes reinterpreted as the other type (C99 onwards spells this out; beware, the same trick is undefined behavior in C++!). It's how programmers peek at the raw bytes of floats, ints, and more.
Tagged unions: the honest pattern
A raw union doesn't remember which member is currently valid — you must. The universal solution pairs the union with an enum tag recording what's inside. This is C's answer to "a value that can be one of several types":
#include <stdio.h>
enum Kind { KIND_INT, KIND_FLOAT };
struct Value {
enum Kind kind; /* the TAG: what's inside? */
union {
int i;
double f;
} as; /* the payload */
};
void print_value(struct Value v) {
switch (v.kind) {
case KIND_INT: printf("int %d\n", v.as.i); break;
case KIND_FLOAT: printf("float %g\n", v.as.f); break;
}
}
int main(void) {
print_value((struct Value){ KIND_INT, { .i = 42 } });
print_value((struct Value){ KIND_FLOAT, { .f = 3.14 } });
return 0;
}$ gcc tagged.c -o tagged && ./tagged int 42 float 3.14
🧠 Checkpoint: In a tagged union, what is the tag for?
- It speeds up member access
- It records which union member is currently the valid one
- The linker requires it
- It fixes the union’s alignment
Show answer
It records which union member is currently the valid one — The union itself has no memory of what was last stored. The tag is your own bookkeeping — set it on every write, switch on it on every read. Interpreters, JSON parsers, and compilers are built on this pattern.
Party trick: discovering endianness
Which byte of a multi-byte integer comes first in memory? Little-endian machines (x86, most ARM) store the least significant byte at the lowest address; big-endian stores the most significant first. A union lets your program check at runtime — the classic C interview trick:
#include <stdio.h>
int main(void) {
union {
unsigned int u;
unsigned char c[4];
} probe = { .u = 1 }; /* bytes: 01 00 00 00 or 00 00 00 01? */
if (probe.c[0] == 1)
printf("little-endian (x86, most ARM)\n");
else
printf("big-endian (network byte order)\n");
return 0;
}$ gcc endian.c -o endian && ./endian little-endian (x86, most ARM) # the low byte of 1 sits at the LOWEST address on this machine
Bit-fields: members measured in bits
Inside a struct, you can give a member a width in bits. The compiler packs adjacent bit-fields into shared storage — ideal for flag sets and matching hardware register layouts:
#include <stdio.h>
struct Flags {
unsigned int visible : 1; /* one bit */
unsigned int locked : 1;
unsigned int mode : 3; /* 3 bits: 0..7 */
};
int main(void) {
struct Flags f = { .visible = 1, .mode = 5 };
f.locked = 1;
printf("sizeof = %zu\n", sizeof f); /* 5 bits, but… */
printf("mode = %u\n", f.mode);
f.mode = 9; /* only 3 bits: 9 mod 8 = 1 */
printf("mode = %u\n", f.mode);
return 0;
}$ gcc flags.c -o flags && ./flags sizeof = 4 mode = 5 mode = 1 # 5 bits of data, one 4-byte storage unit; overflow wrapped mod 8
Bit-field layout is implementation-defined: packing order, straddling of storage units, and more vary by compiler and ABI. Fine within one program; not a portable serialization format. Also: you can't take the address of a bit-field — &f.mode is illegal, since it isn't byte-aligned.
🧠 Checkpoint: On a little-endian machine, after w.u = 0x11223344;, what is w.b[0]?
- 0x11
- 0x22
- 0x33
- 0x44
Show answer
0x44 — Little-endian puts the LEAST significant byte first: 44 33 22 11 in ascending addresses. On big-endian iron the same code prints 0x11 — which is exactly why the union probe works as a detector.
▶ This spot has an interactive editor widget — open the interactive lesson to play with it.
You've now pointed at data of every shape. One target remains, the coolest of all: pointing at code.
▶ Practice this lesson interactively (with live gcc)