Unions & Enumerations

A union lets several different members share the exact same memory, one at a time; an enum gives meaningful names to a set of integer constants. Both make code more expressive than raw structs and raw numbers alone.

C gives you two more user-defined types beyond the struct: the union and the enum, and they solve two genuinely different problems. A union lets several differently-typed members share one single block of memory, so that only one of them holds a meaningful value at any given moment — useful whenever a piece of data can be one of several alternative representations, never several at once. An enum, by contrast, is just a way to attach readable names to a set of integer constants, so a state, a mode, or a choice can be written as RUNNING or MONDAY instead of a bare 2 or 0. This page walks through both features in one pass — how a union is declared, why all of its members land at the same address, what goes wrong when you read the wrong member, and how pairing a union with an enum produces the classic "tagged union" pattern — then does the same for enums: their default numbering, explicit values, switch statements, and their real nature as ordinary integer constants underneath.

What Is a Union

A union is a user-defined type, declared with the union keyword, whose members do not get separate storage the way a struct's members do. Instead, every member is laid on top of the exact same block of memory, so the union only ever needs enough space for one member at a time.

This declares a union type named union Data with three members of different types. Unlike a struct built from the same members, this union does not reserve three separate slots — i, f, and c all occupy the same storage, one at a time.

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

Structure vs Union

The clearest way to understand a union is to place it next to a struct built from the exact same member list. A struct gives every member its own private storage, so all of its fields coexist and can hold independent values at once. A union collapses all of its members onto one shared storage area, so writing to one member changes what you'll read back from every other member too.

That single contrast is the entire idea worth remembering: struct members coexist, union members share storage. Everything else about how they're used — the dot operator on an object, the arrow operator through a pointer — is identical between the two.

struct Data                     union Data
(each member gets its own slot)  (members overlap in storage)

+----------------+                +------------------+
| int i          |                |                  |
+----------------+                |  shared storage  |
| float f        |                |  (int/float/char)|
+----------------+                |                  |
| char c         |                +------------------+
+----------------+

struct -> members coexist        union -> members share storage

Declaring a Union

A union is declared the same way a struct is, just with the union keyword in place of struct. The general form lists a tag name and a set of typed members inside braces.

The union Data declaration only creates a type; no memory exists until a variable of that type, such as data, is actually declared.

union UnionName
{
    data_type member1;
    data_type member2;
    data_type member3;
};

// Example:
union Data
{
    int integer;
    float decimal;
    char character;
};

// Declaring a union object:
union Data data;

Accessing Union Members

Union members are reached exactly the way struct members are: the dot operator (.) for a union object, and the arrow operator (->) for a pointer to a union.

There is nothing union-specific about this syntax — it follows exactly the same member-access rules that structs use.

union Data data;
data.integer = 100;
printf("%d\n", data.integer);

union Data *ptr = &data;
ptr->integer = 100;

// object.member
// pointer->member

Only One Value at a Time

Because a union's members share one block of storage, writing a new member overwrites whatever was previously stored — it does not add a second, independent value alongside the first.

After the second assignment, the int value 10 is no longer the currently stored value — d.f's bytes have replaced it entirely. A union is useful when different representations are alternatives, never values that need to exist at the same time.

It's tempting to treat a union like a struct and expect d.i and d.f to hold two independent values once both have been assigned. They don't — d.f overwrites the exact same bytes d.i used, so only the most recently written member's value is meaningful afterward.

union Data
{
    int i;
    float f;
};

union Data d;
d.i = 10;      // storage now holds the int representation of 10
d.f = 3.14f;   // the SAME storage is overwritten with the float 3.14

Size of a Union

A union's size only needs to be large enough to hold its biggest member, plus whatever padding the implementation needs to satisfy alignment — it is not the sum of every member's size the way a struct's size roughly is.

This union must provide enough storage for the largest member, double, subject to implementation-specific layout and alignment rules — not for int, double, and char added together. Unlike a struct, you should never think of a union as storing all of its members independently.

