Skip to content
VLSI Mentor

C Part I · Module 10 · Structures, Unions, Enums and User-Defined Types

Unions and Shared Storage

A record whose every part is present at once is only half of what real data looks like. The other half is a thing that is one of several possibilities and never more than one at a time: a message that is a status or an error or a sample, a token that is a number or a name. Declaring that as a structure pays for every alternative you did not take. C's answer is a construct whose members all begin at the same address and share one storage area, which saves exactly what you would expect and introduces exactly one hard problem: nothing in the object records which member is currently valid. It also raises a question that attracts more confident misinformation than almost anything else in C, namely what happens when you read a member you did not write. That question has a precise answer in C, a different answer in C++, and an answer that changed in 2007.

Advanced21 min readCPart IFundamentalsTypesMemory

Part I · Module 10 · Chapter 10.7 · Unions and Shared Storage

1. The Declaration, and the One Difference

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    union value {
        int    i;
        double d;
        char   tag[4];
    };

The syntax is a structure's with union in place of struct. Everything from Chapter 10.2 carries over: the tag is not a type name (the type is union value), the trailing semicolon is required, members are reached with . and ->, there are three declaration forms, and the tag has scope.

The difference is where the members live:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
union value : sizeof  8  _Alignof 8   offsets i=0 d=0 tag=0
struct same : sizeof 24  _Alignof 8   offsets i=0 d=8 tag=16

Every member of a union is at offset 0. They do not sit beside one another; they sit on top of one another, sharing the same bytes. A structure's size is roughly the sum of its members; a union's is the size of its largest.

The layout rules from Chapter 10.5 still apply, in their union form:

  • the union's alignment is the largest alignment among its members — 8 here, from the double;
  • the union's size is at least the largest member's size, rounded up to a multiple of that alignment.
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
union small { char a; char b; }   sizeof 1  _Alignof 1
union mixed { char c; int i;  }   sizeof 4  _Alignof 4
The three members of a union drawn against one shared storage area, one column per byte. The code member is an int occupying four bytes starting at offset zero. The measurement member is a double occupying eight bytes, also starting at offset zero. The label member is a char array of sixteen bytes, also starting at offset zero. Beneath them, the union itself is sixteen bytes, the size of its largest member. All three members begin at the same address and overlap completely; only one of them holds a value at any moment.codeint · 4 bytes ·offset 0measurementdouble · 8 bytes · offset 0labelchar[16] · 16 bytes · offset 0union payload16 bytes — as large as its largest member12
Figure 1 — three members against one storage area, one column per byte and all of them starting at offset 0. The structure version of these members would place them end to end and need 32 bytes; the union lays them on top of each other and needs 16, which is the size of the largest. The picture also shows the problem the rest of this chapter is about: the bottom bar is what actually exists in memory, and nothing in it records which of the three bars above is the one currently holding a value.

2. What It Is For

Azvya Education Pvt. Ltd.VLSI Mentor
the saving, measured
#include <stdio.h>

struct as_struct { int kind; int code; double measurement; char label[16]; };

union  payload   { int code; double measurement; char label[16]; };
struct as_union  { int kind; union payload p; };

