Structs & Data Structures · beginner · ~8 min

enums

**What you will learn** - Declare an `enum` to give meaningful names to a small, fixed set of integer constants. - Predict the numeric value of every enumerator, including the default `0`-then-increment rule and explicit overrides. - Combine an `enum` with a `typedef` so you can use a short type name (building directly on the *typedef* lesson). - Drive a `switch` statement from an enum value and use compiler exhaustiveness warnings to catch forgotten cases. - Convert an enum value to a human-readable string safely, including handling unexpected values. - Recognise the limits of enums in C: implementation-defined underlying type and the fact that any `int` can be stored in an enum variable.

Overview

An enumeration (enum) is C's way of attaching readable names to a small, fixed set of related integer constants. Instead of writing magic numbers like 0, 1, and 2 and trying to remember which is which, you write STATE_IDLE, STATE_RUNNING, and STATE_DONE. The compiler still stores plain integers underneath, but your code now documents its own intent.

This matters because real programs are full of "one of a few choices" situations: a traffic light is red, yellow, or green; an HTTP-like request is a GET, POST, or DELETE; a parser is in one of a handful of states. Encoding those choices as named constants makes the code easier to read, harder to get wrong, and friendly to tools: editors can autocomplete the names, and the compiler can warn when a switch forgets a case.

Enums build directly on the typedef lesson you just finished. A bare enum makes you write the keyword every time: enum colour c;. Wrapping it in a typedef lets you write a clean alias such as colour_t c;. The two features are commonly used together, and you will see that pairing throughout this lesson.

A bit of terminology before we go deeper. The whole declaration enum colour { ... } defines an enumerated type. Each name inside the braces — red, green, blue — is an enumerator (also called an enumeration constant). Every enumerator has an integer value. An enum variable is a variable declared with that type; in C it can in practice hold any int, not only the listed values, which is an important detail we return to later.

Why it matters

Named constants are one of the cheapest ways to make code correct and maintainable. Compare two lines:

if (light == 2) { go(); }            /* what is 2? */
if (light == GREEN) { go(); }          /* obvious */

The second version is self-documenting, survives renumbering (change the enum once, every use follows), and lets the compiler help you. When you switch over an enum and enable warnings (-Wall -Wextra), GCC and Clang will tell you if you forgot to handle one of the values — a free correctness check that magic numbers can never give you.

Enums also reduce a whole class of bugs in real software: state machines that silently fall into an undefined state, protocol handlers that misinterpret a command code, and configuration flags that get mixed up. By giving each state and command a name, you make the intended set of values visible in one place, so reviewers and future-you can reason about what is and is not allowed. Even in security-sensitive code, restricting a value to a known, named set is a small but real form of input modelling: it makes "unexpected value" something you can explicitly detect and reject.

Core concepts

1. What an enum is

An enum defines a new type whose values are drawn from a list of named integer constants. The declaration looks like this:

enum colour { RED, GREEN, BLUE };

This creates a type called enum colour and three constants — RED, GREEN, BLUE — that you can use anywhere an integer constant is allowed.

How it works internally: the compiler simply substitutes the integer value wherever you use an enumerator. There is no runtime object for the enum itself; RED is just a name for 0. The constants live in the same namespace as ordinary identifiers, so you cannot reuse RED as a variable name in the same scope.

When to use it: any time a value is logically "one of a small fixed set" — states, kinds, categories, directions, error codes. When not to: for an open-ended or large set (use real data), for true bit flags you need exact widths on (consider explicit unsigned masks), or when you need a value the language guarantees to be exactly 1 byte (the underlying type is not guaranteed).

Knowledge check: In enum colour { RED, GREEN, BLUE };, is GREEN a variable or a constant? What integer does it stand for?

2. Enumerator values: defaults and overrides

If you do not assign values, the first enumerator is 0 and each one after it is one greater than the previous:

enum step { ZERO, ONE, TWO, THREE };  /* 0, 1, 2, 3 */

You can set values explicitly, and counting resumes from whatever you last set:

enum mix { A = 5, B, C = 10, D };     /* A=5, B=6, C=10, D=11 */

