Unions, Enums & typedef
Structures group several values together. C has three more tools for shaping your own types. An enumeration (enum) gives names to a set of related whole-number constants. A union lets several members share the same memory, so a value can be one of several kinds. And typedef gives any type a new, clearer name.
In this lesson you will use all three, combine an enum and a union into a safe "tagged union", and use typedef to make function pointers readable while sorting with qsort.
Enumerations
An enum lists named constants. By default the first is 0 and each next one is one more. Using names instead of bare numbers makes code self-explaining: status == SHIPPED is clearer than status == 2.
#include <stdio.h>
enum order_status {
PLACED, // 0
PACKED, // 1
SHIPPED, // 2
DELIVERED, // 3
};
const char *status_name(enum order_status s)
{
switch (s) {
case PLACED: return "Placed";
case PACKED: return "Packed";
case SHIPPED: return "Shipped";
case DELIVERED: return "Delivered";
}
return "Unknown";
}
int main(void)
{
enum order_status s = PACKED;
printf("Status %d: %s\n", (int) s, status_name(s));
s = DELIVERED;
printf("Status %d: %s\n", (int) s, status_name(s));
printf("Shipped or later? %s\n", s >= SHIPPED ? "yes" : "no");
return 0;
}Status 1: Packed Status 3: Delivered Shipped or later? yes
C cannot print an enum's name by itself; the program stores numbers, so you write a function such as status_name to turn them into text. When a switch on an enum has no default, -Wall warns if you forget one of the enum's values, a useful safety net when someone adds a new status later. You can also choose the values yourself: enum http_code { OK = 200, NOT_FOUND = 404 }; or enum level { LOW = 1, MEDIUM, HIGH };, which gives 1, 2 and 3.
Enum constants are true compile-time constants, so before C23 they were the standard typed alternative to #define for whole numbers, usable as array sizes and case labels.
Enums with a fixed underlying type (C23)
The compiler normally chooses an integer type for each enum, usually as big as an int. C23 lets you choose it yourself, which is useful when you store millions of values or must match a file format.
#include <stdio.h>
enum signal : unsigned char {
RED,
YELLOW,
GREEN,
};
enum old_signal {
OLD_RED,
OLD_YELLOW,
OLD_GREEN,
};
int main(void)
{
enum signal now = GREEN;
printf("C23 enum size: %zu byte\n", sizeof now);
printf("Classic enum size: %zu bytes\n", sizeof(enum old_signal));
printf("GREEN = %d\n", (int) now);
return 0;
}C23 enum size: 1 byte Classic enum size: 4 bytes GREEN = 2
The : unsigned char after the name fixes the underlying type, so every enum signal value takes one byte. The classic enum's size is chosen by the compiler; 4 bytes is typical. GCC supports this C23 feature from version 13 and Clang from version 20.
Unions: members that share memory
A union looks like a structure, but all its members start at the same address and overlap. A union is only as big as its largest member, and it holds one member's value at a time. Writing one member overwrites the others.
#include <stdio.h>
union value {
int whole;
double decimal;
char letter;
};
int main(void)
{
union value v;
printf("Size of the union: %zu bytes\n", sizeof v);
v.whole = 42;
printf("whole = %d\n", v.whole);
v.decimal = 3.75; // overwrites the same memory
printf("decimal = %.2f\n", v.decimal);
return 0;
}Size of the union: 8 bytes whole = 42 decimal = 3.75
After v.decimal = 3.75, reading v.whole would give some of the bits of the double, which is meaningless as an int. A union saves memory, but it does not remember which member is current. You must track that yourself.
Tagged unions
The safe way to use a union is to pair it with an enum, called the tag, that records which member is valid. This pattern models data that comes in several kinds, such as the ways a customer can pay:
#include <stdio.h>
typedef enum { CASH, UPI, CARD } PayKind;
typedef struct {
PayKind kind; // the tag: which union member is valid
int amount;
union {
char upi_id[40]; // valid when kind == UPI
int card_last4; // valid when kind == CARD
};
} Payment;
void print_payment(const Payment *p)
{
switch (p->kind) {
case CASH:
printf("Rs %d paid in cash\n", p->amount);
break;
case UPI:
printf("Rs %d paid by UPI from %s\n", p->amount, p->upi_id);
break;
case CARD:
printf("Rs %d paid by card ending %04d\n", p->amount, p->card_last4);
break;
}
}
int main(void)
{
Payment payments[] = {
{.kind = CASH, .amount = 250},
{.kind = UPI, .amount = 1299, .upi_id = "isha@okbank"},
{.kind = CARD, .amount = 4500, .card_last4 = 731},
};
for (size_t i = 0; i < 3; i++) {
print_payment(&payments[i]);
}
return 0;
}Rs 250 paid in cash Rs 1299 paid by UPI from isha@okbank Rs 4500 paid by card ending 0731
The union inside Payment has no name. This anonymous union (C11) lets you write p->upi_id directly instead of p->details.upi_id. Each payment uses the memory of only its own kind, and the switch on the tag makes sure the program reads the right member.
typedef: new names for types
typedef creates an alias for an existing type. It does not create a new type, only a new name. Use it to make code say what values mean, or to shorten long type names:
typedef long long Paise;makes declarations such asPaise price = 129950;self-explaining.typedef struct { ... } Payment;andtypedef enum { ... } PayKind;let you drop thestructandenumkeywords.typedef int (*Compare)(const void *, const void *);names a function pointer type, which is otherwise hard to read.
A function pointer stores the address of a function, so you can pass a function to another function. The standard library's qsort sorts any array and asks you for a comparison function:
#include <stdio.h>
#include <stdlib.h>
typedef int (*Compare)(const void *, const void *);
int ascending(const void *a, const void *b)
{
int x = *(const int *) a;
int y = *(const int *) b;
return (x > y) - (x < y);
}
int descending(const void *a, const void *b)
{
return ascending(b, a);
}
void sort_and_print(int values[], size_t n, Compare cmp, const char *label)
{
qsort(values, n, sizeof values[0], cmp);
printf("%s:", label);
for (size_t i = 0; i < n; i++) {
printf(" %d", values[i]);
}
printf("\n");
}
int main(void)
{
int marks[] = {67, 92, 45, 88, 73};
size_t n = sizeof marks / sizeof marks[0];
sort_and_print(marks, n, ascending, "Lowest first");
sort_and_print(marks, n, descending, "Highest first");
return 0;
}Lowest first: 45 67 73 88 92 Highest first: 92 88 73 67 45
qsort does not know your element type, so it passes const void * pointers, which the comparison function converts back to const int *. The function must return a negative number, zero or a positive number. (x > y) - (x < y) gives -1, 0 or 1 safely; the tempting x - y can overflow for large values. Without the Compare alias, the parameter would have to be written int (*cmp)(const void *, const void *).
Common mistakes
- Expecting
printfto show enum names. It prints numbers. Write a function or a lookup array to get text. - Reading the wrong union member. Only the member written last holds a meaningful value; use a tag.
- Forgetting to update the tag. If you change a tagged union's data, change its
kindtoo. - Comparison functions that subtract.
return x - y;can overflow; compare instead. - Thinking
typedefmakes a new type.Paiseandlong longare the same type, and the compiler will not stop you mixing them.
Key points
- An
enumnames related integer constants, starting at 0 unless you set values. - C23 lets you fix an enum's underlying type, as in
enum signal : unsigned char { ... };. - A union's members share memory; it is as large as its largest member and holds one value at a time.
- A tagged union pairs an enum tag with a union so the program always knows which member is valid.
typedefgives a type a new name; it is especially useful for structures and function pointers.qsortsorts any array using a comparison function passed as a function pointer.
Exercise
Define enum signal { RED, GREEN, YELLOW }; and an array of names for printing. Write a function enum signal next_signal(enum signal s) that returns the next state in the cycle red, then green, then yellow, then red again. Starting from RED, print the signal for five steps.
Show solution
#include <stdio.h>
enum signal { RED, GREEN, YELLOW };
const char *signal_names[] = {"Red", "Green", "Yellow"};
enum signal next_signal(enum signal s)
{
switch (s) {
case RED: return GREEN;
case GREEN: return YELLOW;
case YELLOW: return RED;
}
return RED;
}
int main(void)
{
enum signal s = RED;
for (int step = 1; step <= 5; step++) {
printf("Step %d: %s\n", step, signal_names[s]);
s = next_signal(s);
}
return 0;
}Step 1: Red Step 2: Green Step 3: Yellow Step 4: Red Step 5: Green
The enum values 0, 1 and 2 double as indexes into signal_names, a common trick for printing enum names. The switch covers every value, and the final return keeps the compiler satisfied.
Quick Quiz
1. In enum level { LOW = 1, MEDIUM, HIGH };, what is the value of HIGH?
Each constant without a value is one more than the previous one: 1, 2, 3.
2. How big is a union whose members are an int, a double and a char?
Union members overlap, so the union needs only enough room for the largest one.
3. What is the tag in a tagged union?
The tag lets code check which kind of data the union holds before reading it.
4. Which comparison function body is safe for qsort with large int values?
Subtraction can overflow, and x > y never returns a negative value; the comparison form gives -1, 0 or 1 safely.
Learn C with a mentor
Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