union Data
{
    int i;
    double d;
    char c;
};

printf("%zu\n", sizeof(union Data));

Union Members Share One Address

Because every member overlaps the same storage, taking the address of any member of a union object gives you the same address — the union's own starting address.

d.i, d.f, and d.c are three different names with three different types, but they all describe the very same address — 0x1000. The member types differ; the storage does not.

union Data d;
printf("%p\n", (void *)&d.i);
printf("%p\n", (void *)&d.f);
printf("%p\n", (void *)&d.c);
// all three printed addresses are identical

Initializing a Union

A union can be initialized at the point of declaration. A designated initializer makes it explicit which member is being initialized, which matters since only one member's worth of storage is being set up.

Both forms initialize the same storage, but the designated form (.i = 10) is generally clearer, since it states exactly which member's value 10 is meant to represent.

union Data d1 = { .i = 10 };   // clearly initializes i

union Data d2 = { 10 };        // initializes the first member, i, by position

Changing the Active Value

Every later assignment to a different member simply overwrites the shared storage again — there is no history kept of what was written before.

union Data
{
    int i;
    float f;
};

union Data d;
d.i = 100;    // i is now the most recently stored member
d.f = 25.5f;  // storage now contains the representation written through f
Write member A
      |
      v
union storage now holds A's value

Write member B
      |
      v
same storage is overwritten with B's value

Reading a Different Union Member Than Was Written

A common misunderstanding is assuming a union automatically converts one member's value into another member's type when you read it. It does not — reading through a different member reinterprets the same stored bit pattern using that member's type, which is a very different thing from a numeric conversion.

Certain forms of reading a different union member are permitted by C's rules for examining object representations, but the resulting value is implementation-dependent in the relevant cases. Portable code should not treat a union as a general-purpose numeric conversion mechanism — use an explicit cast when you actually want int-to-float conversion.

Reading d.f immediately after writing d.i does not turn 10 into 10.0f — it reinterprets the same stored bits as a float, which typically produces a nonsense-looking value rather than the numeric equivalent. If you want an actual conversion, write it explicitly: float f = (float)d.i;

union Data d;
d.i = 10;
printf("%f\n", d.f);   // NOT 10.0 -- this is not int-to-float conversion

// to get an actual conversion, convert explicitly:
float f = (float)d.i;   // f is genuinely 10.0

Using a Union for Alternative Data Representations

One genuinely useful role for a union is representing alternative forms of the same underlying data — a value that might be an integer sometimes and a floating-point number other times, depending on context.

A program can store either an integer or a floating-point value in this union depending on some associated piece of information — but the union itself does not remember which one it currently holds. That missing piece is exactly what a tagged union adds.

union Value
{
    int integer;
    float floating;
};

Tagged Unions

A union does not automatically remember which of its members currently holds the meaningful value — that information has to be stored separately, typically as an enum "tag" placed alongside the union inside a struct.

The type field tells the program which union member should currently be treated as meaningful — here, TYPE_INT says that data.integer is the one to read, not data.floating. This enum-plus-union combination is what's meant by a tagged (or discriminated) union, and it's a widely used pattern for representing values that can take on more than one type.

A plain union has no memory of which member you wrote most recently — nothing stops you from reading v.data.floating right after writing v.data.integer and getting garbage back. Pairing the union with an explicit tag, like the enum type field here, is what makes the active member knowable at all; without something like it, tracking which member is meaningful becomes entirely the programmer's responsibility.

enum ValueType
{
    TYPE_INT,
    TYPE_FLOAT
};

struct Value
{
    enum ValueType type;
    union
    {
        int integer;
        float floating;
    } data;
};

struct Value v;
v.type = TYPE_INT;
v.data.integer = 100;

Anonymous Unions

In some contexts, a union can be declared inside a struct without a tag name of its own. When that's allowed, its members become accessible directly through the enclosing struct, without an extra layer of naming.

