The C Path — learn C, visually

🎯 Pointers & Memory

Unions & bit-fields: one space, many shapes

⏱ 12 min · free interactive lesson · quizzes, visualizations & a real compiler

▶ Open the interactive lesson — free, no signup
Why you're learning this

One 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

word.c
#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;
}
terminal
$ 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":

tagged.c
#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;
}
terminal
$ 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:

endian.c
#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;
}
terminal
$ 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:

flags.c
#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;
}
terminal
$ 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)