---
title: 'Structs, Unions and Enumerations'
source: 'https://academia.sh/en/courses/c-fundamentals/structs-unions-and-enums'
course: 'C Fundamentals'
language: en
updated: '2026-09-26T03:02:42+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - "Measures that a struct's fields are laid out at compile time and that assignment copies every field"
    - 'Safely shows with a tagged read that a union shares the same memory'
    - 'Confirms with an example that enumeration constants are compile-time constants'
---

# Structs, Unions and Enumerations

A struct's fields being laid out at compile time, a union sharing the same memory, and enumeration constants being compile-time constants; showing with a tag field that the language does not track which member of a union is valid.

The Arrays and Strings lesson measured how values of the same type are ordered within an
array. This lesson measures how values of different types are gathered under a single
name: a **struct** places different types side by side, a **union** has the same memory
shared by different interpretations, an **enumeration** produces named constants — all
three mechanisms decided at compile time from the declaration written in the source.

The Programming Fundamentals course (M01/K02) already established record-like data
grouping with Python; there, fields were names inside a dictionary or object, addable and
removable at runtime. C's counterpart is measured here: which fields a struct will carry,
in what order those fields sit in memory, and an enumeration constant's value — all fixed
at compile time from the declaration written in the source, without the program ever
running. This is the last stop on the course's measurement axis: the question opened in
The Place of C lesson, that a source turns into a program by passing through four stages,
closes here by asking at which stage three new mechanisms are decided.

## Struct: Fields Laid Out at Compile Time

A struct declaration fixes every field's name and type in the source; after this
declaration, every variable of that struct type carries the same field layout. While an
array, as measured in the Arrays and Strings lesson, orders values of the same type, a
struct gathers values of different types under a single name — like a point's `x` and `y`
coordinates, values that carry meaning together but can be of different types. Assigning
one struct variable to another copies every field at once.

```c
// struct.c — a struct's fields are laid out at compile time, assignment copies every field
#include <stdio.h>

struct Point {
    int x;
    int y;
};

int main(void) {
    struct Point a = {3, 4};
    struct Point b = a;
    b.x = 99;

    printf("%s %d\n", "a.x", a.x);
    printf("%s %d\n", "a.y", a.y);
    printf("%s %d\n", "b.x", b.x);
    printf("%s %d\n", "b.y", b.y);
    return 0;
}
```

```
a.x 3
a.y 4
b.x 99
b.y 4
```

**BS12.** The `struct Point` declaration fixes at compile time that every `Point`
variable will carry an `x` and a `y` field, and these fields' names and types — this
struct decides which fields it will carry not at runtime, but in the source. The
assignment `b = a` copies both of `a`'s fields into `b`; the subsequent change to `b.x`
then affects only `b`'s own copy, `a.x` is still `3`. This shows how the
parameter-copying rule measured in the previous lesson works for a struct: when a struct
variable is assigned to another or passed to a function, what is transferred is a copy of
**every field** — unlike an array, a struct does not decay into the address of its first
element when passed to a function, it is copied whole, with all its fields.

The order of fields in memory is also determined by the declaration order in the source:
`struct Point`'s `x` field is always laid out before `y`, because the field written first
in the declaration comes first. A struct can also hold another struct as a field — like a
`struct Line { struct Point start; struct Point end; };` carrying `struct Point` fields —
and this nesting carries the same rule in layers: each inner struct's own fields are laid
out, according to declaration order again, inside the outer struct's own field. This
layout decision, just like an array's element count, is fixed by the compiler without the
program ever running — which field sits at which address is determined purely from the
declaration itself, without ever looking at data at runtime. No real address or byte
offset is printed in this course, but that it is a consequence of declaration order shows
why a struct definition determines both the data's **meaning** and its **layout** in
memory at the same time.

## Union: Sharing the Same Memory

A union defines several fields in the **same** memory area — each field corresponds to
the same address, under a different type interpretation. The union itself does not track
which field is "valid"; this information must be carried separately, with a **tag**
field.

