Unions in C: Syntax, Shared Memory and Worked Examples

A C union reuses one storage region for alternative member types, while a separate enum tag keeps each payload read consistent. Safe member switching prevents stale reads, layout mistakes, and character-array errors.

KnowledgeGate Team

Exam prep & CS education

Updated 15 Sep 20265 min read

A union resembles a structure, but its members reuse one storage region instead of living side by side. Only the member your program most recently assigned holds a meaningful value, and a tagged-union pattern records which one that is.

What a union in C actually stores

Consider this declaration:

c
union Reading {
    int count;
    float volts;
    char status[8];
};

count, volts, and status begin at the same location. They are three uses of one payload area, not independent fields.

sizeof(union Reading) is large enough for its largest member and may include alignment padding. Its exact value is implementation-dependent, so assuming eight bytes is not portable. Structure members have separate storage.

Initialisation chooses the first intended value. union Reading r = { .count = 25 }; selects count, while an initialiser without a designator targets the union's first named member. A later assignment reuses the same bytes for another member. Do not use an old member as a portable conversion trick; keep the selected member explicit.

Use a union when one object may hold one of several alternatives. Build the foundations through the C Language Course: Concepts, MCQs & Coding, then follow the wider Coding & DSA Courses for Placements.

Declaring, initialising and switching the active member

One union can move through three intended active members:

c
#include <stdio.h>
#include <string.h>

union Reading {
    int count;
    float volts;
    char status[8];
};

int main(void) {
    union Reading r = { .count = 25 };
    union Reading *rp = &r;
    int through_pointer = rp->count;

    printf("count=%d\n", r.count);
    (void)through_pointer;

    r.volts = 3.5f;
    printf("volts=%.1f\n", r.volts);

    snprintf(r.status, sizeof r.status, "%s", "OK");
    printf("status=%s\n", r.status);
    return 0;
}

The program prints:

Code
count=25
volts=3.5
status=OK

The designated initialiser selects count, and r.count uses dot access. While it is active, rp->count shows arrow access through union Reading *rp = &r. Assigning r.volts selects volts. The bounded snprintf selects status. Earlier logical values do not survive these switches.

Three views of one shared union Reading storage box: active member count 25, then volts 3.5, then status OK.

Fully worked example: build a safe tagged union

A separate tag records which payload may be read:

c
#include <stdio.h>

enum ValueKind { VALUE_INT, VALUE_FLOAT };

struct Value {
    enum ValueKind kind;
    union { int i; float f; } data;
};

void print_value(const struct Value *v) {
    switch (v->kind) {
    case VALUE_INT:
        printf("integer=%d\n", v->data.i);
        break;
    case VALUE_FLOAT:
        printf("float=%.2f\n", v->data.f);
        break;
    default:
        printf("invalid kind\n");
    }
}

int main(void) {
    struct Value a = { .kind = VALUE_INT, .data.i = 27 };
    struct Value b = { .kind = VALUE_FLOAT, .data.f = 3.5f };
    print_value(&a);
    print_value(&b);
    return 0;
}

The program prints:

Code
integer=27
float=3.50

In the first call, a.kind selects a.data.i, which is 27. In the second, b.kind selects b.data.f, which formats as 3.50. The tag makes the selection checkable.

This pattern fits tokens, messages, configuration values, and syntax-tree nodes with one payload form at a time. It does not provide C++ std::variant safety automatically. Code must keep tag and payload in sync.

Tagged union struct Value in two states: kind VALUE_INT with data.i 27, then kind VALUE_FLOAT with data.f 3.5.

Union versus structure: prove the difference with values

Run this comparison:

c
#include <stdio.h>

struct Pair { int x; int y; };
union Choice { int x; int y; };

int main(void) {
    struct Pair p = { 4, 9 };
    printf("p.x=%d p.y=%d\n", p.x, p.y);

    union Choice c = { .x = 4 };
    printf("c.x=%d\n", c.x);
    c.y = 9;
    printf("c.y=%d\n", c.y);

    printf("pair=%zu choice=%zu\n", sizeof p, sizeof c);
    return 0;
}

The first lines are p.x=4 p.y=9, c.x=4, and c.y=9. After writing c.y, do not predict c.x. Record the compiler-specific size line without generalising it.

Property

Structure

Union

Storage

Members do not overlap

Members start at the same address

Meaningful values

All members can be meaningful together

One selected member is meaningful at a time

Size rule

Separate members plus possible padding

At least as large as every member, plus possible alignment padding

Suitable use

A record with simultaneous fields

One of several alternative payloads

The surrounding sequence is covered in C Programming & Data Structures.

Common union errors and how to repair them

  1. Stale member read. After r.count = 25 and r.volts = 3.5f, code still expects 25. The later write reused the storage. Read only the selected member, with a tag when needed.

  2. Tag-payload mismatch. kind says VALUE_INT after code assigns data.f = 3.5f. Use small constructors such as make_int(27) and make_float(3.5f) that update both fields and return a consistent object.

  3. Non-portable layout assumption. Code hard-codes size or byte order. Use sizeof, _Alignof when needed, fixed-width integers when width matters, and explicit serialisation instead of dumping raw union bytes.

  4. Unsafe character-array handling. status[8] cannot be assigned with = after declaration, and unchecked copying can overflow it. Use snprintf(r.status, sizeof r.status, "%s", "OK"). Match formats too: %d is wrong for a floating value.

How exams and interviews test unions

Questions test shared storage, the last written member, portable size rules, and correct tag-payload pairs.

For union Exam { char code[5]; int score; double average; };, its size is at least its largest member, rounded for alignment. A numeric answer needs the given type sizes and alignment rules. Member sizes are not added.

For a safe trace, assign u.score = 18 and print score=18. Then assign u.average = 2.5 and print average=2.5. Mark score active after the first assignment and average active after the second. For teaching-recruitment preparation, see C Programming for Teaching CS Exams.

Three practice tasks with answer checks

  1. Trace union Code { int id; char label[6]; }; from .id = 314, then write "GATE" with snprintf. Check: first id=314, then label=GATE. Reading id afterward cannot recover 314.

  2. Define enum ResultKind { RESULT_MARKS, RESULT_GRADE }; and a tagged union containing either int marks or char grade. Create objects holding 86 and 'A'. Check: a tag-aware print function must produce marks=86 and grade=A.

  3. Without running it, analyse union Buffer { int n; double d; char text[12]; };. Check: all members begin at the union's start and sizeof(union Buffer) >= 12; the exact size and padding are implementation-dependent. Compile a sizeof and _Alignof probe locally, then record rather than generalise the result.

Short version and next step

  • Union members share storage.

  • One member is intentionally active at a time.

  • The largest member and alignment determine required storage.

  • . and -> access members.

  • A separate tag makes alternative payloads safer.

Check sizeof on the actual target instead of memorising a universal number. Compile the tagged-union program with warnings enabled, change 27 and 3.5 to 42 and 6.25, and confirm integer=42 and float=6.25. Then attempt the three tasks. Coding for Placements: C, C++, Java, Python is the broader practice route from C syntax into coding and data-structure problems.