C Unions and Enums: Complete Guide with Examples
Table of Contents
Enums — Named Constants
Magic numbers make code unreadable. What does if (status == 3) mean? Is 3 success? Error? Pending? C enums replace meaningless numbers with descriptive names, making your code self-documenting and less error-prone.
#include <stdio.h>
// Define an enumeration
enum Color {
RED, // 0
GREEN, // 1
BLUE, // 2
YELLOW, // 3
WHITE // 4
};
int main(void) {
enum Color favorite = BLUE;
if (favorite == BLUE) {
printf("Your favorite color is blue\n");
}
// Enums are integers underneath
printf("RED = %d, GREEN = %d, BLUE = %d\n", RED, GREEN, BLUE);
// Output: RED = 0, GREEN = 1, BLUE = 2
return 0;
}
By default, enum values start at 0 and increment by 1. Each constant is just a named int — the compiler replaces every RED with 0, every GREEN with 1, and so on.
Enum Values & Control
You can assign specific values to enum constants:
// Explicit values
enum HttpStatus {
HTTP_OK = 200,
HTTP_CREATED = 201,
HTTP_BAD_REQUEST = 400,
HTTP_UNAUTHORIZED = 401,
HTTP_FORBIDDEN = 403,
HTTP_NOT_FOUND = 404,
HTTP_SERVER_ERROR = 500
};
// Mixed — assigned and auto-incremented
enum LogLevel {
LOG_TRACE = 0,
LOG_DEBUG = 10,
LOG_INFO = 20,
LOG_WARN = 30,
LOG_ERROR = 40,
LOG_FATAL = 50
};
// Count pattern — last element gives total count
enum Direction {
DIR_NORTH,
DIR_SOUTH,
DIR_EAST,
DIR_WEST,
DIR_COUNT // = 4, equals the number of real directions
};
// Use DIR_COUNT for array sizes
const char *dir_names[DIR_COUNT] = {
"North", "South", "East", "West"
};
The DIR_COUNT pattern is extremely common in C programming. By placing a count sentinel at the end, you always have the array size available and it updates automatically when you add new values.
Enum Patterns
Enum with Switch
#include <stdio.h>
typedef enum {
SHAPE_CIRCLE,
SHAPE_RECTANGLE,
SHAPE_TRIANGLE,
SHAPE_COUNT
} Shape;
const char *shape_name(Shape s) {
switch (s) {
case SHAPE_CIRCLE: return "Circle";
case SHAPE_RECTANGLE: return "Rectangle";
case SHAPE_TRIANGLE: return "Triangle";
default: return "Unknown";
}
}
int main(void) {
for (Shape s = 0; s < SHAPE_COUNT; s++) {
printf("Shape %d: %s\n", s, shape_name(s));
}
return 0;
}
Bit Flag Enums
Enums with power-of-two values work as bit flags, allowing combinations with bitwise OR:
#include <stdio.h>
typedef enum {
PERM_NONE = 0, // 0000
PERM_READ = 1 << 0, // 0001
PERM_WRITE = 1 << 1, // 0010
PERM_EXECUTE = 1 << 2, // 0100
PERM_DELETE = 1 << 3, // 1000
PERM_ALL = 0xF // 1111
} Permission;
void check_permissions(unsigned int perms) {
printf("Permissions: ");
if (perms & PERM_READ) printf("READ ");
if (perms & PERM_WRITE) printf("WRITE ");
if (perms & PERM_EXECUTE) printf("EXECUTE ");
if (perms & PERM_DELETE) printf("DELETE ");
printf("\n");
}
int main(void) {
unsigned int user_perms = PERM_READ | PERM_WRITE;
unsigned int admin_perms = PERM_ALL;
printf("User: ");
check_permissions(user_perms);
printf("Admin: ");
check_permissions(admin_perms);
// Add permission
user_perms |= PERM_EXECUTE;
printf("User after promotion: ");
check_permissions(user_perms);
// Remove permission
user_perms &= ~PERM_DELETE;
printf("User without delete: ");
check_permissions(user_perms);
return 0;
}
This pattern is used everywhere in systems programming — file permissions, window styles, network flags, hardware registers.
Unions — Shared Memory
A union looks like a struct, but with one critical difference: all members share the same memory location. Only one member can hold a valid value at a time. The union’s size equals the size of its largest member.
#include <stdio.h>
union Data {
int integer; // 4 bytes
float decimal; // 4 bytes
char string[20]; // 20 bytes
};
// sizeof(union Data) == 20 (size of largest member)
int main(void) {
union Data d;
d.integer = 42;
printf("Integer: %d\n", d.integer);
d.decimal = 3.14f;
printf("Float: %.2f\n", d.decimal);
printf("Integer (corrupted): %d\n", d.integer);
// integer is now garbage — it was overwritten by decimal
return 0;
}
Think of a union as a single parking spot that can hold a car, a truck, or a motorcycle — but only one at a time. The spot is big enough for the largest vehicle.
Union Memory Layout
#include <stdio.h>
union Example {
char c; // 1 byte
int i; // 4 bytes
double d; // 8 bytes
};
int main(void) {
union Example u;
printf("Size of union: %zu\n", sizeof(u)); // 8 (size of double)
// All members share the same address
printf("Address of c: %p\n", (void *)&u.c);
printf("Address of i: %p\n", (void *)&u.i);
printf("Address of d: %p\n", (void *)&u.d);
// All three print the SAME address
return 0;
}
Struct vs Union Memory
| Feature | Struct | Union |
|---|---|---|
| Memory | Sum of all members (+ padding) | Size of largest member |
| Active members | All at once | One at a time |
| Access | All members valid simultaneously | Only last-written member is valid |
| Use case | Group related data | Store one of several types |
Tagged Unions
The most powerful pattern combining enums and unions is the tagged union (also called a discriminated union or variant). An enum tag tells you which union member is currently valid:
#include <stdio.h>
#include <string.h>
typedef enum {
VAL_INT,
VAL_FLOAT,
VAL_STRING,
VAL_BOOL
} ValueType;
typedef struct {
ValueType type; // The "tag" — tells us which member to read
union {
int int_val;
float float_val;
char string_val[64];
int bool_val;
} data;
} Value;
// Type-safe creation functions
Value make_int(int v) {
Value val = { .type = VAL_INT, .data.int_val = v };
return val;
}
Value make_float(float v) {
Value val = { .type = VAL_FLOAT, .data.float_val = v };
return val;
}
Value make_string(const char *s) {
Value val = { .type = VAL_STRING };
strncpy(val.data.string_val, s, 63);
val.data.string_val[63] = '\0';
return val;
}
Value make_bool(int b) {
Value val = { .type = VAL_BOOL, .data.bool_val = b };
return val;
}
void print_value(const Value *v) {
switch (v->type) {
case VAL_INT:
printf("Int: %d\n", v->data.int_val);
break;
case VAL_FLOAT:
printf("Float: %.2f\n", v->data.float_val);
break;
case VAL_STRING:
printf("String: \"%s\"\n", v->data.string_val);
break;
case VAL_BOOL:
printf("Bool: %s\n", v->data.bool_val ? "true" : "false");
break;
}
}
int main(void) {
Value values[] = {
make_int(42),
make_float(3.14f),
make_string("Hello, world!"),
make_bool(1)
};
int count = sizeof(values) / sizeof(values[0]);
for (int i = 0; i < count; i++) {
printf("[%d] ", i);
print_value(&values[i]);
}
return 0;
}
Tagged unions are how dynamically-typed languages implement variables internally. Python’s PyObject, JavaScript V8’s value representation, and Lua’s TValue all use this pattern. Every time you write x = 42 in Python, a tagged union somewhere stores the integer value alongside a type tag.
Bit Fields
Bit fields let you specify exact bit widths for struct members, packing multiple values into minimal space:
#include <stdio.h>
// TCP flags — each only needs 1 bit
typedef struct {
unsigned int fin : 1;
unsigned int syn : 1;
unsigned int rst : 1;
unsigned int psh : 1;
unsigned int ack : 1;
unsigned int urg : 1;
unsigned int reserved : 2;
} TcpFlags;
// Date packed into minimal space
typedef struct {
unsigned int day : 5; // 1-31 (5 bits)
unsigned int month : 4; // 1-12 (4 bits)
unsigned int year : 12; // 0-4095 (12 bits)
} PackedDate;
int main(void) {
TcpFlags flags = {
.syn = 1,
.ack = 1
};
printf("SYN-ACK: syn=%u, ack=%u, fin=%u\n",
flags.syn, flags.ack, flags.fin);
printf("Size of TcpFlags: %zu bytes\n", sizeof(TcpFlags)); // 4
PackedDate today = { .day = 26, .month = 5, .year = 2026 };
printf("Date: %u/%u/%u\n", today.month, today.day, today.year);
printf("Size of PackedDate: %zu bytes\n", sizeof(PackedDate)); // 4
return 0;
}
Bit fields are used extensively in embedded systems, network protocol headers, and hardware register definitions where memory is precious and bit-level control is essential.
Real-World Examples
Event System
typedef enum {
EVENT_KEYPRESS,
EVENT_MOUSE_CLICK,
EVENT_MOUSE_MOVE,
EVENT_WINDOW_RESIZE
} EventType;
typedef struct {
EventType type;
unsigned int timestamp;
union {
struct { int key; int modifiers; } keyboard;
struct { int x; int y; int button; } click;
struct { int x; int y; int dx; int dy; } motion;
struct { int width; int height; } resize;
};
} Event;
void handle_event(const Event *e) {
switch (e->type) {
case EVENT_KEYPRESS:
printf("Key pressed: %d\n", e->keyboard.key);
break;
case EVENT_MOUSE_CLICK:
printf("Click at (%d, %d)\n", e->click.x, e->click.y);
break;
case EVENT_MOUSE_MOVE:
printf("Mouse moved to (%d, %d)\n", e->motion.x, e->motion.y);
break;
case EVENT_WINDOW_RESIZE:
printf("Window resized to %dx%d\n", e->resize.width, e->resize.height);
break;
}
}
Configuration Value
typedef enum { CFG_INT, CFG_FLOAT, CFG_STRING, CFG_BOOL } CfgType;
typedef struct {
char key[32];
CfgType type;
union {
int ival;
float fval;
char sval[128];
int bval;
};
} ConfigEntry;
ConfigEntry config[] = {
{ "port", CFG_INT, .ival = 8080 },
{ "timeout", CFG_FLOAT, .fval = 30.0f },
{ "hostname", CFG_STRING, .sval = "localhost" },
{ "debug", CFG_BOOL, .bval = 0 },
};
Practice Exercises
Exercise 1: State Machine
Implement a traffic light state machine using an enum for states (RED, YELLOW, GREEN) and a function that transitions between them. Print the current state and duration at each step.
Exercise 2: Variant Type
Extend the tagged union Value type to support arrays (a dynamically allocated array of Values). Implement make_array, array_push, array_get, and print_value that handles nested arrays recursively.
Exercise 3: Network Packet Parser
Define structs with bit fields for an Ethernet frame header and IPv4 header. Parse a hardcoded byte array into these structs and print all fields with human-readable labels.
Enums and unions round out C’s type system — enums give meaning to numbers, unions share memory between alternatives, and together they create the tagged union pattern used by nearly every language runtime and systems program. With structs, enums, and unions in your toolkit, you can model any data structure the real world throws at you.