```c
// union.c — a union sharing the same memory, used safely with a tagged read
#include <stdio.h>

enum Kind { INTEGER, DECIMAL };

struct TaggedValue {
    enum Kind kind;
    union {
        int whole;
        float decimal;
    } value;
};

static struct TaggedValue make_integer(int v) {
    struct TaggedValue t;
    t.kind = INTEGER;
    t.value.whole = v;
    return t;
}

static struct TaggedValue make_decimal(float v) {
    struct TaggedValue t;
    t.kind = DECIMAL;
    t.value.decimal = v;
    return t;
}

static void print_value(struct TaggedValue t) {
    if (t.kind == INTEGER) {
        printf("%s %d\n", "read as an integer", t.value.whole);
    } else {
        printf("%s %.2f\n", "read as a decimal", t.value.decimal);
    }
}

int main(void) {
    struct TaggedValue first = make_integer(42);
    struct TaggedValue second = make_decimal(3.5f);
    print_value(first);
    print_value(second);
    return 0;
}
```

```
read as an integer 42
read as a decimal 3.50
```

**BS13.** `whole` and `decimal` inside the `union` share the same memory area — once one
is written, the other's old value becomes meaningless, because both read the same bytes
through a different type lens. `make_integer` writes only `value.whole`, `make_decimal`
only `value.decimal`; `print_value` always looks at the `kind` field to learn which
interpretation is valid and reads only that field. Thanks to this discipline, the lesson
never reads the field that was **not** written — `first` always reads `whole`, `second`
always reads `decimal`. The `union` inside `TaggedValue` (the `value` field) does not
know on its own which member was last written; carrying this information is the `kind`
field's job, and keeping that field correct is the source writer's job.

How much space a union will occupy is also fixed at compile time: the measure the
standard sets is that enough space is allocated to carry the union's **largest** field —
because any field must be writable at any moment. Since no real size is printed in this
course, this lesson does not measure this, but the difference between how the two
declarations grow must be noted: a struct lays its fields one after another, a union
overlaps them. This is why adding a field smaller than the largest to a union generally
does not grow the total space; adding one to a struct grows it unless the field fits into
existing padding. In both cases, alignment has the final word: the standard states that
unnamed padding may appear at the end of both a struct and a union, so the total space
cannot be read directly from the fields' sizes.

## Enumeration: A Compile-Time Constant

An enumeration declaration produces named integer constants; these constants can be used
everywhere a constant expression written in the source can be used — for instance, to
determine an array's length.

```c
// stage.c — enumeration constant (excerpt), used as a compile-time constant
#include <stdio.h>

enum Color { RED = 1, GREEN = 2, BLUE = 4 };

int main(void) {
    printf("%s %d\n", "enumeration constant", (int)BLUE);

    int colors[BLUE];
    printf("%s %d\n", "element count of the array built with the constant",
           (int)(sizeof colors / sizeof colors[0]));
    return 0;
}
```

```
enumeration constant 4
element count of the array built with the constant 4
```

The value of `BLUE` comes from the constant `4` written in the `enum Color` declaration,
and this value is known without the program ever running. The declaration `int
colors[BLUE];` confirms this: `BLUE` is an expression of the kind constant folding, as
measured in the Arrays and Strings lesson, accepts — had it been a variable or a function
call, the array's size could not have been determined at compile time. The `colors`
array's element count, without any real size being printed, is again measured with the
`sizeof` ratio and comes out `4` — the exact same number as `BLUE`, because both derive
from the same compile-time constant.

Explicit values like `RED = 1, GREEN = 2, BLUE = 4` in the `enum Color` declaration are
not mandatory; an enumeration can also be declared with no value written at all, in which
case the first name starts from `0` and each subsequent name takes a value one greater
than the one before. The declaration here writes its values explicitly, because `RED`,
`GREEN` and `BLUE` represent separate bit positions (`1`, `2`, `4`), which lets colors be
combined with a bitmask — an expression like `RED | BLUE` can produce the value `5`,
representing both colors at once. This usage, too, is a property fixed at compile time:
which constant corresponds to which bit position comes from the `=` sign written in the
source, with no data at runtime looked at.

## The Bounding Measurement

A union does not track on its own which of its fields was last written — carrying this
information is not the language's job, but the source writer's. The `TaggedValue` struct
above resolves this with a tag field: `kind` tells which member of `value` is valid, and
`print_value` always looks at `kind` first.

