enum and bit flags
Represent named constants with enum and combine options efficiently with bitwise OR — the bit flags pattern used throughout C APIs and system headers.
- Declare an enum and use its values as named constants
- Explain that enum values are integers under the hood
- Define a set of bit flags using powers of two
- Use bitwise OR to combine flags and bitwise AND to test them
An enum (enumeration) is a way to define a set of named integer constants. Instead of writing if (direction == 2) and wondering what 2 means, you write if (direction == SOUTH) and the code documents itself. Enums are how C APIs represent options, states, error codes, and directions.
Basic enum
#include <stdio.h>
enum Direction {
NORTH, /* 0 */
SOUTH, /* 1 */
EAST, /* 2 */
WEST /* 3 */
};
const char *direction_name(enum Direction d) {
switch (d) {
case NORTH: return "North";
case SOUTH: return "South";
case EAST: return "East";
case WEST: return "West";
default: return "Unknown";
}
}
int main(void) {
enum Direction heading = NORTH;
printf("Heading: %s\n", direction_name(heading));
return 0;
}Enum values start at 0 by default and increment by 1. You can override them:
enum HttpStatus {
HTTP_OK = 200,
HTTP_NOT_FOUND = 404,
HTTP_SERVER_ERROR = 500
};Any value not explicitly assigned follows the previous value + 1.
typedef enum
The same typedef pattern applies:
typedef enum {
STOPPED,
RUNNING,
PAUSED,
ERROR
} ProcessState;
ProcessState state = RUNNING;Now ProcessState can be used without the enum keyword.
Enums under the hood
Enum values are plain integers. This means you can accidentally compare or assign the wrong enum type and the compiler may not warn you:
enum Color { RED, GREEN, BLUE };
enum Size { SMALL, MEDIUM, LARGE };
enum Color c = SMALL; /* compiles without error -- SMALL is 0, same as RED */C's enum is weaker than Rust's or Java's. Treat enum values as typed constants for documentation purposes, but know they are integers.
Bit flags
When you need to represent multiple independent options that can be combined (e.g., file permissions: readable AND writable AND executable), bit flags are the pattern.
Define each flag as a power of two — each occupying a single bit:
typedef unsigned int FileMode;
#define MODE_NONE 0
#define MODE_READ (1 << 0) /* 1 = 0001 */
#define MODE_WRITE (1 << 1) /* 2 = 0010 */
#define MODE_EXECUTE (1 << 2) /* 4 = 0100 */Or equivalently with an enum:
typedef enum {
PERM_NONE = 0,
PERM_READ = 1 << 0, /* 1 */
PERM_WRITE = 1 << 1, /* 2 */
PERM_EXECUTE = 1 << 2 /* 4 */
} Permission;Combining flags with bitwise OR
unsigned int mode = MODE_READ | MODE_WRITE; /* 1 | 2 = 3 = 0011 */Bitwise OR sets each bit independently. MODE_READ | MODE_WRITE gives a value where both bit 0 and bit 1 are set.
Testing flags with bitwise AND
if (mode & MODE_READ) {
printf("Readable\n");
}
if (mode & MODE_WRITE) {
printf("Writable\n");
}
if (!(mode & MODE_EXECUTE)) {
printf("Not executable\n");
}mode & MODE_READ is non-zero (truthy) if the READ bit is set, zero (falsy) if not.
Setting, clearing, and toggling bits
unsigned int flags = 0;
flags |= MODE_READ; /* set the READ bit */
flags &= ~MODE_WRITE; /* clear the WRITE bit */
flags ^= MODE_EXECUTE; /* toggle the EXECUTE bit */~MODE_WRITE is the bitwise NOT of MODE_WRITE: all bits set except bit 1. AND with this clears bit 1.
A practical example: open() flags on Linux
The POSIX open() system call uses this exact pattern:
#include <fcntl.h>
int fd = open("file.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);O_RDWR, O_CREAT, and O_TRUNC are bit flags defined in <fcntl.h>. You combine them with |. The kernel tests individual bits to determine what to do.
Bit flags compact multiple booleans into one integer. Passing eight separate int parameters to indicate options is unwieldy. Passing one unsigned int with eight bits is concise and extensible. This is why bit flags appear throughout POSIX, network protocols, and graphics APIs.
Where to go next
Next: the linked list lab — you will build a singly linked list in C using structs and pointers, supporting insert, delete, and traversal.