Without the anonymous union, this would need an intermediate member name, such as s.data.raw. This is a more advanced language feature, so it's worth checking your compiler's and language standard's support for it when portability matters.

struct Sensor
{
    int id;
    union
    {
        int raw;
        float value;
    };
};

struct Sensor s;
s.id = 1;
s.raw = 500;    // instead of s.data.raw

A Union Member That Is a Struct

A union's member does not have to be a plain scalar type — it can be a whole struct, letting the union represent either a simple value or a compound one.

This union can represent either a plain integer value, or a Point with its own x and y fields — but never both at once, since value and point still share the same union storage.

struct Point
{
    int x;
    int y;
};

union Data
{
    int value;
    struct Point point;
};

Unions Containing Pointers

A union's members can also be pointers of different types, all sharing the same underlying storage the way any other union members do.

As with any union, the pointer values overlap in storage, so only the member that represents the currently intended value should be accessed, following the same design discipline and C rules that apply to every other union.

union Data
{
    int *int_ptr;
    float *float_ptr;
    void *generic_ptr;
};

Practical Example: A Device Measurement

A realistic use case ties the tagged-union idea to something concrete: a device that can report either an integer measurement or a floating-point one, depending on the sensor.

The enum identifies which interpretation is currently active, while the union supplies the shared storage that actually holds whichever kind of value that turns out to be. Checking m.type before reading m.value.floating is what keeps this safe.

#include <stdio.h>

enum ValueType
{
    VALUE_INT,
    VALUE_FLOAT
};

struct Measurement
{
    enum ValueType type;
    union
    {
        int integer;
        float floating;
    } value;
};

int main(void)
{
    struct Measurement m;
    m.type = VALUE_FLOAT;
    m.value.floating = 23.5f;

    if (m.type == VALUE_FLOAT)
    {
        printf("Value = %.2f\n", m.value.floating);
    }

    return 0;
}

What Is an Enumeration

An enumeration (enum) is a user-defined integer type made up of named constants, called enumerators. Instead of writing a bare number whose meaning has to be remembered separately, you write a name that says what it means.

MONDAY is a far more readable stand-in for 0 than the literal 0 itself, especially once a program is checking and comparing this value in several places.

enum Day
{
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY
};

// instead of:      int day = 0;
// we can write:     enum Day day = MONDAY;

Default Enumeration Values Start at Zero

If no values are written explicitly, the first enumerator is given the value 0, and each one after it counts up by one.

It's easy to assume enumerators start counting from 1, especially with a day-of-week list that intuitively feels one-based. C's enums start at 0 unless you say otherwise, so MONDAY above is 0, not 1.

enum Day
{
    MONDAY,      // 0
    TUESDAY,     // 1
    WEDNESDAY,   // 2
    THURSDAY,    // 3
    FRIDAY       // 4
};

Assigning Explicit Enumeration Values

Any enumerator can be given an explicit value, which is especially useful when the values carry a specific meaning or need to match an external specification such as a protocol or error-code table.

SUCCESS, ERROR_TIMEOUT, ERROR_INVALID, and ERROR_BUSY now map to exactly 0, 10, 20, and 30 respectively, matching whatever external meaning those specific numbers are supposed to carry.

enum ErrorCode
{
    SUCCESS = 0,
    ERROR_TIMEOUT = 10,
    ERROR_INVALID = 20,
    ERROR_BUSY = 30
};

Repeated and Mixed Enumeration Values

Enumeration values do not need to be consecutive, and more than one enumerator is allowed to share the exact same value.

OK and SUCCESS represent the exact same integer value, 0 — which is legal, and sometimes convenient when two names are meant to be interchangeable synonyms for the same outcome.

enum State
{
    STATE_IDLE = 0,
    STATE_START = 5,
    STATE_RUNNING = 10,
    STATE_STOP = 20
};

enum Result
{
    OK = 0,
    SUCCESS = 0,   // same value as OK
    FAILED = 1
};

Declaring and Using Enum Variables

