What is Union? Memory Sharing & How It Differs from Struct

AdSense Placeholder (Header)
🗂️

What you'll learn in this post

What a union is, why it exists, how it shares memory, how to use it in C, how it compares to struct and enum, real-world examples, and common gotchas.

🤔 What Problem Does Union Solve?

Imagine you're writing firmware for a tiny microcontroller — a chip with very limited RAM. You need a variable that can hold either a temperature reading (a float), a sensor ID (an int), or a status flag (a char) — but never all three at once.

With a regular struct, the compiler reserves memory for all three fields simultaneously, even if you're only ever using one at a time. On a microcontroller with 2 KB of RAM, that kind of waste adds up fast.

💡
The Core Problem Sometimes you need a variable that can hold one of several different types, but never more than one at a time. Storing all of them wastes memory. Union was designed exactly for this situation.

📖 What Exactly Is a Union?

A union is a user-defined data type in C that lets multiple members share the same block of memory. You declare it just like a struct, but the compiler only allocates enough space for the largest member. All other members fit inside that same space.

This means at any point in time, a union variable holds exactly one value — whichever member you last wrote to. Writing to a different member overwrites whatever was there before.

🎯
Simple Definition A union is a memory-efficient container where all members sit at the same address. The size of the union equals the size of its largest member. Only one member holds valid data at any given time.

✏️ Syntax & Declaration

The syntax is almost identical to a struct — just swap the keyword:

union_syntax.c C
union UnionName {
    type1 member1;
    type2 member2;
    type3 member3;
};

Let's make a real example — a union that can hold an int, a float, or a char:

data_union.c C
// Define the union
union Data {
    int   i;
    float f;
    char  c;
};

int main() {
    union Data d;

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

    d.f = 3.14f;               // ⚠️ overwrites d.i
    printf("float: %.2f\n", d.f); // ✅ 3.14

    d.c = 'Z';                 // ⚠️ overwrites d.f
    printf("char : %c\n", d.c);  // ✅ Z

    printf("Size : %zu bytes\n", sizeof(d));  // 4 (size of float/int)

    return 0;
}

Notice how you use the dot . operator to access members — exactly like a struct. The difference is entirely in what happens under the hood in memory.

🧠 How Union Memory Works (The Key Insight)

This is the single most important thing to understand about unions. Let's compare a struct and a union with the exact same three members:

Memory Layout Comparison

struct Data (12 bytes total)
int
4 B
float
4 B
char
1+3 B
Each member gets its own space
union Data (4 bytes total)
int
float
char
4 B
All members share one block

The struct needs 12 bytes (4 for int + 4 for float + 1 for char + 3 padding). The union needs just 4 bytes — the size of the largest member. That's a 3× reduction in memory for this simple case.

size_comparison.c C
// Same three members — very different sizes
struct SData { int i; float f; char c; };
union  UData { int i; float f; char c; };

printf("struct size: %zu\n", sizeof(struct SData)); // 12
printf("union  size: %zu\n", sizeof(union  UData)); // 4
⚠️
The Trade-Off A union saves memory, but you can only reliably read the member you most recently wrote. Reading a different member after writing another gives you garbage — or reinterpreted bytes, which is sometimes intentional (more on that below).

🛠️ Using typedef with Union

Just like with structs, typing union before every variable declaration gets tedious. The typedef pattern cleans this up instantly:

typedef_union.c C
// Without typedef — verbose every time
union Sensor { int id; float temp; char status; };
union Sensor s1;   // must write "union Sensor"

// With typedef — much cleaner
typedef union {
    int   id;
    float temp;
    char  status;
} Sensor;

Sensor s1;    // clean and concise
s1.temp = 36.6f;

Professional C codebases almost always use typedef union. It makes your code look and feel like a proper custom type — which is exactly what it is.

🌍 Real-World Use Cases

Unions aren't just textbook theory — they appear in real production code, especially anywhere hardware or network protocols are involved.

🔌

Embedded / IoT

Store different sensor readings (temperature, pressure, voltage) in one compact variable.

🌐

Network Packets

Parse protocol headers where the same bytes mean different things depending on the packet type.

⚙️

Hardware Registers

Access the same register as a whole integer or as individual bytes/bits.

🎨

Type Punning

Reinterpret the raw bytes of one type as another — e.g., float bits as an int for fast tricks.

Here's a real-world embedded example — a sensor reading union:

sensor.c C
typedef enum { TEMP, PRESSURE, VOLTAGE } SensorType;

typedef struct {
    SensorType type;   // what kind of reading?
    union {             // the reading itself (anonymous union)
        float temperature;
        float pressure;
        int   voltage_mv;
    } value;
} SensorReading;