If this discipline were abandoned — if `print_value` never looked at `kind` and, say,
always read `value.decimal` — then for `first` (which `make_integer` had written only
`value.whole` into), the byte pattern read would have been written as an `int` and read
as a `float`. This shows that the only thing a union guarantees is **sharing the same
address**, not which type interpretation is meaningful — the result of a mismatched
interpretation is an unspecified bit pattern, and this course's general rule does not try
it in a code block. The defined counterpart set alongside it is the tagged read above
itself: every read learns which member was last written to that union from an
independent field (`kind`) and reads only that member, and this measurement produces the
same two lines every time it is run.

For the tagged read to work, the tag must always be updated together with the
corresponding write — both `make_integer` and `make_decimal` set `kind` before writing
their own field. If a third function wrote `value.whole` and forgot to update `kind`,
`print_value` reading that variable would still look at the stale `kind` value and read
the wrong member — a union can neither produce nor prevent this inconsistency on its own,
because the union itself only provides memory sharing, not staying in sync with a tag.
This reflects a deliberate design decision in C's union mechanism: the language does not
take on the cost of tracking field selection — it does not separately record which field
is active on every write — and leaves this responsibility, just as with array length
information, entirely to the source writer.

This also shows that a struct and a union are two complementary mechanisms: a struct
allocates separate memory for each field and makes all of them valid at once, while a
union shares a single memory area and makes only one valid. `TaggedValue` combines the
two — the outer struct carries both the tag and the shared field, and thanks to the tag
field, the information a union does not track ends up kept at the struct level.

This combination forms the basis of a widespread pattern in C called a "tagged union": a
source writer who wants to model a value carrying only one of several possible shapes
uses these three mechanisms — struct, union and enumeration — together. The enumeration
expresses which shape is valid with named constants; the union has those shapes share a
common memory area; the struct carries the two together as a single value. The three
mechanisms measured in this lesson are therefore not three independent subjects, but
three components of the same pattern.

## Summary

- A struct's fields, together with their names and types, are fixed at **compile time**;
  when a struct variable is assigned to another, every field is copied at once.
- A union defines several fields at the same memory address; the language does not track
  which field was last written, and the source writer must carry this information
  separately (with a tag field).
- Enumeration constants (like `BLUE` inside `enum Color`) are **compile-time constants**;
  they can be used everywhere a constant expression is required, such as determining an
  array's length — class **compiler**.
- Reading a member not written into a union (untagged, under a wrong type assumption)
  reinterprets the bytes as another type; C does not forbid this, but the value of bytes
  not corresponding to the last-written member is unspecified, and if the pattern read is
  a trap representation, the behavior is undefined. This is not run in this course; the
  defined counterpart is the tagged read.
- A struct keeps every field in separate memory and makes all of them valid at once,
  while a union shares a single memory area and makes only one valid — two complementary
  mechanisms.
- When used together, enumeration, union and struct form the "tagged union" pattern: the
  enumeration names which shape is valid, the union shares the shapes' memory, the struct
  combines the two into a single value.

## Next Step

Every decision measured in this topic stayed within a single run of a program: a type's
range, a name's scope, an operator's choice, a branch's chosen path, a call's bound body,
an array's length, a union's valid field — all of it could be read either from the source
or from that run's own data. The Input and Output lesson adds one last question to this
ground: at which stage is the data a program takes in from outside, or writes out to the
outside, interpreted? Every placeholder in a format string must match the type of its
corresponding argument, but the compiler can only catch this match under certain
conditions — the language itself does not guarantee it. In the `TaggedValue` example
measured in this lesson, keeping the tag correct was the source writer's job; in the next
lesson, keeping the format string consistent with its arguments will likewise be the
source writer's responsibility.

The seven lessons measured in this topic each touched every one of the four classes
(preprocessor, compiler, linker, runtime) at least once: Data Types and Operators at the
compiler, Control Flow at runtime, Variables and Functions at the linker, Arrays and
Strings at both, and this lesson landed at the compiler once again. The closing lesson
gathers the decisions these seven lessons measured into a single table, and adds input
and output's own share to that table — format strings being interpreted at runtime.