Values do not have to be unique — two enumerators may share the same number, though that is usually a sign of a mistake.

A memory/value picture of the mix example:

Name :  A     B     C      D
Value:  5     6     10     11
        |     |      |      |
        set   +1     set    +1

Pitfall: explicit values can accidentally collide. enum { LOW = 1, MEDIUM = 1, HIGH = 2 }; compiles fine, but LOW and MEDIUM are now indistinguishable.

Knowledge check (predict the output): For enum e { P = 2, Q, R = 1, S };, what are the values of P, Q, R, and S?

3. Pairing enums with typedef

Without a typedef you must write enum every time:

enum state s = STATE_IDLE;   /* keyword 'enum' required */

Using the typedef technique from the previous lesson, you give the type a one-word alias:

typedef enum { STATE_IDLE, STATE_RUNNING, STATE_DONE } state_t;
state_t s = STATE_IDLE;      /* clean */

This is the most common style in real code. The enum here is anonymous (it has no tag), and state_t becomes the name you use.

Knowledge check (explain in your own words): Why does state_t s; work but state s; (without typedef and without the enum keyword) does not compile?

4. Enums with switch and exhaustiveness

Enums shine with switch. Because the set of values is known, the compiler can warn when a switch on an enum misses a case (with -Wall/-Wextra):

switch (s) {
    case STATE_IDLE:    /* ... */ break;
    case STATE_RUNNING: /* ... */ break;
    case STATE_DONE:    /* ... */ break;
}

If you later add STATE_PAUSED to the enum and forget to handle it here, the compiler points it out. This exhaustiveness check is one of the biggest practical reasons to use enums instead of integers.

Pitfall: adding a default: case can silence the missing-case warning, because the compiler considers every value handled. Use default: deliberately, not reflexively.

5. The underlying type is implementation-defined

In C, the value of each enumerator is of type int (the constants are int constants). The enum variable itself has an implementation-defined integer type that must be able to hold all the enumerators — often int, but not guaranteed. So:

  • Do not assume sizeof(enum X) == 4 (or any fixed size) when packing into a struct or a binary file format.
  • An enum variable can legally hold values outside the named set (for example, the result of (enum colour)42), so always handle the "unexpected value" path.
Logical view:   RED   GREEN   BLUE
Values:          0      1       2
Storage:        [ some integer type chosen by the compiler ]
                 ^ can physically hold 42, -1, etc.

Syntax notes

Three common forms, annotated:

/* 1. Tagged enum: type is 'enum colour' */
enum colour { RED, GREEN, BLUE };
enum colour c = GREEN;            /* must write 'enum' */

/* 2. Explicit values; counting resumes after each assignment */
enum http { GET = 1, POST, PUT = 10, DELETE };  /* 1,2,10,11 */

/* 3. typedef + anonymous enum: clean one-word type name */
typedef enum {
    LOG_INFO,                     /* 0 */
    LOG_WARN,                     /* 1 */
    LOG_ERROR                     /* 2 */
} log_level_t;                    /* the alias you use everywhere */

log_level_t level = LOG_WARN;     /* no 'enum' keyword needed */

Key rules: enumerators are separated by commas; a trailing comma after the last one is allowed in C11; the closing brace is followed by a semicolon. Enumerators are visible in the enclosing scope, so their names must not clash with other identifiers there.

Lesson

What an enum is

An enum (enumeration) defines a set of named integer constants. Instead of scattering raw numbers through your code, you give each value a clear name.

Default values

By default, the first constant is 0. Each constant after it is one more than the previous one.

You can also set the values yourself:

enum colour { red = 1, green = 2, blue = 4 };

When to use enums

Reach for an enum whenever a value should be one of a small, fixed set of named integers. Common cases:

  • State machines (the states a program can be in)
  • Message kinds
  • Error codes

Pair an enum with a switch statement. The compiler can then warn you if you forget to handle one of the cases (an exhaustiveness check).

Code examples

#include <stdio.h>

/* A small finite state machine for a turnstile, expressed as an enum.
   Pairing enum with typedef (from the typedef lesson) gives a clean type. */