void printReading(SensorReading r) {
    switch (r.type) {
        case TEMP:     printf("Temp: %.1f°C\n",   r.value.temperature); break;
        case PRESSURE: printf("Pres: %.2f bar\n", r.value.pressure);    break;
        case VOLTAGE:  printf("Volt: %d mV\n",    r.value.voltage_mv);  break;
    }
}

int main() {
    SensorReading s;

    s.type            = TEMP;
    s.value.temperature = 36.6f;
    printReading(s);   // Temp: 36.6°C

    s.type           = VOLTAGE;
    s.value.voltage_mv = 3300;
    printReading(s);   // Volt: 3300 mV

    return 0;
}

This pattern — a struct that holds a type tag alongside a union — is called a tagged union (or discriminated union). It's one of the most powerful and practical patterns in C.

Pro Tip: Tagged Union Always pair a union with an enum (or int) "type tag" that records which member is currently active. Without this, you or your team might accidentally read the wrong member — and the compiler won't warn you.

⚔️ Union vs Struct vs Enum — Side by Side

C has three main user-defined data types and they're often confused, especially by beginners. Here's the clearest breakdown:

struct Structure
  • Each member has its own memory
  • All values stored simultaneously
  • Size = sum of all members
  • Best for grouping related data
union Union
  • All members share one memory block
  • Only one value valid at a time
  • Size = size of largest member
  • Best for memory-efficient alternatives
enum Enumeration
  • Named integer constants
  • No runtime data storage per se
  • Size = size of int
  • Best for named states / flags

Here's a code-level side-by-side comparison of all three:

all_three.c C
/* ── ENUM: named constants, no data ── */
enum Color { RED, GREEN, BLUE };

/* ── STRUCT: all members stored, 12 bytes ── */
struct Pixel_S {
    int  x;          // 4 bytes
    int  y;          // 4 bytes
    char label;     // 1 byte (+ 3 padding)
};                  // total: 12 bytes

/* ── UNION: only one member valid, 4 bytes ── */
union Pixel_U {
    int  x;          // 4 bytes
    int  y;          // 4 bytes (same slot as x!)
    char label;     // 1 byte (same slot!)
};                  // total: 4 bytes

int main() {
    printf("%zu\n", sizeof(struct Pixel_S)); // 12
    printf("%zu\n", sizeof(union  Pixel_U)); // 4
    return 0;
}
Feature union struct
Memory allocation Shared — all members overlap Separate — each member owns space
Size = Size of largest member = Sum of all members (+ padding)
Active members Only 1 at a time All, simultaneously
Use when… You need one of several types You need all fields together
Memory efficiency ✅ Excellent ⚠️ Higher usage
Safety ⚠️ Easy to misread members ✅ All members always valid

🐛 Common Mistakes & Gotchas

1. Reading a member you didn't write last

This is the #1 union mistake. After you write to d.f, reading d.i gives you the raw bytes of that float reinterpreted as an int — which is almost certainly not what you want.

gotcha1.c C
union Data { int i; float f; };
union Data d;

d.f = 3.14f;
printf("%d\n", d.i); // ❌ garbage! You wrote f, not i
printf("%.2f\n", d.f); // ✅ correct — last written member

2. Forgetting to track the active member

A union by itself has no idea which member is currently valid. Always pair your union with a type tag (an enum or int) that explicitly records what's currently stored. This is the tagged union pattern shown earlier.

3. Initializing only the first member

In C, you can only initialize the first member of a union in a brace-initializer. Trying to initialize a later member with a designated initializer may not behave as expected in older compilers.

init.c C
union Data { int i; float f; };

union Data d1 = { 10 };         // ✅ initializes d1.i = 10
union Data d2 = { .f = 3.14f }; // ✅ C99 designated initializer
🚨
Never Assume Both Members Are Valid After any write to a union, treat all other members as invalid. This is not like a struct where every field has its own safe slot. One write, one valid value — that's the contract.

📌 Quick Summary

  • A union is a user-defined type where all members share the same memory location.
  • Its size equals the size of its largest member — not the sum.
  • Only one member is valid at a time — writing to one overwrites the others.
  • Use unions when you need to hold one of several types and memory is tight.
  • Always combine a union with a type tag (enum) to know which member is active — this is the tagged union pattern.
  • Access members with . (dot) for variables and -> for pointers, just like struct.
  • Use typedef union to avoid repeating the union keyword on every declaration.
  • Unions shine in embedded systems, network protocol parsing, and hardware register access.