πŸ‡―πŸ‡΅ ζ—₯本θͺž | πŸ‡ΊπŸ‡Έ English
Advertisement

C Unions

Multiple members sharing one memory slot. Learn it alongside struct.

πŸ“– What to learn on this page
βœ… Must-know essentials
  • All union members share one memory slot
  • sizeof is the size of the largest member
  • Only one member is valid at a time
⭐ Read if you have time
  • Classic endianness detection
  • Tagged union (enum + union)
  • Type punning in C11
πŸ’ͺ Not getting it first time is normal
Unions pack several views over one memory slot β€” abstract, and rarely needed day-to-day. It's OK to skim.
How to retry
  1. Solidify structs first
  2. Verify with sizeof that the size equals the largest member
  3. The endianness check example is the most practical takeaway
  4. Not getting it is fine β€” return when you meet a real use case
πŸ’‘ Tip: Real-world uses are "tagged union" and type punning. For beginners, mastering struct is enough.

What is a union

A union is a type whose members all occupy the same memory. Its size equals the largest member. Only one member is meaningful at a time.
#include <stdio.h>

union Data {
    int    i;
    float  f;
    char   s[8];
};

int main(void) {
    union Data d;

    d.i = 42;
    printf("d.i = %d\n", d.i);        // 42

    d.f = 3.14f;                    // i becomes invalid now
    printf("d.f = %f\n", d.f);
    printf("d.i = %d\n", d.i);        // bit pattern of the float

    printf("sizeof = %zu\n", sizeof(d));// 8 (largest member)
    return 0;
}
Difference from struct: struct places members side by side (sum of sizes); union stacks them (size of the largest).

struct vs union (memory picture)

struct S { int a; int b; }

a
a
a
a
a
0-3
b
b
b
b
b
4-7
sizeof = 8 bytes. a and b are independent.

union U { int a; int b; }

a = b
a/b
a/b
a/b
a/b
0-3
sizeof = 4 bytes. Writing a also changes b β€” same location.

Experiment

#include <stdio.h>

struct S { int a; int b; };
union  U { int a; int b; };

int main(void) {
    struct S s = {10, 20};
    printf("struct: a=%d b=%d size=%zu\n", s.a, s.b, sizeof(s));
    // β†’ a=10 b=20 size=8

    union U u;
    u.a = 10;
    u.b = 20;
    printf("union:  a=%d b=%d size=%zu\n", u.a, u.b, sizeof(u));
    // β†’ a=20 b=20 size=4   (same slot!)
}

Size & alignment

A union is at least as large as its biggest member; alignment requirements may add trailing padding.
union Mix {
    char   c;       // 1 byte
    int    i;       // 4 bytes
    double d;       // 8 bytes
};
// sizeof(union Mix) is at least 8, aligned to double

View the same slot two ways

union Split {
    int           n;
    struct { char b0, b1, b2, b3; } bytes;
};

union Split s;
s.n = 0x12345678;
printf("%02x %02x %02x %02x\n",
       (unsigned)s.bytes.b0, (unsigned)s.bytes.b1,
       (unsigned)s.bytes.b2, (unsigned)s.bytes.b3);
// little-endian: 78 56 34 12
// big-endian:    12 34 56 78

Endianness check

Unions give a clean way to find out whether the current CPU is little- or big-endian. A classic textbook example.
#include <stdio.h>

int is_little_endian(void) {
    union { int i; char c[sizeof(int)]; } u;
    u.i = 1;
    return u.c[0] == 1;
}

int main(void) {
    printf("%s-endian\n",
           is_little_endian() ? "little" : "big");
}

Why it works

The 4 bytes of int i = 1:
little-endian (x86 etc.): 01 00 00 00 β†’ c[0] == 1
big-endian (some ARM etc.): 00 00 00 01 β†’ c[0] == 0
Without union: int n = 1; char *p = (char*)&n; also works, but the union form is often considered friendlier to the compiler's strict-aliasing rules.

Tagged union (variant pattern)

When a value can hold one of several types, pair a "which type" tag (enum) with the data union inside a struct. This is C's version of a sum type / Rust's enum.
#include <stdio.h>

enum ValueKind { V_INT, V_FLOAT, V_STRING };

struct Value {
    enum ValueKind kind;        // "which is active"
    union {
        int   i;
        float f;
        char  s[32];
    } data;
};

void print_value(struct Value v) {
    switch (v.kind) {
        case V_INT:    printf("int: %d\n", v.data.i);    break;
        case V_FLOAT:  printf("float: %f\n", v.data.f); break;
        case V_STRING: printf("str: %s\n", v.data.s);   break;
    }
}

int main(void) {
    struct Value a = {V_INT, .data.i = 42};
    struct Value b = {V_FLOAT, .data.f = 3.14f};
    struct Value c;
    c.kind = V_STRING;
    snprintf(c.data.s, sizeof(c.data.s), "hello");

    print_value(a);  // int: 42
    print_value(b);  // float: 3.14
    print_value(c);  // str: hello
}
Common in JSON parsers, ASTs, and protocol messages. Wrap reads/writes behind functions so callers can't forget to check kind.

Size considerations

// sizeof(struct Value) = enum(4) + largest member(32) + padding
// even a small int value pays the 32-byte string slot

Pitfalls & when to use

Gotchas

When unions shine

  1. Saving memory when you know only one field is ever active (embedded systems)
  2. Variant types: JSON values, AST nodes, protocol messages (with a tag)
  3. Inspecting representation: endianness, bit patterns
  4. Hardware register maps: the same register as bits and as a word
Modern alternatives: if you just need memory savings, a void* + size pair or C++'s std::variant / Rust's enum is safer. In C, a tagged union with wrapper functions is the pragmatic choice.

Challenges

Challenge 1: float bit pattern
Use union { float f; uint32_t u; } to print the 32-bit hex representation of f = 1.0f (IEEE-754 β†’ 0x3F800000).
Challenge 2: is_little_endian()
Implement the function from the text on your machine and verify. Most Mac/Linux/Windows desktops are little-endian.
Challenge 3: Tiny value type
Build a tagged union struct Value with int, float, and bool. Implement Value add(Value a, Value b): numeric types add, two bools OR; mismatched kinds print an error.
Challenge 4: RGBA color
Union of a 32-bit integer and four 8-bit bytes (R,G,B,A). Pack/unpack 0xRRGGBBAA through both views.

See also