typedef enum {
    STATE_LOCKED,    /* 0: default start state */
    STATE_UNLOCKED,  /* 1 */
    STATE_COUNT      /* 2: handy sentinel = number of real states */
} state_t;

/* Inputs the turnstile can receive. */
typedef enum {
    EVENT_COIN,      /* 0 */
    EVENT_PUSH       /* 1 */
} event_t;

/* Convert a state to text. Handles unexpected values defensively. */
static const char *state_name(state_t s) {
    switch (s) {
        case STATE_LOCKED:   return "LOCKED";
        case STATE_UNLOCKED: return "UNLOCKED";
        case STATE_COUNT:    break;  /* not a real state; fall through */
    }
    return "UNKNOWN";  /* covers STATE_COUNT and any out-of-range value */
}

/* Compute the next state from the current state and an event. */
static state_t next_state(state_t s, event_t e) {
    switch (s) {
        case STATE_LOCKED:
            return (e == EVENT_COIN) ? STATE_UNLOCKED : STATE_LOCKED;
        case STATE_UNLOCKED:
            return (e == EVENT_PUSH) ? STATE_LOCKED : STATE_UNLOCKED;
        case STATE_COUNT:
        default:
            return s;  /* defensive: unknown state stays put */
    }
}

int main(void) {
    /* A short sequence of events to simulate. */
    event_t events[] = { EVENT_PUSH, EVENT_COIN, EVENT_PUSH, EVENT_COIN };
    size_t n = sizeof events / sizeof events[0];

    state_t s = STATE_LOCKED;
    printf("start: %s\n", state_name(s));

    for (size_t i = 0; i < n; i++) {
        const char *ev = (events[i] == EVENT_COIN) ? "COIN" : "PUSH";
        s = next_state(s, events[i]);
        printf("after %-4s -> %s\n", ev, state_name(s));
    }
    return 0;
}

What it does: it models a turnstile that is LOCKED until a coin is inserted, becomes UNLOCKED, and locks again when pushed. The program feeds a fixed list of events through next_state and prints the resulting state after each one. STATE_COUNT is a common trick: by placing it last, its value (2) equals the number of real states, useful for sizing arrays.

Expected output:

start: LOCKED
after PUSH -> LOCKED
after COIN -> UNLOCKED
after PUSH -> LOCKED
after COIN -> UNLOCKED

Edge cases: pushing while LOCKED does nothing (stays LOCKED); inserting a coin while UNLOCKED does nothing (stays UNLOCKED). The default/UNKNOWN paths exist so that a value outside the named set never produces undefined behaviour or a misleading label.

Line by line

  1. The first typedef enum { ... } state_t; defines the state type. STATE_LOCKED becomes 0, STATE_UNLOCKED becomes 1, and STATE_COUNT becomes 2. Because STATE_COUNT is last, it conveniently equals the count of real states.
  2. The second enum defines events: EVENT_COIN = 0, EVENT_PUSH = 1.
  3. state_name uses a switch. With warnings on, the compiler checks that the listed enum cases are handled. STATE_COUNT is intentionally not a real state, so it breaks and falls through to the final return "UNKNOWN", which also catches any out-of-range value.
  4. next_state encodes the transition rules. From LOCKED, only EVENT_COIN unlocks; everything else keeps it LOCKED. From UNLOCKED, only EVENT_PUSH locks it again. The default branch is a safety net.
  5. In main, events[] lists the inputs and n computes the element count with the sizeof array / sizeof element idiom.
  6. The loop applies each event in turn, reassigning s, and prints the new state name.

A trace of the state variable as the loop runs:

step  event   s before    rule applied                 s after
----  -----   ---------    --------------------------   ----------
  0   PUSH    LOCKED       push while locked -> stay     LOCKED
  1   COIN    LOCKED       coin while locked -> unlock   UNLOCKED
  2   PUSH    UNLOCKED     push while unlocked -> lock   LOCKED
  3   COIN    LOCKED       coin while locked -> unlock   UNLOCKED

The printed lines match this table exactly, which is why the final state is UNLOCKED.

Common mistakes

Mistake 1 — Assuming the first value is 1. Beginners often expect counting to start at 1.