int main(void)
{
    printf("all members present (struct): %zu bytes\n", sizeof(struct as_struct));
    printf("one at a time (tag + union) : %zu bytes\n", sizeof(struct as_union));
    printf("union payload alone         : %zu bytes\n", sizeof(union payload));
    return 0;
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
all members present (struct): 32 bytes
one at a time (tag + union) : 24 bytes
union payload alone         : 16 bytes

Eight bytes per record, a quarter of it, on a structure with three alternatives. The saving grows with the number of alternatives and with how different their sizes are — a message type with ten alternatives of which one is large pays for the large one only.

Use a union when the alternatives are genuinely exclusive — a message that is either a status or an error or a sample; a token that is either a number or an identifier. If two members can meaningfully hold values at the same time, they are not alternatives and a union is the wrong tool.

The second use is reading one storage area through two types, which §4 covers. The third use — making a structure match a byte layout someone else defined — is a real one and this chapter is not the place to judge it: that question involves padding, alignment and byte order together, and Chapter 14.5 is written for it.

3. The Hard Problem: Nothing Records Which Member Is Valid

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    union value v;
    v.i = 1;                    /* i is the stored member */
    v.d = 2.5;                  /* now d is */
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
after v.i = 1   : v.i = 1
after v.d = 2.5 : v.d = 2.5   (v.i is no longer the stored member)

The second assignment did not "also" set d. It overwrote the storage. At most one member's value is stored at any time — that is the standard's wording, and it is a statement about the object, not about your intentions.

And the object does not remember which. sizeof(union value) is 8 bytes of payload and zero bytes of bookkeeping. If a function receives a union value, there is no expression it can write to discover which member was last stored. The information does not exist.

A bare union is almost never the right thing to pass around. The fix is to carry the answer alongside it, which is §5's tagged union, and it is the only discipline that makes unions safe to use at a distance.

4. Reading a Member You Did Not Write

This is the question c.ts singles out, and it deserves to be answered exactly rather than with a rule of thumb.

Azvya Education Pvt. Ltd.VLSI Mentor
one storage area, two types
#include <stdio.h>

union view {
    unsigned int  n;
    unsigned char b[sizeof(unsigned int)];
};

int main(void)
{
    union view v;
    v.n = 0x01020304u;

    printf("wrote n = 0x%08x\n", v.n);
    printf("the same storage read as bytes:");
    for (size_t i = 0; i < sizeof v.b; i++) { printf(" %02x", v.b[i]); }
    printf("\n");

    v.b[0] = 0xff;                       /* change ONE byte */
    printf("after v.b[0] = 0xff, n = 0x%08x\n", v.n);

    return 0;
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
wrote n = 0x01020304
the same storage read as bytes: 04 03 02 01
after v.b[0] = 0xff, n = 0x010203ff

The bytes of the unsigned int were read through a different member, and then one of them was changed and the change was visible through the first member. The two members are the same storage, demonstrated both ways.

Which byte is which is not this chapter's subject. The order above is a property of the target, it is called byte order or endianness, and Chapter 14.3 is about it. What matters here is only that the same bytes are being read through a second type.

Now the rule.

5. The Tagged Union

The discipline that solves §3's problem is to put the answer in the object, next to the union:

Azvya Education Pvt. Ltd.VLSI Mentor
a tag that records which member is valid
#include <stdio.h>

#define MSG_STATUS 0
#define MSG_ERROR  1
#define MSG_SAMPLE 2

struct message {
    int kind;                       /* which member of body is valid */
    union {
        int    status;
        char   error[12];
        double sample;
    } body;
};

static void print_message(const struct message *m)
{
    switch (m->kind) {
        case MSG_STATUS: printf("status %d\n", m->body.status);   break;
        case MSG_ERROR:  printf("error \"%s\"\n", m->body.error); break;
        case MSG_SAMPLE: printf("sample %g\n", m->body.sample);   break;
        default:         printf("unknown kind %d\n", m->kind);    break;
    }
}

/* one constructor per alternative: the tag cannot disagree with the body */
static struct message make_status(int s)
{
    struct message m = { .kind = MSG_STATUS };
    m.body.status = s;
    return m;
}

static struct message make_sample(double d)
{
    struct message m = { .kind = MSG_SAMPLE };
    m.body.sample = d;
    return m;
}

int main(void)
{
    struct message a = make_status(200);
    struct message b = make_sample(36.6);

    struct message c = { .kind = MSG_ERROR };
    for (int i = 0; i < 5; i++) { c.body.error[i] = "oops"[i]; }

    print_message(&a);
    print_message(&b);
    print_message(&c);

    printf("sizeof(struct message) = %zu, sizeof(body) = %zu\n",
           sizeof(struct message), sizeof(a.body));
    return 0;
}

Output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
status 200
sample 36.6
error "oops"
sizeof(struct message) = 24, sizeof(body) = 16

Four things in that program are the pattern rather than the example.

The union is declared inline, inside the structure, with no tag of its own. It is never used separately, so it does not need a name — Chapter 10.2 §3's untagged form, used deliberately for once.

Every read goes through a switch on the tag. The default case exists because kind is an int and can hold anything; §6 shows why that matters.

There is one constructor per alternative, and each one sets the tag and the body together. This is the whole trick: if the only way to build a message is through a function that sets both, the tag cannot be wrong. A caller who never touches body directly cannot create an inconsistent object.

The constructors return by value — Chapter 10.4 §6 measured that this is cheap and composes, and a 24-byte message is comfortably in the range where it is the right choice.

A note on the tag's type: int with #defined names is used here because Chapter 10.8 is the chapter that introduces a better tool for exactly this job, and it is the next one.

6. What Goes Wrong Without the Discipline

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    struct message bad = { .kind = MSG_SAMPLE };
    bad.body.status = 7;                      /* tag says sample, body holds an int */
    print_message(&bad);
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
tag and body disagree -> sample 3.45846e-323

The tag says sample, so print_message reads body.sample — eight bytes reinterpreted as a double. The int 7 occupies the first four, and the remaining four are zero because the declaration zero-initialised the whole object (Chapter 10.2 §5). The resulting bit pattern is a very small subnormal double, printed as 3.45846e-323.

Three observations, in order of importance.

This is not undefined behaviour. It is §4's defined reinterpretation, of an object every byte of which is determinate. It is a wrong answer, arrived at legally — which is worse than a crash, for all the reasons Chapter 9.12 §4 gave.

It would be undefined if the object had not been zero-initialised. Write struct message bad; with no initialiser and the upper four bytes are indeterminate, and reading them is Chapter 9.12's problem. The example's correctness as a demonstration depends on that initialiser.

The value is this implementation's, not a guarantee. What bit pattern 7-then-zeros means as a double is a property of the target's floating-point representation.

Nothing in the language will catch this. The compiler gave no warning: bad.body.status = 7; is a perfectly ordinary assignment to a perfectly ordinary member. The only defence is that no code outside the constructors ever writes to body — which is a convention, enforced by review, exactly like Chapter 9.8's ownership rules and for exactly the same reason.

7. One More Guarantee: The Common Initial Sequence

C gives unions a second, narrower guarantee that is worth knowing because it is the one case where reading "the wrong member" is not merely defined but intended.

If a union contains several structures that share a common initial sequence — a leading run of members with compatible types, in the same order — you may inspect that common part through any of them, anywhere the union's declaration is visible.

Azvya Education Pvt. Ltd.VLSI Mentor
inspecting the shared header through either member
#include <stdio.h>

struct header_only { int kind; int length; };
struct with_body   { int kind; int length; double value; };

union packet { struct header_only h; struct with_body b; };

int main(void)
{
    union packet p;
    p.b.kind   = 3;
    p.b.length = 16;
    p.b.value  = 1.5;

    printf("written through b: kind=%d length=%d value=%g\n",
           p.b.kind, p.b.length, p.b.value);
    printf("read through h   : kind=%d length=%d\n", p.h.kind, p.h.length);
    printf("sizeof union packet = %zu\n", sizeof(union packet));
    return 0;
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
written through b: kind=3 length=16 value=1.5
read through h   : kind=3 length=16
sizeof union packet = 16

kind and length are the common initial sequence of both structures, so reading them through h after writing through b is explicitly sanctioned — not reinterpretation, but the same members at the same offsets.

This is how C expresses "a family of record types that all start with a header". The header can be read first, inspected to decide which alternative this is, and then the right member used. The guarantee stops at the end of the common run: reading p.h tells you nothing about value, and there is no value in struct header_only to ask.

8. When Not to Use a Union

When the alternatives are not exclusive. If two members can both be meaningful at once, you want a structure. A union here is a bug waiting for the second one to be set.

When the saving is not real. A union of three ints saves nothing and costs you the tag. Measure sizeof both ways before restructuring anything.

When you are tempted to pass the bare union around. Without a tag it is unusable at a distance (§3). Bundle it with its tag in a structure and pass that.

When what you actually want is a byte layout defined elsewhere. That is a real need and a union is sometimes part of the answer, but the question also involves padding, alignment and byte order, and it is Chapter 14.5's.

One feature worth knowing exists without using it yet: C11 allows an anonymous union inside a structure, whose members are reached as if they were the structure's own (m.status instead of m.body.status). It is convenient and it hides which members share storage, which is exactly the thing a reader of a tagged union needs to see. Spell out body until you have a reason not to.

9. What You Should Be Able to Say Now

  1. Every member of a union is at offset 0 — measured, all three offsets zero — and they share one storage area.
  2. A union's size is its largest member's, rounded up to its alignment; its alignment is the largest member's.
  3. At most one member's value is stored at any time, and the object carries no record of which.
  4. Measured: the same three alternatives cost 32 bytes as a structure and 24 as a tag plus union.
  5. Use a union when the alternatives are genuinely exclusive.
  6. Reading a member other than the one last written is type punning, and in C it is defined — the bytes are reinterpreted (C17 6.5.2.3 footnote).
  7. That footnote arrived in C99 TC3, in 2007; C11, C17 and C23 carry it. Advice older than that may be wrong.
  8. In C++ it is undefined — this is a real C/C++ disagreement.
  9. Two caveats: the value is implementation-dependent, and reading a larger member reads bytes you never wrote (indeterminate — 9.12).
  10. A trap representation makes the read undefined; unsigned char has no trap representations, so a byte-array member is the safest target.
  11. The tagged union — a tag plus the union, one constructor per alternative — is the discipline that makes unions safe, and it is a convention, not an enforcement.
  12. The common initial sequence guarantee lets you read a shared leading header through any member structure.

10. Exercises

Exercise 1 — predict the sizes. Give sizeof and _Alignof for each, using Chapter 10.5 §2's numbers, then check.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
union a { char c; short s; };
union b { int i; char buf[10]; };
union c { double d; char buf[4]; };
union d { char x[3]; char y[3]; };

Exercise 2 — structure or union. For each, say which you would use and why.

  • a colour with red, green and blue components
  • a parsed token that is either an integer, a name, or an operator
  • a measurement with a value and a unit
  • a result that is either a value or an error code, never both

Exercise 3 — classify four claims. ISO C guarantee, implementation-dependent, undefined, or false?

  • a union's members all start at the same address
  • reading a union member other than the one written is undefined in C
  • reading a union member other than the one written is undefined in C++
  • a union remembers which member was last written

Exercise 4 — the oversized read. A union holds an int and a double. You write the int and read the double. Say exactly which bytes are defined, which are not, and what one change to the declaration of the object would make the whole read determinate.

Exercise 5 — build the tagged union. Write a tagged union for a value that is either a long, a double, or a 32-character name, with one constructor per alternative and one reader that switches on the tag. State the one rule a user of your type must follow.

Exercise 6 — find the inconsistency. This has a bug that produces a wrong answer with no warning and no undefined behaviour. Name it and give the fix that makes it unwritable.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
struct message m = { .kind = MSG_ERROR };
m.body.sample = 1.0;
print_message(&m);

Exercise 7 — the common initial sequence. Given §7's union packet, say which of these is guaranteed, and why.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
p.b.kind = 1;   int k = p.h.kind;
p.h.kind = 1;   double v = p.b.value;
p.b.value = 2;  int n = p.h.length;

Exercise 8 — the date question. A colleague cites a C book from 1995 saying union type punning is undefined. Explain what changed, when, and what you would check before relying on the change in a particular codebase.

11. Summary

  • All members at offset 0, sharing one storage area; size = largest member, rounded to the union's alignment.
  • At most one member's value is stored, and the object does not record which.
  • Measured: three alternatives cost 32 bytes as a structure, 24 as a tag plus union.
  • Use a union only when the alternatives are genuinely exclusive.
  • Type punning is defined in C — the bytes are reinterpreted (C17 6.5.2.3 footnote) — and undefined in C++.
  • The footnote arrived with C99 TC3 in 2007; older advice can be wrong.
  • The value is implementation-dependent, reading a larger member reads bytes you never wrote, and a trap representation makes the read undefined.
  • unsigned char has no trap representations — the safest punning target.
  • The tagged union with one constructor per alternative is the discipline; it is a convention, not an enforcement.
  • The common initial sequence may be read through any member structure.

The tag in §5 was an int with three #defined names, and that arrangement is doing a job badly in three separate ways. The names are not grouped — nothing says MSG_STATUS, MSG_ERROR and MSG_SAMPLE belong to one set. The numbering is maintained by hand, so inserting an alternative means renumbering. And the tag's type is int, which accepts every integer there is, so m.kind = 97; compiles happily and lands in the default case you were obliged to write. C has a construct designed precisely for a small set of named related constants, and the next chapter is about it — including the part that surprises people, which is how much less type checking it gives you than its appearance suggests.

Standards & specifications

Governing standard
ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)

Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C Programming curriculum.