🧬 Types & Qualifiers, In Depth
enum: constants with names and superpowers
▶ Open the interactive lesson — free, no signupThe 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.
#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;
}$ 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 &:
#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:
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;
}$ 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
- Debugger: an enum shows up as
AMBER; a #define is gone after preprocessing — you see naked1. - Scope: enums obey block scope; #defines bleed across the entire file from their definition point.
- Grouping: one enum declares a related set; #defines are loose confetti.
- Constant expressions: both work in
caselabels and array sizes — this is where enum beatsconst int, too.
▶ 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.