Enums: Named Constants

Some things in reality can only be one of a limited set of options. A traffic light is red, yellow, or green - never purple. A playing card’s suit is clubs, diamonds, hearts, or spades - there is no fifth suit. A day of the week is one of seven possibilities - not eight, not six.

Enums let you model this in code. When you define an enum, you’re saying: “This thing can only ever be one of these specific values. Nothing else exists.”

This is different from using an int, where any number is technically valid. With enums, you’re encoding your understanding of reality into the type system. A function that takes enum Day is saying “give me a day” - not “give me a number that I’ll interpret as a day.”

The Magic Number Problem

But there’s also a practical problem enums solve. Magic numbers are everywhere in code. What does if (status == 3) mean? Is 3 good or bad? What about set_color(2) - is that red? Blue? Who knows.

Enums give names to numbers. Instead of status == 3, you write status == GAME_OVER. Instead of set_color(2), you write set_color(COLOR_BLUE). Now anyone reading your code knows exactly what’s happening.

This connects to the DRY principle - Don’t Repeat Yourself. If you use the number 3 to mean “game over” in twenty places, and later decide it should be 4, you have twenty places to change. With an enum, you change it once. The name GAME_OVER stays the same everywhere.

So enums give you two things: they model reality as a closed set of possibilities, and they eliminate magic numbers that make code hard to read and maintain.

Closed by Design

Here’s something important: enums are intentionally closed. If you have an enum with three values, there are exactly three valid options. Want a fourth? You have to modify the enum definition itself.

This sounds like a limitation, but it’s actually a feature. When reality changes - when your game adds a new state, or your protocol adds a new message type - the enum forces you to explicitly acknowledge that change. You can’t quietly slip in a new value. You have to update the enum, and then the compiler will helpfully point out every switch statement that doesn’t handle the new case.

Compare this to using plain integers, where anyone can pass 42 and your code has to somehow deal with it. Enums say: “These are the options. If you need more options, that’s a deliberate design decision, not an accident.”

This is a trade-off. Some systems need to be open for extension without modification (a plugin architecture, for example). Enums are the opposite - they’re for when you want to lock down the possibilities and make changes visible and intentional.

Your First Enum

enum Day {
    SUNDAY,
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY
};

This creates a new type called enum Day with seven possible values. Under the hood, these are just integers:

  • SUNDAY is 0
  • MONDAY is 1
  • TUESDAY is 2
  • …and so on

But you never have to remember that. You just use the names.

Using Enums

#include <stdio.h>

enum Day {
    SUNDAY,
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY
};

int main(void) {
    enum Day today = WEDNESDAY;

    if (today == SATURDAY || today == SUNDAY) {
        printf("It's the weekend!\n");
    } else {
        printf("It's a weekday.\n");
    }

    return 0;
}

See how readable that is? today == SATURDAY is crystal clear. No need to remember that Saturday is number 6.

How Values Are Assigned

By default, enum values start at 0 and count up by 1:

enum Color {
    RED,      // 0
    GREEN,    // 1
    BLUE      // 2
};

But you can set your own values:

enum HttpStatus {
    OK = 200,
    NOT_FOUND = 404,
    SERVER_ERROR = 500
};

Once you set a value, the next ones continue counting from there:

enum Priority {
    LOW = 1,
    MEDIUM,    // 2 (continues from LOW)
    HIGH,      // 3
    CRITICAL = 10,
    EMERGENCY  // 11 (continues from CRITICAL)
};

These integer values are called the enum’s underlying values or backing values. The name (LOW, MEDIUM) is what you use in code; the underlying value is what the compiler actually stores and compares.

This distinction matters: some languages call the position in declaration order the “ordinal” (LOW is 0th, MEDIUM is 1st), but that’s different from the underlying value (LOW is 1, MEDIUM is 2). In C, you mostly work with names and let the compiler worry about values - unless you’re interfacing with external systems that expect specific numbers, like HTTP status codes or hardware registers.

The typedef Shortcut

Just like with structs, typing enum Day everywhere gets old. Use typedef to create a shorter name:

typedef enum {
    SUNDAY,
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY
} Day;