enum day { MON, TUE, WED };   /* MON is 0, not 1! */
int weekday = MON + 1;        /* off-by-one if you assumed MON==1 */

The first enumerator is 0 unless you set it. If you want 1-based values, say so: enum day { MON = 1, TUE, WED };. Recognise this when array indices or printed numbers are consistently one off.

Mistake 2 — Reusing an enumerator name as a variable. Enumerators share the normal identifier namespace.

enum colour { RED, GREEN, BLUE };
int RED = 5;   /* ERROR: 'RED' already names a constant */

The fix is to choose distinct names; a common convention is ALL_CAPS for enumerators so they stand out from variables.

Mistake 3 — Forgetting break in a switch. Without break, control falls through to the next case.

switch (s) {
    case STATE_LOCKED:   handle_locked();   /* missing break! */
    case STATE_UNLOCKED: handle_unlocked(); break;
}

When s == STATE_LOCKED, both handlers run. Corrected: add break; after each case unless you deliberately want fall-through (and comment it when you do).

Mistake 4 — Trusting that an enum variable holds only named values. A cast or bad input can store anything.

state_t s = (state_t)99;        /* legal, but not a named state */
printf("%s\n", state_name(s));  /* relies on the UNKNOWN fallback */

Always include a default: or final fallback so unexpected values are handled rather than silently misread.

Debugging tips

Compiler messages to expect:

  • enumeration value 'STATE_X' not handled in switch (with -Wall -Wextra) means your switch is missing a case. Add it, or add a deliberate default:.
  • redeclaration of 'RED' or 'RED' redeclared as different kind of symbol means an enumerator name clashes with a variable or another constant. Rename one.
  • expected ';' before '}' near an enum usually means a missing semicolon after the closing brace of the enum declaration.

Runtime / logic errors:

  • Off-by-one printed numbers almost always trace back to the 0-based default. Print the raw integer with printf("%d\n", (int)value); to confirm.
  • A state machine "sticking" on one state often means a missing transition or a missing break causing fall-through.
  • Unexpected UNKNOWN/default hits mean a value outside the named set reached your code — log the raw integer to find where it came from.

Questions to ask when it doesn't work:

  1. What is the actual integer value? (printf("%d\n", (int)e);)
  2. Did I assume 1-based when it's 0-based, or assign explicit values that collide?
  3. Does every switch case have a break, and is there a default for surprises?
  4. Are warnings on? Re-compile with -Wall -Wextra — it catches most enum bugs for free.

Memory safety

Enums in C are integers, so the main concerns are about values, not memory blocks:

  • Out-of-range values are well-formed but unmodelled. An enum variable can legally hold any value of its underlying integer type, including ones with no name. Using such a value as an array index (for example, into a lookup table sized only for the named values) causes an out-of-bounds read — undefined behaviour. Always bounds-check before indexing with an enum, or include a default: path.
  • Do not assume a fixed sizeof. The underlying type is implementation-defined. Writing an enum directly to a file or sending it over a network and reading it back on another platform can break. For wire/file formats, serialise to a fixed-width integer (for example uint8_t/uint32_t) explicitly.
  • Beware signed/unsigned and overflow. If you assign very large explicit values, ensure they fit the chosen underlying type; relying on overflow is undefined.
  • Initialise enum variables. An uninitialised enum variable contains an indeterminate value, just like any other uninitialised integer; reading it is undefined behaviour. Always give a sensible starting value (often the 0 / default state).

Robustness habit: treat an enum coming from untrusted input (a file, a network packet, a config value) as an integer to validate first, then map into your enum only after confirming it is one of the allowed values.

Real-world uses

Concrete uses: Enums are everywhere in systems code. Operating systems use them for file open modes and process states; networking code names protocol commands and packet types; graphics libraries (e.g. OpenGL) expose hundreds of named constants as enums; compilers and interpreters model token kinds and AST node types with them; embedded firmware uses them for device states and pin modes. The C standard library itself uses enum-like named constants for things such as SEEK_SET/SEEK_CUR/SEEK_END.