Once an enum type is defined, you can declare variables of that type and assign or compare them using the named enumerators directly.

enum Day
{
    MONDAY,
    TUESDAY,
    WEDNESDAY
};

enum Day today;
today = TUESDAY;

if (today == TUESDAY)
{
    printf("Tuesday\n");
}

Enums With switch

Enumerations pair especially well with switch statements — each case can be written against a named constant instead of a raw number, so the logic reads like a description of the states involved.

Compare this to the equivalent written with bare numbers — case 0:, case 1:, case 2: — which behaves identically but forces a reader to look up what each number means.

case 0: / case 1: / case 2: compiles and runs exactly like the enum version above, but a reader — including future you — has to go look up what each number stands for. Prefer comparing against the named constant, if (state == STATE_RUNNING) rather than if (state == 2), so the code documents its own meaning.

enum State
{
    IDLE,
    RUNNING,
    ERROR
};

enum State state = RUNNING;

switch (state)
{
    case IDLE:
        printf("Idle\n");
        break;
    case RUNNING:
        printf("Running\n");
        break;
    case ERROR:
        printf("Error\n");
        break;
    default:
        printf("Unknown state\n");
        break;
}

Enum Constants Are Just Integers

Enumeration constants such as IDLE, RUNNING, and ERROR are, underneath the readable names, ordinary integer constants. The specific integer type the compiler picks to represent an enumeration is determined by the implementation, subject to the language's rules and the range of values the enum needs to represent.

Don't assume every enum is silently just an int under the hood. The C standard only guarantees the compiler picks some integer type able to represent all of the enumeration's values, so sizeof(enum State) is implementation-defined — check it with sizeof if the exact size matters, rather than assuming it equals sizeof(int).

// avoid assuming this is universally guaranteed:
// sizeof(enum State) == sizeof(int)

printf("%zu\n", sizeof(enum State));   // check the actual size instead

Enum Values in Arithmetic

Because enumeration constants are integer constants, they can take part in ordinary integer expressions just like any int value.

This compiles and works fine, but enumeration types are meant primarily to express a meaningful set of named states or choices — leaning on them as a general substitute for arbitrary integers everywhere tends to make code harder to read, not easier.

enum Priority
{
    LOW = 1,
    MEDIUM = 2,
    HIGH = 3
};

int x = HIGH + 10;   // valid -- x is 13

Enum-to-Integer Assignment Is Loose

Enum variables are assigned using enumerator names in ordinary use, but C's type rules also allow assigning a plain integer expression to an enum variable — even a value that isn't one of the type's own named enumerators.

The value 10 does not automatically become one of the named enumerators — it's simply an integer value now associated with the enum object. This is one reason C's enum shouldn't be treated exactly like the strongly restricted enum types found in some other languages.

enum State
{
    IDLE,
    RUNNING,
    ERROR
};

enum State state;
state = RUNNING;   // normal usage

state = 10;        // compiles, subject to C's type rules --
                   // 10 is not one of IDLE/RUNNING/ERROR

typedef With Enum

Combining typedef with an enum lets you drop the enum keyword from every later declaration, which is a common convention in real C code.

// without typedef:
enum State
{
    IDLE,
    RUNNING,
    ERROR
};
enum State state;

// with typedef:
typedef enum
{
    IDLE,
    RUNNING,
    ERROR
} State;

State state;   // shorter

Enum as a State Machine

One of the most common practical uses of enum is representing the states of a finite state machine, with a switch statement driving the transitions between them.

Each case represents one recognized state, and each assignment to state represents one transition — a clean, readable way to express a finite state machine without magic numbers scattered through the logic.

typedef enum
{
    STATE_OFF,
    STATE_INIT,
    STATE_RUNNING,
    STATE_ERROR
} SystemState;

SystemState state = STATE_INIT;