int main(void) {
    Day today = FRIDAY;  // Much cleaner than "enum Day today"
    return 0;
}

This is the pattern you’ll see in most real C code.

Enums and Switch Statements

Enums work beautifully with switch statements. They’re made for each other:

#include <stdio.h>

typedef enum {
    MENU_NEW_GAME,
    MENU_LOAD_GAME,
    MENU_OPTIONS,
    MENU_QUIT
} MenuChoice;

void handle_menu(MenuChoice choice) {
    switch (choice) {
        case MENU_NEW_GAME:
            printf("Starting new game...\n");
            break;
        case MENU_LOAD_GAME:
            printf("Loading saved game...\n");
            break;
        case MENU_OPTIONS:
            printf("Opening options...\n");
            break;
        case MENU_QUIT:
            printf("Goodbye!\n");
            break;
    }
}

int main(void) {
    handle_menu(MENU_NEW_GAME);
    handle_menu(MENU_QUIT);
    return 0;
}

Compare that to:

switch (choice) {
    case 0:  // What's 0? New game? Quit?
        // ...
    case 1:  // No idea what this means
        // ...
}

The enum version tells you exactly what each case does.

Enums vs #define

You might have seen constants defined with #define:

#define COLOR_RED   0
#define COLOR_GREEN 1
#define COLOR_BLUE  2

This works, but enums are better for several reasons:

1. Enums create a type. The compiler knows that Color is different from Day:

typedef enum { RED, GREEN, BLUE } Color;
typedef enum { SUNDAY, MONDAY } Day;

Color c = SUNDAY;  // Some compilers warn: mixing types!

2. Enums are easier to debug. Many debuggers show enum names instead of numbers. Instead of seeing status = 3, you see status = GAME_OVER.

3. Enums group related constants. All color values are clearly part of the same group:

// With enums - clearly grouped
typedef enum { RED, GREEN, BLUE } Color;

// With #define - just floating around
#define RED   0
#define GREEN 1
#define BLUE  2
// Are these colors? Error codes? Who knows?

