Unions and Bit-Fields

A union lets several members share the same memory, holding only one of them at a time. Combined with a tag that records which member is in use, it models values that can be one of several kinds. Bit-fields, at the other end, pack several tiny values into a single integer.

Unions

A union is declared like a struct, but all members start at the same address, and the union is only as big as its largest member:

C
#include <stdio.h>

union number {
    int i;
    double d;
    char bytes[8];
};

int main(void) {
    union number n;
    n.i = 42;
    printf("as int: %d\n", n.i);
    n.d = 2.5;            // overwrites the same memory
    printf("as double: %.1f\n", n.d);
    printf("sizeof: %zu\n", sizeof n);
    return 0;
}
Output
as int: 42
as double: 2.5
sizeof: 8

Only the member you wrote last holds a meaningful value. Reading a different member reinterprets the same bytes, which is rarely what you want.

Tagged unions

The usual pattern pairs a union with an enum that records which member is valid — a "tagged union" or "variant":

C
#include <stdio.h>

enum shape_kind { CIRCLE, RECTANGLE, TRIANGLE };

struct shape {
    enum shape_kind kind;
    union {
        struct { double radius; } circle;
        struct { double width, height; } rect;
        struct { double base, height; } tri;
    };
};

double area(const struct shape *s) {
    switch (s->kind) {
        case CIRCLE:    return 3.14159265 * s->circle.radius * s->circle.radius;
        case RECTANGLE: return s->rect.width * s->rect.height;
        case TRIANGLE:  return 0.5 * s->tri.base * s->tri.height;
    }
    return 0;
}

int main(void) {
    struct shape shapes[] = {
        { .kind = CIRCLE, .circle = { 1.0 } },
        { .kind = RECTANGLE, .rect = { 3.0, 2.0 } },
        { .kind = TRIANGLE, .tri = { 4.0, 5.0 } },
    };
    for (int i = 0; i < 3; i++) {
        printf("shape %d: area %.2f\n", i, area(&shapes[i]));
    }
    printf("each shape takes %zu bytes\n", sizeof shapes[0]);
    return 0;
}
Output
shape 0: area 3.14
shape 1: area 6.00
shape 2: area 10.00
each shape takes 24 bytes

The union here is anonymous (C11): its members are reached directly as s->circle instead of s->u.circle. Interpreters use this pattern to store values that can be numbers, strings or lists; event systems use it for messages of different types.

Bit-fields

A struct member can be given a width in bits. The compiler packs adjacent bit-fields into the same storage unit:

C
#include <stdio.h>

struct pixel_flags {
    unsigned visible : 1;
    unsigned selected : 1;
    unsigned layer : 4;      // 0..15
    unsigned opacity : 7;    // 0..127
};

int main(void) {
    struct pixel_flags f = { .visible = 1, .layer = 9, .opacity = 100 };
    f.selected = 1;
    printf("visible %u, selected %u, layer %u, opacity %u\n", f.visible, f.selected, f.layer, f.opacity);
    printf("size: %zu bytes\n", sizeof f);
    f.layer = 15;
    printf("layer now %u\n", f.layer);
    return 0;
}
Output
visible 1, selected 1, layer 9, opacity 100
size: 4 bytes
layer now 15

Assigning a value too big for the field keeps only the low bits, and compilers warn when they can see it happening.

Bit-fields are convenient, but the exact layout — bit order, padding, how fields straddle storage units — is up to the compiler. For data that must match an exact format (files, network packets, hardware registers), use plain integers with shifts and masks (lesson 14) instead.

Inspecting bytes

Looking at the bytes of a value through an unsigned char pointer is always allowed, and shows how the machine stores numbers. Most computers today are little-endian: the lowest byte comes first.

C
#include <stdio.h>
#include <stdint.h>

int main(void) {
    uint32_t value = 0x11223344;
    const unsigned char *bytes = (const unsigned char *) &value;
    for (size_t i = 0; i < sizeof value; i++) {
        printf("%02X ", bytes[i]);
    }
    printf("(%s-endian)\n", bytes[0] == 0x44 ? "little" : "big");
    return 0;
}
Output
44 33 22 11 (little-endian)

Try it yourself

Model a configuration value that can be an integer, a decimal or a string, and print each kind correctly.

Try it yourself · main.c
Edit the code, copy it into main.c, then compile and run it with cc -std=c17 -Wall main.c -o main && ./main. How to set up a C compiler
Output
port       = 8080
ratio      = 0.75
hostname   = "example.org"