switch (state)
{
    case STATE_OFF:
        state = STATE_INIT;
        break;
    case STATE_INIT:
        state = STATE_RUNNING;
        break;
    case STATE_RUNNING:
        /* continue operation */
        break;
    case STATE_ERROR:
        /* handle error */
        break;
    default:
        state = STATE_ERROR;
        break;
}

Union vs Enumeration at a Glance

Although a union and an enum are both user-defined C types, they solve entirely different problems and shouldn't be reached for interchangeably.

Feature               Union                                    Enumeration
--------------------------------------------------------------------------------
Main purpose           Share storage between alternatives        Give names to integer constants
Storage                One shared storage area                    Integer-like type
Members                Can have different types                   Named integer constants
Access                 . or ->                                    Use enumerator names directly
Example                union Data                                 enum State
Typical use            Alternative representations                States, modes, categories
Memory optimization    Yes, when alternatives are exclusive       Not its main purpose

Combining Union and Enum: The Tagged Union Pattern

Pairing an enum with a union is a particularly powerful and common pattern: the enum records what kind of value is stored, while the union provides the actual shared storage for it.

The enum answers "what kind of value is stored?" and the union answers "where is that value stored?" — together, that's what people mean by a tagged union or discriminated union: a struct made of exactly a type tag plus a union of alternatives.

enum DataType
{
    TYPE_INT,
    TYPE_FLOAT,
    TYPE_CHAR
};

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

struct Value
{
    enum DataType type;
    union Data data;
};

struct Value value;
value.type = TYPE_INT;
value.data.i = 100;
Value
                |
       +--------+--------+
       |                 |
      type              data
       |                 |
   TYPE_INT            union
                         |
              +----------+----------+
              |          |          |
             int       float       char

When to Use a Union

Reach for a union when several possible data representations are genuinely mutually exclusive, when memory usage matters and the alternatives are never needed simultaneously, when you need alternative views of the same storage and understand the relevant C object-representation rules, when you're implementing a tagged or discriminated value, or when hardware or protocol data demands a carefully controlled representation.

Don't use a union simply because it's smaller than the equivalent struct, without also thinking through how its members will actually be accessed and what your program's semantics require of the active member.

When to Use an Enumeration

Reach for an enum whenever a variable represents one of a finite set of named choices: implementing states, representing modes or categories, writing readable switch statements, eliminating magic numbers, or naming protocol and application states.

enum State
{
    OFF,
    ON
};

enum Direction
{
    NORTH,
    SOUTH,
    EAST,
    WEST
};

enum ErrorCode
{
    ERROR_NONE = 0,
    ERROR_TIMEOUT = 10,
    ERROR_INVALID = 20
};

Quick Reference

A compact summary of the union, enum, and tagged-union syntax covered on this page.

What is the fundamental difference between how a struct and a union allocate storage for their members?

A struct gives every member its own private storage, so all members can hold independent values at the same time. A union collapses all of its members onto a single shared storage area, so at most one member's value is meaningful at any given moment.

Why do all members of a union share exactly the same address?

Because a union's members overlap the same block of memory rather than each getting their own slot, taking the address of any member simply returns the union's own starting address — there is only one address to give.

What problem does a tagged union solve that a plain union by itself does not?

A plain union has no built-in way of remembering which member was most recently written, so nothing stops you from reading the wrong one. A tagged union adds an explicit tag, usually an enum stored alongside the union in a struct, that records which member is currently meaningful.

Why is enum Day { MONDAY, TUESDAY, ... } considered more readable than using plain integers, even though the compiler ultimately represents each name as an integer anyway?

The named enumerator documents its own meaning at the point of use — comparing against MONDAY tells a reader what's being checked, while comparing against 0 forces them to remember or look up what that particular number represents.

Why shouldn't code rely on sizeof(enum State) always being equal to sizeof(int)?

The C standard only requires the compiler to pick some integer type capable of representing all of the enumeration's values; which exact type that is remains implementation-defined, so the size of an enum type can differ from the size of int on some implementations.

Write a C program that declares a union Data with int, float, and char members, stores an int value in it and prints it, then overwrites the same union with a float value and prints that instead, demonstrating that both values share one storage location.