When to use #define instead:

  • When you need constants that aren’t integers (like #define PI 3.14159)
  • When you need to use the value in places enums can’t go (like array sizes in older C)

For most cases, prefer enums.

Practical Example: Game States

Games are perfect for enums. A game is always in one state:

#include <stdio.h>

typedef enum {
    STATE_MENU,
    STATE_PLAYING,
    STATE_PAUSED,
    STATE_GAME_OVER,
    STATE_VICTORY
} GameState;

void print_state(GameState state) {
    switch (state) {
        case STATE_MENU:
            printf("At the main menu\n");
            break;
        case STATE_PLAYING:
            printf("Game in progress!\n");
            break;
        case STATE_PAUSED:
            printf("Game paused\n");
            break;
        case STATE_GAME_OVER:
            printf("Game Over - You lost!\n");
            break;
        case STATE_VICTORY:
            printf("Congratulations! You won!\n");
            break;
    }
}

int main(void) {
    GameState state = STATE_MENU;
    print_state(state);

    state = STATE_PLAYING;
    print_state(state);

    state = STATE_VICTORY;
    print_state(state);

    return 0;
}

Without enums, you’d have to remember that 0 is menu, 1 is playing, 2 is paused, and so on. With enums, the code documents itself.

Practical Example: Error Codes

Functions often return error codes. Enums make them meaningful:

#include <stdio.h>

typedef enum {
    ERR_NONE = 0,
    ERR_FILE_NOT_FOUND,
    ERR_PERMISSION_DENIED,
    ERR_OUT_OF_MEMORY,
    ERR_INVALID_INPUT
} ErrorCode;

ErrorCode open_file(const char *filename) {
    // Pretend we tried to open a file
    // For this example, we'll just return an error
    return ERR_FILE_NOT_FOUND;
}

int main(void) {
    ErrorCode result = open_file("data.txt");

    if (result != ERR_NONE) {
        switch (result) {
            case ERR_FILE_NOT_FOUND:
                printf("Error: File not found\n");
                break;
            case ERR_PERMISSION_DENIED:
                printf("Error: Permission denied\n");
                break;
            case ERR_OUT_OF_MEMORY:
                printf("Error: Out of memory\n");
                break;
            case ERR_INVALID_INPUT:
                printf("Error: Invalid input\n");
                break;
            default:
                printf("Error: Unknown error\n");
                break;
        }
    }

    return 0;
}

Notice ERR_NONE = 0. This is a convention - zero usually means “no error” or “success”. That way you can write if (result) to check for any error.

Practical Example: Directions

Here’s a simple example for moving around a grid:

#include <stdio.h>

typedef enum {
    DIR_UP,
    DIR_DOWN,
    DIR_LEFT,
    DIR_RIGHT
} Direction;

void move(int *x, int *y, Direction dir) {
    switch (dir) {
        case DIR_UP:
            (*y)--;
            break;
        case DIR_DOWN:
            (*y)++;
            break;
        case DIR_LEFT:
            (*x)--;
            break;
        case DIR_RIGHT:
            (*x)++;
            break;
    }
}

int main(void) {
    int x = 5, y = 5;

    printf("Starting at (%d, %d)\n", x, y);

    move(&x, &y, DIR_UP);
    printf("After moving up: (%d, %d)\n", x, y);

    move(&x, &y, DIR_RIGHT);
    move(&x, &y, DIR_RIGHT);
    printf("After moving right twice: (%d, %d)\n", x, y);

    return 0;
}

Printing Enum Names

One downside of enums: C doesn’t automatically know how to print their names. This doesn’t work:

Day today = MONDAY;
printf("%s\n", today);  // WRONG - prints garbage or crashes

You need to create a helper function:

const char* day_name(Day d) {
    switch (d) {
        case SUNDAY:    return "Sunday";
        case MONDAY:    return "Monday";
        case TUESDAY:   return "Tuesday";
        case WEDNESDAY: return "Wednesday";
        case THURSDAY:  return "Thursday";
        case FRIDAY:    return "Friday";
        case SATURDAY:  return "Saturday";
        default:        return "Unknown";
    }
}

int main(void) {
    Day today = MONDAY;
    printf("Today is %s\n", day_name(today));  // "Today is Monday"
    return 0;
}

It’s a bit of extra work, but you only write it once.

Counting Enum Values

A useful trick - add a “COUNT” value at the end:

typedef enum {
    COLOR_RED,
    COLOR_GREEN,
    COLOR_BLUE,
    COLOR_COUNT  // This equals 3 - the number of real colors
} Color;

int main(void) {
    printf("There are %d colors\n", COLOR_COUNT);

    // Loop through all colors
    for (int c = 0; c < COLOR_COUNT; c++) {
        printf("Color %d\n", c);
    }

    return 0;
}

Since enums count from 0, the last “COUNT” value automatically equals the total number of real values.

Try It Yourself

  1. Create an enum for traffic light colors (RED, YELLOW, GREEN). Write a function that takes a color and prints what drivers should do.

  2. Create an enum for playing card suits (HEARTS, DIAMONDS, CLUBS, SPADES). Write a program that prints all four suit names.

  3. Create an enum for months of the year with values 1-12 (JANUARY = 1, FEBRUARY, …). Write a function that returns how many days are in each month.

  4. Create a simple menu system with an enum. Ask the user to enter 1, 2, or 3, convert that to an enum value, and use a switch to handle each choice.

Common Mistakes

  • Forgetting the semicolon. The closing brace needs a semicolon: };

  • Treating enums as strings. printf("%s", MONDAY) doesn’t work. Enums are integers, not strings.

  • Assuming enum values. Don’t write if (status == 2) when you mean if (status == STATE_PAUSED). Even if you know 2 is PAUSED today, that might change.

  • Missing switch cases. Some compilers warn if you switch on an enum but don’t handle every value. Pay attention to those warnings.

  • Mixing enum types. Putting a Color value where a Day is expected. C allows this (they’re all integers), but it’s a bug waiting to happen.

Next Up

In Part 13, we’ll learn about structs - how to create your own data types by grouping related values together.


Enjoyed This?

If this helped something click, subscribe to my YouTube channel. More content like this, same approach - making things stick without insulting your intelligence. It’s free, it helps more people find this stuff, and it tells me what’s worth making more of.