Unions and Enums in C
Understand union memory layout, how to use enum and typedef enum, and practical use cases for both.
Union Basics and Memory Layout
A union allocates enough memory for its largest member, and all members share that same memory region. Only one member holds a valid value at any time — writing to one member invalidates all others. This makes unions memory-efficient for representing values that can be one of several types, but requires discipline to use safely.
#include <stdio.h>
union Data {
int i;
float f;
double d;
char bytes[8];
};
int main(void) {
union Data u;
/* sizeof equals the largest member (double = 8 bytes) */
printf("sizeof(union Data) = %zu\n", sizeof(union Data)); /* 8 */
u.i = 42;
printf("i = %d\n", u.i);
u.f = 3.14f;
printf("f = %.2f\n", u.f);
/* u.i is now meaningless — we overwrote the shared memory */
/* Inspect the raw bytes of a float — all members share the same address */
u.f = 1.0f;
printf("1.0f in hex: ");
for (int i = 0; i < (int)sizeof(float); i++) {
printf("%02X ", (unsigned char)u.bytes[i]);
}
printf("\n"); /* 00 00 80 3F (little-endian IEEE 754) */
return 0;
}
Tagged Union (Discriminated Union)
The most practical use of unions is the tagged union pattern: pair a union with an enum tag that records which member is currently valid. This gives you a type-safe variant — a value that can be an int, a float, a string, or any other type, with the type tracked explicitly at runtime.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
/* The tag enum — records which union member is active */
typedef enum {
VAL_INT,
VAL_FLOAT,
VAL_STRING,
VAL_BOOL
} ValueType;
/* The tagged union — tag + union together */
typedef struct {
ValueType type;
union {
int i;
double f;
char *s; /* heap-allocated string */
int b; /* boolean: 0 or 1 */
};
} Value;
Value make_int(int i) { return (Value){.type=VAL_INT, .i=i}; }
Value make_float(double f) { return (Value){.type=VAL_FLOAT, .f=f}; }
Value make_bool(int b) { return (Value){.type=VAL_BOOL, .b=!!b}; }
Value make_string(const char *s) {
Value v = {.type = VAL_STRING};
v.s = strdup(s);
return v;
}
void print_value(const Value *v) {
switch (v->type) {
case VAL_INT: printf("int(%d)", v->i); break;
case VAL_FLOAT: printf("float(%.4g)", v->f); break;
case VAL_STRING: printf("string(\"%s\")", v->s); break;
case VAL_BOOL: printf("bool(%s)", v->b ? "true" : "false"); break;
}
}
void free_value(Value *v) {
if (v->type == VAL_STRING) {
free(v->s);
v->s = NULL;
}
}
int main(void) {
Value vals[] = {
make_int(42),
make_float(3.14159),
make_string("hello"),
make_bool(1),
};
int n = sizeof(vals) / sizeof(vals[0]);
for (int i = 0; i < n; i++) {
print_value(&vals[i]);
printf("\n");
free_value(&vals[i]);
}
return 0;
}
Type Punning with Unions
Unions are commonly used to inspect the raw bit representation of a value — for example, examining the sign, exponent, and mantissa fields of a IEEE 754 float. This technique is called type punning.
#include <stdio.h>
#include <stdint.h>
union FloatBits {
float f;
uint32_t bits;
};
void print_float_bits(float f) {
union FloatBits u = {.f = f};
/* IEEE 754 single: 1 sign bit, 8 exponent bits, 23 mantissa bits */
printf("%.6g = 0x%08X (sign=%u exp=%u mant=%u)\n",
f,
u.bits,
(u.bits >> 31) & 0x1,
(u.bits >> 23) & 0xFF,
u.bits & 0x7FFFFF);
}
int main(void) {
print_float_bits(1.0f);
print_float_bits(-1.0f);
print_float_bits(0.5f);
print_float_bits(3.14f);
return 0;
}
Enums
An enum defines a set of named integer constants. Without enums, you’d use bare integer literals or #define constants — both of which are harder to read and provide no type safety. Enums group related constants under a single type name and make switch statements self-documenting.
#include <stdio.h>
/* Basic enum — values 0, 1, 2, 3 assigned automatically */
enum Direction { NORTH, EAST, SOUTH, WEST };
/* Custom values — useful for HTTP status codes, error codes, etc. */
enum HttpStatus {
HTTP_OK = 200,
HTTP_CREATED = 201,
HTTP_NO_CONTENT = 204,
HTTP_BAD_REQUEST = 400,
HTTP_UNAUTHORIZED = 401,
HTTP_FORBIDDEN = 403,
HTTP_NOT_FOUND = 404,
HTTP_SERVER_ERROR = 500,
};
/* Bit-flag enum — each value is a distinct power of 2, so they can be OR'd together */
enum Permission {
PERM_NONE = 0,
PERM_READ = 1 << 0, /* 1 */
PERM_WRITE = 1 << 1, /* 2 */
PERM_EXECUTE = 1 << 2, /* 4 */
PERM_ALL = PERM_READ | PERM_WRITE | PERM_EXECUTE,
};
const char *direction_name(enum Direction d) {
switch (d) {
case NORTH: return "North";
case EAST: return "East";
case SOUTH: return "South";
case WEST: return "West";
default: return "Unknown";
}
}
int main(void) {
enum Direction dir = EAST;
printf("Heading: %s\n", direction_name(dir));
/* Enum values are just integers under the hood */
printf("NORTH=%d EAST=%d SOUTH=%d WEST=%d\n", NORTH, EAST, SOUTH, WEST);
/* Combine bit flags with OR, test with AND */
int perms = PERM_READ | PERM_WRITE;
if (perms & PERM_WRITE) printf("Can write\n");
if (!(perms & PERM_EXECUTE)) printf("Cannot execute\n");
perms |= PERM_EXECUTE; /* grant execute */
perms &= ~PERM_WRITE; /* revoke write */
return 0;
}
typedef enum
typedef eliminates the need to write enum before every use, making enum types feel like first-class types. The sentinel pattern — adding a _COUNT member at the end — is especially useful for sizing arrays and validating input.
#include <stdio.h>
typedef enum {
LOG_DEBUG,
LOG_INFO,
LOG_WARNING,
LOG_ERROR,
LOG_FATAL,
LOG_LEVEL_COUNT /* sentinel: its integer value equals the number of valid levels */
} LogLevel;
/* Use the sentinel to size the lookup table — stays in sync automatically */
static const char *LEVEL_NAMES[LOG_LEVEL_COUNT] = {
"DEBUG", "INFO", "WARNING", "ERROR", "FATAL"
};
void log_message(LogLevel level, const char *msg) {
if (level < LOG_DEBUG || level >= LOG_LEVEL_COUNT) return;
printf("[%s] %s\n", LEVEL_NAMES[level], msg);
}
/* State machine — enum makes the states self-documenting */
typedef enum {
STATE_IDLE,
STATE_CONNECTING,
STATE_CONNECTED,
STATE_DISCONNECTING,
STATE_ERROR,
} ConnectionState;
const char *state_name(ConnectionState s) {
static const char *names[] = {
"IDLE", "CONNECTING", "CONNECTED", "DISCONNECTING", "ERROR"
};
if (s < 0 || s >= (int)(sizeof(names)/sizeof(names[0]))) return "UNKNOWN";
return names[s];
}
int main(void) {
log_message(LOG_INFO, "Server started");
log_message(LOG_WARNING, "High memory usage");
log_message(LOG_ERROR, "Connection refused");
ConnectionState state = STATE_IDLE;
printf("State: %s\n", state_name(state));
state = STATE_CONNECTING;
printf("State: %s\n", state_name(state));
state = STATE_CONNECTED;
printf("State: %s\n", state_name(state));
return 0;
}
A useful pattern: add a sentinel like LOG_LEVEL_COUNT or STATE_COUNT at the end of an enum. Its integer value equals the number of valid values, making it easy to size arrays and validate inputs.