C Enums

An enum in C is a user-defined type that gives names to a fixed set of integer constants. Enums are useful when a value should represent one choice from a small list, such as a status, direction, menu option, or mode. They make code easier to read than plain numbers because STATUS_DONE explains intent better than 2.

Overview: How Enums Work

An enum definition creates an enumeration type and a set of named constants called enumerators. Each enumerator has an integer value. By default, the first value is 0, the next is 1, and so on, unless you assign values yourself.

In C, enum constants behave like integer constants. This is powerful because you can use them in switch statements, array indexes, comparisons, and initializers. It also means C enums are not as strict as enums in some other languages. A variable of an enum type can often be assigned an integer value by the compiler, even if that value does not match one of the named enumerators. Good C programs still treat the enum names as the valid set.

The compiler chooses an integer type compatible with the enum type that can represent its values. You should not write code that depends on a specific enum size. Use enums for meaning, not for exact storage layout, unless you are working with a carefully documented compiler or binary interface.

Enums work especially well with structures. A structure can contain an enum member to describe an object’s state without using vague integers or strings. For example, an order structure might contain enum OrderStatus status;, making the status field compact, readable, and easy to compare.

Syntax

enum TypeName {
    NAME_ONE,
    NAME_TWO,
    NAME_THREE = 10
};
Part Meaning
enum The keyword that begins an enumeration declaration.
TypeName The enum tag. The full type name is enum TypeName.
NAME_ONE An enumerator, which is a named integer constant.
= 10 An optional explicit value. The next unnamed value continues from it.
Semicolon Required after the closing brace because the enum definition is a declaration.

You can declare a variable with enum TypeName variable;. Many C codebases use uppercase enumerator names to show that they are constants. If you use typedef, you can create a shorter type name, such as typedef enum { OFF, ON } SwitchState;.

Examples

Basic Enum Values

#include <stdio.h>

enum Direction {
    NORTH,
    EAST,
    SOUTH,
    WEST
};

