Lesson Progress
0%
Lesson  ยท  Unions in C

Unions โ€” One Address, Many Interpretations

A struct gives every member its own private room in memory. A union gives every member the exact same room, and trusts you to remember which one is actually moved in. This lesson builds that idea from first principles, contrasts it directly against structures, and ends with two examples you won't find in a typical intro tutorial.

What a union is
Memory layout vs struct
Overwrite demonstration
Tagged union pattern
๐Ÿ“–

What Is a Union? โ€” The Formal Definition

A union is a user-defined data type, declared with the same syntax as a struct, whose members all share the exact same block of memory instead of each getting their own. At any given moment, a union holds a meaningful value for only one of its members โ€” writing to one member overwrites whatever bytes were stored for any other member.

Contrast this with a structure: every member of a struct gets its own separate memory, laid out one after another, and all members remain simultaneously valid and independently readable.

Propertystructunion
Memory per memberEach member gets its own spaceAll members share one space
Total sizeSum of all members (plus padding)Size of the largest member only
Valid members at onceAll members, simultaneouslyOnly one member at a time
Writing one memberLeaves all other members unaffectedOverwrites the bytes of every other member
Typical useGrouping related, independent fields (a Student's roll, name, marks โ€” all needed together)Representing "one value that could be one of several types" (a Value that's either an int OR a float, never both)

The syntax is deliberately almost identical to a struct โ€” this is intentional, since the C designers wanted the two features to feel like close relatives:

syntax_comparison.c
syntax_comparison.c
C
struct Point {      // STRUCT โ€” x and y both exist at once
    int x;
    int y;
};

union Data {       // UNION โ€” only one of these is meaningful at a time
    int   i;
    float f;
    char  c;
};
๐Ÿ’ก Why would you ever want this? Memory efficiency and representing "variant" data โ€” a single value that is sometimes an int, sometimes a float, but never needs to be both simultaneously. Embedded systems, low-level device drivers, and interpreters for other languages use unions constantly for exactly this reason.
memory layout
1

Memory Layout โ€” Seeing the Difference Visually

Take the exact same three members โ€” int i, float f, char c โ€” and place them in a struct versus a union. This is where the real difference becomes impossible to miss.

struct Data โ€” each member gets its own 4 bytes (char is padded to align nicely) โ†’ total โ‰ˆ 9-12 bytes
struct Data
int i
0-3
float f
4-7
char c
8
union Data โ€” all three members overlap the SAME 4 bytes โ†’ total = 4 bytes (the size of the largest member, float)
union Data
int ifloat fchar c
0-3 (shared)

This single picture explains almost everything unusual about unions: because i, f, and c

example 1
2

Example 1 โ€” Proving the Overlap (sizeof & Overwrite Demo)

Struct vs union, side by side

This example declares a struct and a union with identical members, prints both sizes to confirm the theory above, then demonstrates the union's defining behavior: writing to one member destroys the value previously stored in another.

struct_vs_union_memory.c
struct_vs_union_memory.c
C
#include <stdio.h>

struct DataStruct {
    int   i;
    float f;
    char  c;
};

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

int main() {
    struct DataStruct s;
    union DataUnion u;

    printf("sizeof(struct DataStruct) = %zu bytes\n", sizeof(s));
    printf("sizeof(union DataUnion)   = %zu bytes\n\n", sizeof(u));

    // --- In the struct, all three fields hold independent values ---
    s.i = 65;
    s.f = 3.14;
    s.c = 'Z';
    printf("struct -> i=%d, f=%.2f, c=%c   (all three remain valid)\n\n", s.i, s.f, s.c);

    // --- In the union, writing i overwrites whatever was in f and c ---
    u.i = 65;
    printf("After u.i = 65   -> u.i = %d\n", u.i);

    u.f = 3.14;   // this OVERWRITES the same 4 bytes u.i was using
    printf("After u.f = 3.14 -> u.f = %.2f, but u.i is now garbage: %d\n", u.f, u.i);

    u.c = 'Z';    // this overwrites the first byte of whatever u.f held
    printf("After u.c = 'Z' -> u.c = %c, but u.f is now garbage: %f\n", u.c, u.f);

    return 0;
}
terminal โ€” typical 64-bit output
output
sizeof(struct DataStruct) = 12 bytes
sizeof(union DataUnion)   = 4 bytes

struct -> i=65, f=3.14, c=Z   (all three remain valid)

