C Enums: Naming a Fixed Set of Values
Define C enums to represent a fixed set of named integer constants, understand their default and explicit integer representation, and recognize how this pattern is the direct ancestor of Objective-C's NS_ENUM macro.
Many values in real programs aren't arbitrary numbers — they're one of a small, fixed set of named possibilities: a traffic light is red, yellow, or green; a network request is idle, loading, or failed. Representing such a state as a plain int (0, 1, 2...) works but is easy to misread and easy to get wrong, since nothing stops you from assigning an out-of-range or meaningless number. C's enum keyword solves exactly this problem by letting you give meaningful names to a fixed set of integer constants, and it's the direct ancestor of the NS_ENUM macro you'll meet later in Objective-C, which builds type safety on top of this exact same underlying mechanism.
Defining an Enum and typedef'ing It
An enum definition lists a set of named constants inside braces: enum DeviceState { StateIdle, StateRunning, StateError };. Just like with structs, this alone only teaches the compiler about a new type; declaring a variable of that type still requires writing enum DeviceState in plain C, unless you pair the definition with typedef the same way you would for a struct.
The idiomatic C pattern, and the one you'll see everywhere in Objective-C headers, combines both in one statement: typedef enum { StateIdle, StateRunning, StateError } DeviceState;. This defines an anonymous enum and simultaneously gives it the bare alias DeviceState, so you can write DeviceState state = StateRunning; afterward with no enum keyword required.
Each name inside the braces — StateIdle, StateRunning, StateError — becomes a genuine named constant, usable anywhere an integer constant is expected, including in switch statements, array indices, and comparisons. Enum constant names live in the same namespace as ordinary variables and other enum constants at that scope, so two different enums in the same file cannot reuse an identical constant name without a naming conflict — a detail Objective-C's naming conventions (prefixing constants, e.g. StateIdle rather than bare Idle) exist partly to avoid.
• enum DeviceState { StateIdle, StateRunning, StateError }; — defines the type; needs enum keyword to use in plain C. • typedef enum { ... } DeviceState; — the idiomatic pattern giving a bare, keyword-free alias. • Each name (StateIdle, etc.) becomes a real named integer constant usable anywhere an int is expected. • Enum constant names share one namespace at their scope — avoid reusing the same name across different enums.
Default Integer Representation
Under the hood, an enum is really nothing more than a set of named int constants — C doesn't create any special runtime type or safety check beyond the compiler recognizing the names. By default, the first name in the list is assigned the integer value 0, and each subsequent name is assigned the previous value plus one: StateIdle is 0, StateRunning is 1, StateError is 2.
You can prove this to yourself by printing an enum constant with %d in printf — it prints as a plain integer, because that's genuinely all it is at the machine level. This also means an enum variable can be compared to, assigned from, or mixed with plain int values with no error in C (though doing so carelessly defeats the whole purpose of using named constants in the first place, and is exactly the kind of looseness that later Objective-C tooling like NS_ENUM tightens up).
Because the values are just sequential ints starting at zero by default, an enum works naturally as a switch statement's controlling expression, and as an index into a parallel array of related data (for example, an array of human-readable strings, one per state, indexed directly by the enum value) — a common and efficient real-world pattern.
• By default, the first enum name gets 0, and each following name is one more than the previous. • An enum constant is really just an int at the machine level — no extra runtime type checking in plain C. • Enum values can be printed with %d and freely mixed with plain ints, for better or worse. • Sequential 0-based values make enums natural for switch statements and array-indexing patterns.
Explicit Value Assignment
You aren't limited to the default 0, 1, 2... sequence — any enum constant can be given an explicit value with =, and every name after it (unless it also gets an explicit value) continues counting up from there. For example, enum HTTPStatus { StatusOK = 200, StatusNotFound = 404, StatusServerError = 500 }; gives each constant the exact numeric value that matches a real-world protocol code, which is essential when the numbers themselves carry external meaning (like matching values from a file format, a hardware register, or a network protocol).
If only some constants get explicit values, the ones without an explicit value simply resume counting from the last explicit value plus one: enum Weekday { Sunday = 1, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday }; gives Sunday the value 1, and Monday through Saturday automatically become 2 through 7, without needing to write out every number by hand.
Explicit values are also how C represents bitmask-style enums, where each constant is a distinct power of two (1, 2, 4, 8...) so that multiple flags can be combined with the bitwise OR operator into a single integer — a pattern Objective-C's NS_OPTIONS macro (a sibling of NS_ENUM covered later) formalizes for exactly this use case.
• enum HTTPStatus { StatusOK = 200, StatusNotFound = 404 }; — explicit values matching external meaning. • Unassigned names after an explicit value resume counting up from that value plus one. • Sunday = 1, Monday, ..., Saturday yields 1 through 7 without writing every number. • Powers-of-two explicit values (1, 2, 4, 8...) enable combinable bitmask-style enums, later formalized by NS_OPTIONS.
Use Cases: Representing a Fixed Set of States
The single best use case for an enum is exactly the DeviceState example: any variable that can only ever be one of a small, known, fixed set of named possibilities, where using a plain int or a plain string would either lose meaning (what does 2 mean without looking it up?) or waste memory and comparison speed (comparing strings is far slower than comparing integers). State machines, parser modes, error/result categories, days of the week, directions (North/South/East/West), and command types are all classic enum territory.
A switch statement over an enum reads almost like prose and lets the compiler help catch mistakes: switch (state) { case StateIdle: ...; case StateRunning: ...; case StateError: ...; } clearly enumerates every handled case by name. Many compilers can even warn you if a switch over an enum fails to handle one of its defined cases, catching a class of bug before the program ever runs — a genuinely valuable safety net that plain integers don't give you.
It's worth being honest about the limits of plain C enums, since this sets up exactly why Objective-C later adds NS_ENUM on top: in strict C, nothing stops you from assigning DeviceState s = 47; even though 47 isn't one of the three defined states, because underneath it's still just an int and the compiler mostly doesn't enforce membership. This looseness is a real gap — it's exactly the gap NS_ENUM (covered in a later Objective-C-specific lesson) closes, by wrapping this same underlying C enum pattern with stronger typing support recognized by Xcode's tooling and the compiler's static analysis.
• Best fit: a variable that's always exactly one of a small, known, fixed set of named possibilities. • Enums make switch statements read clearly and let some compilers warn about unhandled cases. • Comparing/switching on an int-backed enum constant is fast — no string comparison needed. • Plain C enums don't strictly enforce membership (DeviceState s = 47; compiles) — this gap is exactly what NS_ENUM later addresses.
Enum Definition, typedef, and Explicit Values
typedef'd DeviceState needs no enum keyword to declare a variable; plain enum HTTPStatus still does, since it wasn't typedef'd here. Comments show each constant's actual underlying integer value in the default 0, 1, 2... sequence.
typedef enum {
StateIdle, // 0
StateRunning, // 1
StateError // 2
} DeviceState;
enum HTTPStatus {
StatusOK = 200,
StatusNotFound = 404,
StatusServerError = 500
};
DeviceState d = StateRunning;
enum HTTPStatus code = StatusNotFound;Code Examples
With no explicit values assigned, the compiler numbers the constants starting at 0 and counting up by one for each subsequent name, proving that an enum constant is, underneath the readable name, just a plain int. Expected result: StateIdle = 0 StateRunning = 1 StateError = 2 d = 1
Switching on an enum reads clearly by name rather than by magic number, and many compilers can warn if a case for one of the enum's defined values is ever left unhandled. Expected result: Device has an error
When the underlying numeric values matter externally (here, real HTTP status codes), explicit assignment ties each name directly to the exact number other systems expect, rather than an arbitrary 0-based sequence. Expected result: Status code: 404
#include <stdio.h>
typedef enum {
StateIdle,
StateRunning,
StateError
} DeviceState;
int main(void) {
DeviceState d = StateRunning;
printf("StateIdle = %d\n", StateIdle);
printf("StateRunning = %d\n", StateRunning);
printf("StateError = %d\n", StateError);
printf("d = %d\n", d);
return 0;
}#include <stdio.h>
typedef enum {
StateIdle,
StateRunning,
StateError
} DeviceState;
void describe(DeviceState state) {
switch (state) {
case StateIdle:
printf("Device is idle\n");
break;
case StateRunning:
printf("Device is running\n");
break;
case StateError:
printf("Device has an error\n");
break;
}
}
int main(void) {
describe(StateError);
return 0;
}#include <stdio.h>
enum HTTPStatus {
StatusOK = 200,
StatusNotFound = 404,
StatusServerError = 500
};
int main(void) {
enum HTTPStatus code = StatusNotFound;
printf("Status code: %d\n", code);
return 0;
}Important Rules
Pair enum with typedef so callers can use the bare type name without repeating enum everywhere.
Use explicit values whenever the numbers themselves carry external meaning (protocol codes, file formats, hardware flags).
Prefer an enum over a plain int or string whenever a variable is always exactly one of a small, fixed, known set of options.
Don't rely on plain C enums to reject invalid values — they don't; treat that as a real gap that NS_ENUM later helps with in Objective-C.
Give enum constants a shared, descriptive prefix (like StateIdle, StateRunning) to avoid naming collisions across different enums.
State Machines in Real Apps
A typical networking layer in an app models a request's lifecycle with an enum much like DeviceState — RequestIdle, RequestLoading, RequestSucceeded, RequestFailed — and switches over it to decide whether to show a spinner, render data, or display an error message, keeping the UI logic exhaustive and easy to reason about.
enum DeviceState { StateIdle, StateRunning, StateError }; DeviceState d = StateIdle; // error: needs 'enum' keyword in plain C typedef enum { StateIdle, StateRunning, StateError } DeviceState; DeviceState d = StateIdle;
Without a typedef, plain C requires writing enum DeviceState d = StateIdle; — the bare type name DeviceState only works because the typedef gave it that alias.
// Assuming enum values start at 1 by default enum Direction { North, South, East, West }; if (North == 1) { /* never true */ } enum Direction { North, South, East, West }; if (North == 0) { /* North really is 0 by default */ }
Unless explicit values are assigned, C enums start counting from 0, not 1 — a frequent off-by-one assumption error for programmers coming from languages or conventions where 1-based numbering feels more natural.
Given enum Size { Small, Medium, Large }; with no explicit values, what integer does Medium hold? A. 0 B. 1 C. 2 D. It's undefined until assigned
B. 1 — By default the first name (Small) is 0, and each following name increases by one, so Medium is 1 and Large is 2.
In enum Weekday { Sunday = 1, Monday, Tuesday };, what value does Tuesday get? A. 0 B. 1 C. 2 D. 3
C. 2 — Sunday is explicitly 1, and each subsequent unassigned name counts up by one from the previous value, so Monday becomes 2 and Tuesday becomes 3.
What is true about a plain C enum constant at the machine level? A. It's a distinct runtime type that the compiler strictly validates against the enum's defined members B. It's ultimately just a named integer constant, with no special runtime type-safety beyond the name C. It's stored as a string internally D. It can only be used inside switch statements
B. It's ultimately just a named integer constant, with no special runtime type-safety beyond the name — C enums are, underneath the readable names, plain integers; the compiler does not stop an enum-typed variable from holding a value outside its defined set. This looseness is exactly what NS_ENUM later tightens in Objective-C.
Which Objective-C feature is described as the direct descendant of plain C enums, adding type safety on top of the same pattern? A. NSValue B. NS_ENUM C. typedef D. CGRect
B. NS_ENUM — NS_ENUM is a macro built directly on top of C's enum mechanism, adding stronger typing that Xcode and the compiler can check, while still fundamentally relying on the same named-integer-constant pattern taught in this lesson.
Define a typedef'd enum called TrafficLight with values LightRed, LightYellow, LightGreen (default sequential values). Write a function const char *describeLight(TrafficLight light) that returns "Stop", "Caution", or "Go" via a switch statement, and call it from main() for LightYellow, printing the result.
Remember switch cases use the bare constant names (LightRed, not TrafficLight.LightRed — C enums aren't scoped that way).