Part of C in 100s

C in 100 Seconds: Unions — Shared Memory | Episode 25

Celest KimCelest Kim

Video: C in 100 Seconds: Unions — Shared Memory One Field at a Time | Episode 25 by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

C Unions: Shared Memory, One Field at a Time

A union is like a struct, but all fields share the same memory. sizeof(union) is the size of the largest field. Used for variant types and low-level binary parsing.

Unions look like structs but with a twist: only one field is "live" at a time. Reading a field other than the last one written returns garbage (or, occasionally, useful reinterpretation).

The basic shape

#include <stdio.h>

union Value {
  int i;
  float f;
  char c;
};

int main() {
  union Value v;

  v.i = 42;
  printf("int:   %d\n", v.i);

  v.f = 3.14;
  printf("float: %.2f\n", v.f);
  printf("int after float: %d (garbage)\n", v.i);

  printf("\nsizeof union: %lu\n", sizeof(union Value));   // 4 (size of largest)
  printf("sizeof int:   %lu\n", sizeof(int));              // 4
  printf("sizeof float: %lu\n", sizeof(float));            // 4

  return 0;
}

Same memory, multiple views

union Value v;
v.i = 42;       // writes 4 bytes
v.f = 3.14;     // overwrites the same 4 bytes
v.i;            // now reads the bytes of 3.14 as if they were an int (garbage value)

In a struct, each field has its own memory. In a union, all fields overlap — they live at the same address.

struct Both { int i; float f; };   // 8 bytes (4 + 4, possibly + padding)
union Either { int i; float f; };  // 4 bytes (max of the two)

Variant types (tagged unions)

The most common real use:

typedef enum { TYPE_INT, TYPE_FLOAT, TYPE_STRING } ValueType;

typedef struct {
  ValueType type;
  union {
    int i;
    float f;
    char *s;
  } as;
} Value;

void print(Value v) {
  switch (v.type) {
    case TYPE_INT: printf("int: %d\n", v.as.i); break;
    case TYPE_FLOAT: printf("float: %.2f\n", v.as.f); break;
    case TYPE_STRING: printf("string: %s\n", v.as.s); break;
  }
}

Value v = {.type = TYPE_INT, .as.i = 42};
print(v);

The type tag tells you which union field is live. The union as saves space — only one of the three is allocated, not all three.

This pattern is how interpreters represent dynamically-typed values, how database row formats work, etc.

Low-level reinterpretation

union FloatBits {
  float f;
  uint32_t u;
};

union FloatBits b = { .f = 3.14 };
printf("0x%x\n", b.u);   // bytes of 3.14 as hex

You can write a float and read it as an int — bypassing the type system. Used for:

  • Reading IEEE-754 floating-point bits.
  • Endian-neutral byte access.
  • Hash functions on float values.

This used to violate strict-aliasing rules in some contexts; the C committee added a special exception for type-punning via unions in C11. (Older C, you'd use memcpy instead.)

Anonymous unions

C11 added anonymous unions:

struct Tagged {
  int type;
  union {
    int i;
    float f;
  };   // no name — fields are accessed directly
};

struct Tagged t;
t.i = 42;        // direct, no .as. prefix
t.f = 3.14;      // same

Cleaner than the named-union style. Available since C11; check compiler support.

Memory layout

union {
  char c;     // 1 byte
  int i;      // 4 bytes
  double d;   // 8 bytes
} v;

sizeof(v)   // 8 — size of largest field

The union is exactly the size of its biggest field (plus alignment padding). All fields start at the same offset (0).

For mixed-size fields, smaller writes leave the upper bytes of larger fields as whatever was there before. Reading the larger field after a smaller write gives partial garbage:

union { char c; int i; } v;
v.i = 0x12345678;
v.c = 'A';     // overwrites just the first byte
// v.i is now 0x12345641 (or 0x41345678 on big-endian)

When to use

  • Tagged unions / variants. Save space when only one of N values is needed.
  • Binary protocols. Parsing fixed-size records where one field is one of several types.
  • Hardware register access. Memory-mapped registers viewed as struct or as raw int.
  • Type-punning. Float bits, etc.

When not to use

  • "I want to save space and don't need both at once." Modern compilers and big RAM make this premature optimization. A struct is clearer.
  • "I want polymorphism." Better in C++ via inheritance, or via function pointers in C (episode 27).

Common mistakes

Reading the wrong field. Writing v.f = 3.14 and reading v.i gives the bit pattern of 3.14, not 3 or 0. Misuse — always know which field is "live."

No type tag. Without a tag, the union itself doesn't know which field is current. Always pair with an enum or other tracker.

Different sizes. A union of int and double — writing int only sets 4 of the 8 bytes; the other 4 keep whatever was there.

Memory copying with memcpy. memcpy(&u1, &u2, sizeof(u1)) copies all bytes — including the bytes that aren't part of the live field. May leak garbage into u1's "active" field. Usually OK for unions of equal-sized fields.

Strict-aliasing in older C. Pre-C11, type-punning via unions was technically undefined. Use memcpy for portable bit-reinterpretation.

What's next

Episode 26: typedef. Custom type names. The typedef we've been using since episode 23, in detail.

Recap

union { type1 a; type2 b; ... } — all fields share memory; size = largest field. Reading a non-live field is undefined. Use for: variant types (paired with enum tag), low-level binary access, type-punning floats. Anonymous unions (C11+) skip the field-prefix. Always know which field is live; always pair with a type tag for variants.

Next episode: typedef.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.