After u.i = 65   -> u.i = 65
After u.f = 3.14 -> u.f = 3.14, but u.i is now garbage: 1078523331
After u.c = 'Z' -> u.c = Z, but u.f is now garbage: 3.209386
โš ๏ธ This is not a bug โ€” it's the entire feature. A union never warns you when you read the "wrong" member; it always hands back whatever raw bytes currently sit in that shared memory, reinterpreted according to whichever type you asked for. The responsibility to track which member is currently valid belongs entirely to your program.
example 2
3

Example 2 โ€” The Tagged Union (Real-World Pattern)

How unions are actually used in production code

Raw unions are dangerous alone โ€” nothing stops you from writing an int and reading it back as a float by mistake. The fix, used constantly in real compilers, interpreters, and network protocol code, is the tagged union: pair the union with an extra field (the "tag") that records which member is currently valid.

tagged_union.c
tagged_union.c
C
#include <stdio.h>

// The tag: an enum recording which member is currently valid
typedef enum { TYPE_INT, TYPE_FLOAT, TYPE_STRING } ValueType;

// The union itself: only ONE of these three is meaningful at a time
typedef union {
    int   asInt;
    float asFloat;
    char  asString[20];
} ValueData;

// The tagged union: pairs the tag with the data so we always know which member to read
typedef struct {
    ValueType type;   // which member is valid right now
    ValueData data;   // the actual shared storage
} Value;

void printValue(Value v) {
    switch (v.type) {   // the tag tells us EXACTLY which member to read
        case TYPE_INT:
            printf("Integer: %d\n", v.data.asInt);
            break;
        case TYPE_FLOAT:
            printf("Float: %.2f\n", v.data.asFloat);
            break;
        case TYPE_STRING:
            printf("String: %s\n", v.data.asString);
            break;
    }
}

int main() {
    Value values[3];

    values[0].type = TYPE_INT;
    values[0].data.asInt = 42;

    values[1].type = TYPE_FLOAT;
    values[1].data.asFloat = 3.14159;

    values[2].type = TYPE_STRING;
    for (int i = 0; values[2].data.asString[i] != '\0' && i < 19; i++);   // (see note below)

    printf("A mixed list of 3 self-describing values:\n");
    printValue(values[0]);
    printValue(values[1]);

    // simpler and correct way to set the string member:
    sprintf(values[2].data.asString, "Hello");
    printValue(values[2]);

    printf("\nsizeof(Value) = %zu bytes (tag + largest member, not the sum of all 3)\n", sizeof(Value));

    return 0;
}
terminal
output
A mixed list of 3 self-describing values:
Integer: 42
Float: 3.14
String: Hello

sizeof(Value) = 28 bytes (tag + largest member, not the sum of all 3)
๐Ÿ’ก This IS how real variant types work. Scripting language interpreters (a variable that could hold a number, string, or object), JSON parsers, and many struct-based "event" systems in game engines all use exactly this tagged-union pattern under the hood: one enum saying "what kind," one union holding "the actual value," combined into a single struct.

theory recap
4

When to Reach for a struct vs a union

The deciding question is always the same: do all these pieces of data need to exist and be readable at the same time, or is it really just one value that happens to come in different possible shapes?

ScenarioUseWhy
A student's roll number, name, and marksstructAll three fields are needed together, at all times
A configuration value that's either an int, float, or stringunion (tagged)Only ever one type at a time โ€” no need for three separate fields sitting mostly unused
A network packet header interpreted differently by protocol typeunionSame raw bytes, different meaning depending on context
An RGB color's red, green, and blue componentsstructAll three components are always needed simultaneously to describe one color
Memory-constrained embedded firmware storing one of several sensor readingsunionSaves memory โ€” only one sensor's data needs storage at any moment
โš ๏ธ Common mistake: reaching for a union purely to "save memory" on a struct where every field really is needed at once. That's not what unions are for โ€” it will just silently corrupt your data as different fields get read and written. Use a union only when the fields are genuinely mutually exclusive.
quiz
Q

Quick Quiz

Question 1 of 5

What is the size of a union, compared to its members?

Question 2 of 5

In a union with int i and float f, what happens if you set u.i = 10 and then u.f = 2.5?

Question 3 of 5

What is a "tagged union"?

Question 4 of 5

Which scenario is a better fit for a struct than a union?

Question 5 of 5

Why doesn't a union "warn" you when you read the wrong member?

โœ“

Lesson Checklist

  • I can define a union in my own words, contrasted against a struct
  • I understand why sizeof(union) equals its largest member's size
  • I can explain why writing one union member corrupts the others
  • I can declare and use a union with multiple member types
  • I understand what a tagged union is and why it's used
  • I can build a simple tagged union with an enum + union + struct
  • I can decide when a scenario calls for a struct vs a union
  • I completed the quiz