Best-practice habits:

  • Beginner: Use ALL_CAPS or a clear prefix (STATE_, LOG_) so enumerators read as constants and group visibly. Prefer named values over magic numbers. Pair enums with switch and let -Wall -Wextra enforce exhaustiveness.
  • Beginner: Use a common prefix to avoid name clashes, since enumerators live in the global identifier space.
  • Advanced: Add a trailing *_COUNT sentinel for sizing arrays, but exclude it from switch handling logic. Keep enum-to-string converters next to the enum definition so they stay in sync. For serialisation, map to a fixed-width integer rather than writing the enum's raw bytes. Avoid relying on a specific sizeof. When using enums as bit flags, assign distinct powers of two and document that they are meant to be OR-ed.
  • Maintainability: When you add a new enumerator, search for every switch over that type and update it; compiler warnings help, but a default: can hide them, so review consciously.

Practice tasks

Beginner 1 — Day of week values. Define enum day { MON, TUE, WED, THU, FRI, SAT, SUN };. In main, print each day's name alongside its integer value (one per line). Requirements: use a switch (or a parallel string array) to get the name; print the value with %d. Expected output starts: MON = 0, TUE = 1, ... Hint: remember the default is 0-based. Concepts: default values, switch.

Beginner 2 — One-based months. Define enum month { JAN = 1, FEB, MAR };. Print the three names with their values and confirm JAN is 1. Requirements: set JAN = 1 explicitly and let the rest follow. Constraint: do not assign values to FEB or MAR. Hint: counting resumes from the last explicit value. Concepts: explicit values, auto-increment.

Intermediate 1 — Traffic light cycle. Define enum light { RED, GREEN, YELLOW }; and a function enum light next_light(enum light l) that returns the next light in the cycle RED -> GREEN -> YELLOW -> RED. Requirements: use a switch with a default that returns RED defensively; print 6 steps starting from RED. Expected output: RED, GREEN, YELLOW, RED, GREEN, YELLOW. Hint: mirror the next_state pattern from the lesson. Concepts: enum + switch, state transitions.

Intermediate 2 — Safe parse. Write int parse_level(int raw, log_level_t *out) where log_level_t is { LOG_INFO, LOG_WARN, LOG_ERROR }. Given an integer (as if from input), set *out and return 1 only if raw is a valid level; otherwise return 0 and leave *out unchanged. Requirements: validate the integer before storing it in the enum. Input/output example: parse_level(1, &lvl) -> returns 1, lvl == LOG_WARN; parse_level(7, &lvl) -> returns 0. Hint: check raw >= LOG_INFO && raw <= LOG_ERROR. Concepts: untrusted-value validation, out-of-range handling.

Challenge — Mini command dispatcher. Define enum cmd { CMD_HELP, CMD_ADD, CMD_QUIT, CMD_COUNT };. Write enum cmd cmd_from_string(const char *s) mapping "help", "add", "quit" to their commands and anything else to a sentinel meaning "not found" (you may return CMD_COUNT). Then write a loop that reads command words from a fixed array, dispatches each via a switch, and prints what it would do. Requirements: handle the not-found case explicitly; use CMD_COUNT as the array-size sentinel for an optional name table; ensure every real command has a switch case. Constraints: no global state; functions only. Hint: keep the string table and the enum in the same place so they cannot drift apart. Concepts: enum-to/from-string, switch exhaustiveness, sentinel sizing.

Summary

  • An enum gives readable names to a small, fixed set of integer constants, making code self-documenting and tool-friendly.
  • The first enumerator defaults to 0; each subsequent one is one greater unless you assign an explicit value, after which counting resumes from that value. Values may collide if you are not careful.
  • Pair enums with typedef (from the previous lesson) to get a clean one-word type name: typedef enum { ... } state_t;.
  • Combine enums with switch; compile with -Wall -Wextra to get exhaustiveness warnings, and remember a default: can silence them.
  • Most important syntax: enum name { A, B = 5, C }; and the typedef form above.
  • Common mistakes: assuming 1-based values, missing break in a switch, reusing enumerator names, and trusting that an enum variable only ever holds named values.
  • Remember: an enum variable can physically hold any integer of its (implementation-defined) underlying type, so validate untrusted values, include a fallback path, initialise enum variables, and never assume a fixed sizeof.

Practice with these exercises