int main(void) {
    enum Direction heading = EAST;

    printf("Heading value: %d
", heading);

    if (heading == EAST) {
        printf("Move one step to the right
");
    }

    return 0;
}

Output:

Heading value: 1
Move one step to the right

NORTH receives value 0, EAST receives value 1, SOUTH receives value 2, and WEST receives value 3. The program could have used the number 1, but EAST makes the meaning clear.

Explicit Values for Real Codes

#include <stdio.h>

enum HttpStatus {
    HTTP_OK = 200,
    HTTP_CREATED = 201,
    HTTP_BAD_REQUEST = 400,
    HTTP_NOT_FOUND = 404
};

int main(void) {
    enum HttpStatus status = HTTP_NOT_FOUND;

    printf("Status code: %d
", status);

    if (status >= 400) {
        printf("Request failed
");
    }

    return 0;
}

Output:

Status code: 404
Request failed

Explicit values are useful when the numbers already have an external meaning, such as protocol codes, hardware register values, or file format constants. If an enumerator after an explicit value does not provide its own value, it receives the previous value plus one.

Enums with switch

#include <stdio.h>

enum TaskState {
    TASK_NEW,
    TASK_RUNNING,
    TASK_PAUSED,
    TASK_DONE
};

const char *state_name(enum TaskState state) {
    switch (state) {
        case TASK_NEW:
            return "new";
        case TASK_RUNNING:
            return "running";
        case TASK_PAUSED:
            return "paused";
        case TASK_DONE:
            return "done";
        default:
            return "unknown";
    }
}

int main(void) {
    enum TaskState state = TASK_RUNNING;

    printf("Task is %s
", state_name(state));

    state = TASK_DONE;
    printf("Task is %s
", state_name(state));

    return 0;
}

Output:

Task is running
Task is done

A switch is a natural partner for an enum because it lists behavior for each named state. The default case protects the function if it receives an integer value that is not one of the named enum constants.

Enum Members Inside Structures

#include <stdio.h>

enum Priority {
    PRIORITY_LOW = 1,
    PRIORITY_NORMAL = 2,
    PRIORITY_HIGH = 3
};

struct Ticket {
    int id;
    enum Priority priority;
    int open;
};

const char *priority_label(enum Priority priority) {
    switch (priority) {
        case PRIORITY_LOW:
            return "low";
        case PRIORITY_NORMAL:
            return "normal";
        case PRIORITY_HIGH:
            return "high";
        default:
            return "invalid";
    }
}

int main(void) {
    struct Ticket ticket = {1042, PRIORITY_HIGH, 1};

    printf("Ticket #%d
", ticket.id);
    printf("Priority: %s
", priority_label(ticket.priority));
    printf("Open: %s
", ticket.open ? "yes" : "no");

    return 0;
}

Output:

Ticket #1042
Priority: high
Open: yes

The structure stores the ticket’s priority as an enum instead of a magic number. This keeps the structure compact while still making code that reads or writes the field easy to understand.

How It Works Step by Step

When the compiler reads an enum definition, it assigns integer values to each enumerator. If no value is provided, the first enumerator is 0. Each following enumerator gets the previous value plus one, except when you provide another explicit value.

After that, the enumerator names can be used anywhere an integer constant expression is allowed. For example, you can use them in case labels, array sizes, and initializers. The enum type itself can be used for variables, function parameters, return values, and structure members.

At runtime, an enum variable stores an integer representation. Comparing state == TASK_DONE is an integer comparison. Passing an enum to a function passes that stored value. A switch on an enum checks the stored value against the integer values assigned to the case labels.

Because C permits close interaction between enums and integers, the compiler usually cannot guarantee that an enum variable contains only a named value. That is why validation and default handling are important when enum values come from user input, files, network data, or casts.

Common Mistakes

Forgetting the Semicolon

An enum definition must end with a semicolon: enum Color { RED, GREEN, BLUE };. Without it, the next declaration often produces a confusing compiler error because the parser is still expecting the enum declaration to finish.

Assuming Every Integer Is Valid

This program uses a defensive function so unexpected enum values do not silently look valid.

#include <stdio.h>

enum Color {
    COLOR_RED,
    COLOR_GREEN,
    COLOR_BLUE
};

const char *color_name(enum Color color) {
    switch (color) {
        case COLOR_RED:
            return "red";
        case COLOR_GREEN:
            return "green";
        case COLOR_BLUE:
            return "blue";
        default:
            return "invalid color";
    }
}

int main(void) {
    enum Color color = (enum Color)9;

    printf("%s
", color_name(color));
    return 0;
}

Output:

invalid color

The cast creates an enum variable containing a value outside the named set. A program that reads numeric data and converts it to an enum should check that the value is allowed before trusting it.

Reusing Enumerator Names in One Scope

Unscoped enumerator names share the surrounding identifier scope. You cannot define two enums in the same scope that both contain a name like READY. Use prefixes such as FILE_READY and NETWORK_READY to avoid collisions.

Depending on Enum Size

Do not assume sizeof(enum Priority) is always the same as sizeof(int). The compatible integer type is chosen by the implementation. If exact binary layout matters, use fixed-width integer types and document the conversion between the stored value and the enum meaning.

Best Practices

  • Use enums when a variable should hold one value from a known, small set.
  • Give enumerators clear prefixes, such as TASK_ or COLOR_, to avoid name conflicts.
  • Use explicit values when the numbers have external meaning; otherwise let the compiler count from zero.
  • Handle invalid values when enums come from input, casts, files, or network data.
  • Use enums in structures for state fields instead of vague integers or string labels.
  • Prefer switch for behavior that depends on each enum value, and consider a default branch for defensive code.
  • Do not use enums as bit flags unless each value is explicitly assigned a power of two and the code documents that intent.

Practice Exercises

  1. Define an enum named Weekday with seven days. Write a function that returns whether a given day is a weekend.
  2. Create a struct Package with an id, weight, and enum shipping status. Print a readable status label.
  3. Define an enum for log levels with explicit values 10, 20, and 30. Print only messages at or above a chosen level.

Summary

  • An enum gives names to related integer constants.
  • Default enum values start at 0 and increase by one, but you can assign explicit values.
  • C enum variables store integer representations, so defensive code should handle invalid values.
  • Enums pair well with switch statements and structure fields.
  • Clear prefixes and careful validation make enums safer in larger C programs.