Both assignments target the same union object, so writing d.f after d.i overwrites the storage that d.i used. Printing d.i before the second assignment shows the int value; printing d.f afterward shows the float value, but at no point do both values exist at the same time.

Write a C program that declares a union Data with int, float, and double members and prints the size of the union alongside the individual sizes of int, float, and double, to show that the union's size matches its largest member rather than the sum of all three.

A union only needs enough storage for its largest member, subject to alignment, so sizeof(union Data) matches sizeof(double) here rather than the combined size of int, float, and double added together.

Write a C program that declares a union Data with int and float members, stores a value in the int member, and prints the addresses of both the int and float members to show they are identical.

Because i and f share the union's single storage area, taking the address of either member yields the same address — the union object's own starting address — even though the two members have different types.

Write a C program that implements a tagged union using an enum DataType with values TYPE_INT, TYPE_FLOAT, and TYPE_CHAR, and a struct Value containing that enum plus a union of int, float, and char members, then stores an int value and correctly prints it by checking the tag first.

The type field records which union member was written, so checking it before reading value.data.i guarantees the program interprets the shared storage using the same type it was written with, rather than guessing.

Write a C program that models a struct Measurement with an enum ValueType tag and a union holding either an int or a float reading, sets it to a floating-point measurement, and prints it correctly by checking the tag before reading the union.

The enum tag identifies which kind of measurement is currently stored, so the if/else picks the matching union member to read — m.value.floating in this case — instead of assuming a fixed type for every measurement.

Write a C program that defines an enum Day with the default values MONDAY through FRIDAY, declares an enum Day variable, and uses a switch statement to print the full name of whichever day is stored.

MONDAY through FRIDAY are automatically numbered 0 through 4 since no explicit values were given, and the switch compares today against each named constant to print the matching day's name.

Write a C program that defines an enum ErrorCode with explicit values SUCCESS = 0, ERROR_TIMEOUT = 10, ERROR_INVALID = 20, and ERROR_BUSY = 30, and prints a descriptive message for a given error code using a switch statement.

Each enumerator is assigned a specific, externally meaningful value, and the switch matches the code parameter against those named constants rather than the underlying numbers, so the messages stay readable and correct even if new codes are added later.

Write a C program that uses typedef with an anonymous enum to define a SystemState type with values STATE_OFF, STATE_INIT, STATE_RUNNING, and STATE_ERROR, then simulates a small state machine that steps through a few transitions, printing the state name at each step.

The typedef lets SystemState be used directly without writing enum each time, and the loop repeatedly prints the current state's name before applying the same transition logic a real state machine would use to move to the next state.

Write a C program that defines a union Data with an int member and a struct Point member (containing x and y fields), and demonstrates storing and printing each alternative representation separately, one at a time.

value and point occupy the same union storage, so setting d.value and later setting d.point.x/d.point.y are two separate, non-overlapping uses of that storage — printing d.value after the point fields were set would no longer show 42, since the earlier representation has been overwritten.

Write a C program that declares an anonymous union inside a struct Sensor (holding an id and either a raw int reading or a float value reading), and demonstrates setting and printing values directly through the struct without an intermediate union member name.

Because the union inside struct Sensor is anonymous, its members raw and value are accessed directly as s.raw and s.value instead of through an extra name like s.data.raw — they still share the same storage, so setting s.value afterward overwrites the earlier s.raw reading.

// Union
union Data
{
    int i;
    float f;
    char c;
};

union Data d;
d.i = 10;
printf("%d\n", d.i);

// Pointer:
union Data *p = &d;
p->i = 20;
// Enumeration
enum State
{
    IDLE,
    RUNNING,
    ERROR
};

enum State state = RUNNING;
// Tagged union
enum Type
{
    TYPE_INT,
    TYPE_FLOAT
};

struct Value
{
    enum Type type;
    union
    {
        int i;
        float f;
    } data;
};