The C Path — learn C, visually

🧬 Types & Qualifiers, In Depth

enum: constants with names and superpowers

⏱ 12 min · free interactive lesson · quizzes, visualizations & a real compiler

▶ Open the interactive lesson — free, no signup
Why you're learning this

The web's famous "404 Not Found" is a named number — exactly the kind enum creates. Instead of scattering a bare 2 through your code and hoping everyone remembers it means "green light", you write GREEN — and the compiler will even warn you when a switch forgets to handle one of the possibilities.

Code full of magic numbers — if (state == 2) — is code nobody can read. enum mints a family of named integer constants in one line, and unlocks a compiler superpower along the way.

traffic.c
#include <stdio.h>

enum Light { RED, AMBER, GREEN };          /* 0, 1, 2      */
enum Http  { OK = 200, MOVED = 301,
             NOT_FOUND = 404, TEAPOT = 418 };
enum Mix   { A = 5, B, C, D = 40, E };     /* 5,6,7,40,41  */

int main(void) {
    enum Light l = AMBER;
    printf("light=%d  teapot=%d  E=%d\n", l, TEAPOT, E);
    return 0;
}
terminal
$ gcc traffic.c -o traffic && ./traffic
light=1  teapot=418  E=41

The rules: constants count up from 0 by default; give any of them an explicit = value and counting resumes from there. Duplicated values are legal (handy for aliases like COLOR_DEFAULT = COLOR_BLUE).

🧠 Checkpoint: In enum { P = 3, Q, R = 2, S }; what are Q and S?

  • Q=4, S=3
  • Q=4, S=4
  • Q=0, S=1
  • Compile error: duplicate values
Show answer

Q=4, S=3 — Counting resumes after each explicit value: Q = P+1 = 4, S = R+1 = 3. Note Q and... wait, S=3 equals P? Perfectly legal — duplicate enum values are allowed.

Bit-flag enums: one int, many booleans

Give each constant its own bit with shifts, and a single integer becomes a set you can combine with | and test with &:

perms.c — a set in a single int
#include <stdio.h>

enum Perm {
    PERM_READ  = 1u << 0,   /* 0b001 */
    PERM_WRITE = 1u << 1,   /* 0b010 */
    PERM_EXEC  = 1u << 2,   /* 0b100 */
};

int main(void) {
    unsigned p = PERM_READ | PERM_EXEC;      /* combine: 0b101 */

    if (p & PERM_EXEC)  puts("can execute");
    if (!(p & PERM_WRITE)) puts("read-only!");

    p |=  PERM_WRITE;    /* grant a flag  */
    p &= ~PERM_EXEC;     /* revoke a flag */
    printf("now p = 0b%03b\n", p);          /* %b is C23     */
    return 0;
}

This spot has an interactive bits widget — open the interactive lesson to play with it.

🧠 Checkpoint: Why must bit-flag enumerators be powers of two (1, 2, 4, 8…)?

  • C requires enum values to be powers of two
  • So each flag owns exactly one bit — combinations never collide
  • Because shifts are faster than addition
  • To keep sizeof(enum) small
Show answer

So each flag owns exactly one bit — combinations never collide — With one bit per flag, OR-ing any subset produces a unique pattern and & can test each flag independently. If EXEC were 3 (0b011), it would overlap READ|WRITE and testing would lie.

Enums are just ints (mostly)

In classic C, enumeration constants have type int, and the enum variable itself is some implementation-chosen integer type big enough for the values. There's no range checking: enum Day d = 99; compiles fine. Treat enums as documentation plus warnings, not as a safety fence.

🎉

C23 upgrade: you can now pin the underlying type — enum Status : unsigned char { OK, FAIL }; — guaranteeing sizeof(enum Status) == 1. Great for packing structs and for talking to hardware or file formats. C23 also lets enumerators exceed int range, taking a larger type automatically.

The superpower: switch coverage warnings

Switch over an enum, list its cases, and leave out the default — now if anyone ever adds a new enumerator, the compiler points at every switch that forgot to handle it:

next.c — one case missing, on purpose
enum Light { RED, AMBER, GREEN };

enum Light next(enum Light l) {
    switch (l) {                 /* no default — deliberately! */
    case RED:    return GREEN;
    case GREEN:  return AMBER;
    }                            /* forgot AMBER...            */
    return RED;
}
terminal
$ gcc -Wall -c next.c
next.c: In function 'next':
next.c:4:5: warning: enumeration value 'AMBER' not handled in switch [-Wswitch]
💡

This is why seasoned C programmers often omit default: in enum switches — a default silences -Wswitch forever. No default means the compiler audits your coverage for free, at every compile, for the lifetime of the codebase.

🧠 Checkpoint: You add a fourth value to an enum used in switches all over a big codebase. What is the cheapest way to find every spot that must be updated?

  • grep for the enum name
  • Recompile with -Wall: every switch without a default that misses the new case gets flagged
  • Run the test suite and hope
  • Add a default: abort(); everywhere
Show answer

Recompile with -Wall: every switch without a default that misses the new case gets flagged — -Wswitch (part of -Wall) reports each enum switch that does not handle every enumerator — but only when there is no default clause to swallow the omission. The compiler becomes your refactoring checklist.

enum vs #define

This spot has an interactive editor widget — open the interactive lesson to play with it.

We keep saying "an enum is int-sized" — time to meet the operator that lets you actually measure types: sizeof.

▶ Practice this lesson interactively (with live gcc)