Why this matters
Programs are full of values that come from a small fixed set: the state of a connection (connecting, open, closing, closed), the kind of a token (number, identifier, operator), a direction, a log level. Using bare numbers like 2 for "closing" makes code unreadable and fragile. Enums give such values names. typedef gives any type a new name, which C programmers use to shorten struct types, to make function pointer types readable, and to hide implementation details. Both show up in essentially every real C code base.
Enumerations
#include <stdio.h>
enum color { RED, GREEN, BLUE, COLOR_COUNT };
static const char *color_names[COLOR_COUNT] = { "red", "green", "blue" };
enum http_status { HTTP_OK = 200, HTTP_NOT_FOUND = 404, HTTP_TEAPOT = 418 };
const char *describe(enum color c)
{
switch (c) {
case RED: return "warm";
case GREEN: return "natural";
}
return "?";
}
int main(void)
{
for (enum color c = RED; c < COLOR_COUNT; c++)
printf("%d %s %s\n", c, color_names[c], describe(c));
printf("sizeof(enum color) = %zu\n", sizeof(enum color));
enum http_status s = HTTP_NOT_FOUND;
printf("status %d\n", s);
enum color oops = 42; /* compiles: enums are just ints */
printf("oops = %d\n", oops);
return 0;
}
0 red warm
1 green natural
2 blue ?
sizeof(enum color) = 4
status 404
oops = 42
What the declaration does:
enum color { RED, GREEN, BLUE, COLOR_COUNT };defines a typeenum colorand four enumeration constants. They are numbered from 0 unless you say otherwise:REDis 0,GREEN1,BLUE2,COLOR_COUNT3.- You can assign values explicitly, as in
HTTP_NOT_FOUND = 404. A constant without a value is one more than the previous one. - The constants are plain
intconstants, usable anywhere an integer constant is: array sizes,caselabels, arithmetic. - An
enumvariable is stored as an integer (4 bytes with gcc here).
Enums in C are not type-safe. enum color oops = 42; compiles without a warning, and you can pass an enum http_status where an enum color is expected. The names are for humans and for the compiler's switch checking, nothing more. (C++ and Rust are stricter.)
The count sentinel and lookup tables
Two idioms in the example deserve attention.
The sentinel. Adding COLOR_COUNT as the last constant gives you the number of real values for free. Use it to size arrays and bound loops; when you add a color before it, the count updates itself.
Lookup tables. Because the constants are 0, 1, 2, ..., an array indexed by the enum maps each value to data: names, sizes, handlers. color_names[c] turns a color into text. Keep the table and the enum in the same order (and next to each other in the source) so they cannot drift apart.
enum value: RED(0) GREEN(1) BLUE(2)
| | |
color_names: [ "red" , "green" , "blue" ]
switch and -Wswitch
switch over an enum is where enums earn their keep. Compiling the example with -Wall gives:
e1.c:11:5: warning: enumeration value 'BLUE' not handled in switch [-Wswitch]
e1.c:11:5: warning: enumeration value 'COLOR_COUNT' not handled in switch [-Wswitch]
When a switch on an enum variable has no default label, gcc checks that every constant has a case. This is extremely valuable when a program grows: add a new value to the enum and the compiler lists every switch you forgot to update. Adding a default: silences the check, so many style guides recommend no default in switches over enums and a separate return or error after the switch for impossible values. (The sentinel shows up in the warning too; handle it with a case COLOR_COUNT: that falls through to the error path.)
typedef: new names for types
typedef creates an alias for an existing type:
typedef unsigned char byte; /* byte now means unsigned char */
typedef struct point Point; /* Point now means struct point */
Syntactically, a typedef looks exactly like a variable declaration with typedef in front, and the name being "declared" becomes the type's new name. That rule makes even complicated typedefs readable.
The commonest use is to drop the struct keyword, often combined with the definition:
#include <stdio.h>
typedef struct {
double x, y;
} Vec2;
typedef double (*unary_fn)(double);
Vec2 vec2_add(Vec2 a, Vec2 b)
{
return (Vec2){ a.x + b.x, a.y + b.y };
}
double twice(double v) { return 2 * v; }
double square(double v) { return v * v; }
Vec2 vec2_map(Vec2 v, unary_fn f)
{
return (Vec2){ f(v.x), f(v.y) };
}
int main(void)
{
Vec2 a = { 1.5, 2.0 }, b = { 0.5, -1.0 };
Vec2 c = vec2_add(a, b);
printf("c = (%g, %g)\n", c.x, c.y);
unary_fn ops[] = { twice, square };
for (int i = 0; i < 2; i++) {
Vec2 m = vec2_map(c, ops[i]);
printf("op %d: (%g, %g)\n", i, m.x, m.y);
}
return 0;
}
c = (2, 1)
op 0: (4, 2)
op 1: (4, 1)
New pieces:
typedef struct { ... } Vec2;defines an anonymous struct (no tag) and names itVec2. A struct that must point to itself, like a linked-list node, needs a tag, because the typedef name does not exist yet inside the braces:typedef struct node { int value; struct node *next; } Node;typedef double (*unary_fn)(double);names a function pointer type. Comparedouble (*f)(double)written out in every parameter list: the typedef is far easier to read.(Vec2){ a.x + b.x, a.y + b.y }is a compound literal (C99): an unnamed value of typeVec2built on the spot. It lets you return or pass a struct without a temporary variable.
When to typedef
Opinions differ, and you will see both styles:
- Many projects typedef every struct (
Vec2,Shape) for brevity, and it is the norm in libraries such as SDL and GLib. - The Linux kernel style guide forbids typedefs for structs: writing
struct task_struct *reminds the reader that it is a struct, and that copying it may be expensive. - Everyone agrees typedefs are good for function pointer types and for opaque types: a header declares
typedef struct parser Parser;without revealing the members, so users can only holdParser *pointers and call your functions. The standardFILEtype is the classic example; you will build one in the multi-file module.
A naming caution: POSIX reserves type names ending in _t (size_t, pid_t, uint32_t). Naming your own types point_t is common and usually harmless, but strictly speaking those names belong to the system.
Common mistakes
- Enum and table out of sync. Inserting a constant in the middle of the enum without inserting its name at the same position shifts every name after it. Keep them adjacent, and consider
static const char *names[COLOR_COUNT]so a missing entry is at least NULL rather than out of bounds. - Using an enum value as an index without checking. Since any int fits in an enum variable,
color_names[c]with a corruptedcreads out of bounds. default:in every switch, which hides the-Wswitchwarning exactly when you need it.- Duplicate constants across enums. Enum constants share one namespace with variables and functions in their scope: two enums cannot both have
RED. Prefixes (COLOR_RED,LIGHT_RED) avoid collisions. - Self-referencing an anonymous typedef:
typedef struct { Node *next; } Node;fails withunknown type name 'Node'; give the struct a tag.
Recap
enum name { A, B, C };creates named int constants numbered from 0 (or as given); enum variables are ints, with no type checking.- A trailing
COUNTconstant sizes arrays and loops; lookup tables indexed by the enum map values to names and data. switchwithoutdefaultover an enum lets-Wswitchcatch unhandled values.typedefnames a type; write it like a variable declaration withtypedefin front. Use it for structs (by preference), function pointer types and opaque types.- Compound literals
(Type){ ... }create unnamed struct values inline.
Going further: look at /usr/include/x86_64-linux-gnu/bits/confname.h in the sandbox for a large real enum (the names accepted by pathconf and sysconf), and notice how each constant is followed by #define NAME NAME, a trick that lets code test for it with #ifdef (the preprocessor module explains why that is needed).