CUnions

Unions

A union looks like a struct syntactically, but its memory layout is completely different. Where a struct gives every member its own separate space, a union gives all of its members the exact same space — enough to hold its largest member, and no more. Every member of a union is really just a different way of interpreting the same bytes.

struct vs. union at a glance

struct

union

Memory layout

Each member gets its own space; total size is (roughly) the sum of all members.

All members share one space; total size is the size of the largest member.

Values stored

All members hold independent values simultaneously.

Only one member holds a meaningful value at a time.

Typical use

Grouping several related values together.

Representing one value that can be one of several types (a variant).

Fundamentally different, despite similar syntax
A struct combines values; a union overlaps them. Reading a struct member always gives you back what you last wrote to that specific member. Reading a union member can give you back bytes that were written through a completely different member.
Writing one member, reading another

C
#include <stdio.h>

union Data {
    int i;
    float f;
    char bytes[4];
};

int main(void) {
    union Data d;

    d.i = 1078530011;   // write through the int member
    printf("as int:   %d\n", d.i);
    printf("as float: %f\n", d.f);   // reading through a different member

    printf("size of union Data: %zu bytes\n", sizeof(union Data));

    return 0;
}
as int:   1078530011
as float: 3.141593
size of union Data: 4 bytes

d.i and d.f occupy the exact same 4 bytes. Writing 1078530011 through d.i stores a particular bit pattern; reading that same memory back through d.f reinterprets those same bits as an IEEE 754 float, which happens to be approximately 3.141593. Nothing was "converted" — the bits never changed, only how they were read did.

Practical use: variant data with a tag

In real code, a union is almost always paired with a separate "tag" field that records which member is currently valid — this pattern is called a tagged union, and it is how many languages implement variant/sum types under the hood.

C
#include <stdio.h>

typedef enum { TYPE_INT, TYPE_FLOAT, TYPE_STRING } ValueType;

typedef struct {
    ValueType type;      // the tag: which member is currently valid
    union {
        int i;
        float f;
        char str[32];
    } value;
} Variant;

void print_variant(const Variant *v) {
    switch (v->type) {
        case TYPE_INT:
            printf("int: %d\n", v->value.i);
            break;
        case TYPE_FLOAT:
            printf("float: %f\n", v->value.f);
            break;
        case TYPE_STRING:
            printf("string: %s\n", v->value.str);
            break;
    }
}

int main(void) {
    Variant a = {.type = TYPE_INT, .value.i = 42};
    Variant b = {.type = TYPE_STRING, .value.str = "hello"};

    print_variant(&a);
    print_variant(&b);

    return 0;
}
int: 42
string: hello
Reading the wrong member is technically undefined
Deliberately reading a union member other than the one most recently written to (as in the `int`/`float` example above, called "type punning") is, strictly speaking, undefined behavior in standard C, even though most compilers support it as a common, well-understood extension. Some platforms and compiler flags can behave unexpectedly with this pattern. The tagged-union style shown above — always reading through the same member you last wrote, guided by an explicit tag — is the safe, fully portable way to use unions.
  • All members of a union share the same memory; its size is the size of its largest member.

  • Writing one member and reading another reinterprets the same underlying bytes.

  • Pair a union with a tag field (often an enum) so code always knows which member is currently valid.

  • Reading a member other than the one last written is undefined behavior in the standard, even though it is widely supported in practice — prefer the tagged pattern.