C

C Fundamentals

18 lessons

Progress0%
1. Introduction to C
1What is C?
2. Variables and Data Types
1Data Types in C
3. Control Flow
ConditionalsLoops
4. Functions
Defining FunctionsRecursion
5. Arrays and Pointers
Arrays and StringsPointers
6. Memory Management
Dynamic MemoryStructs and Files
7. Preprocessor & Macros
Preprocessor DirectivesMacros & Inline Functions
8. Bitwise Operations
Bitwise OperatorsBit Flags & Masking
9. Enums, Unions & typedef
Enums & typedefUnions & Complex Types
10. Multi-file Programs
Header Files & Compilation UnitsLinkage, Storage Classes & Make
All Tutorials
CEnums, Unions & typedef
Lesson 15 of 18 min
Chapter 9 · Lesson 1

Enums & typedef

Enums & typedef

Enumerations (enum)

An enum defines a set of named integer constants. Compared to bare #define constants, enums are scoped, type-checkable (with compiler warnings), and visible in debuggers:

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

By default the first enumerator is 0, and each subsequent one increments by 1. You can assign explicit values:

c
enum HttpStatus {
    HTTP_OK           = 200,
    HTTP_NOT_FOUND    = 404,
    HTTP_SERVER_ERROR = 500
};

Enums are integers under the hood — you can use them in switch statements, arithmetic, and comparisons. An unnamed enum is legal and is often used purely to define integer constants without the enum tag:

c
enum { MAX_RETRIES = 3, TIMEOUT_MS = 5000 };

typedef — Creating Type Aliases

typedef introduces an alias for an existing type, improving readability and portability:

c
typedef unsigned long long uint64;
typedef int (*CompareFunc)(const void *, const void *);

The C99 idiomatic pattern for structs uses typedef to avoid writing struct everywhere:

c
typedef struct {
    int x;
    int y;
} Point;

Point p = {3, 4};   /* instead of: struct Point p = {3, 4}; */

For a struct that must refer to itself (linked list node), the tag is needed:

c
typedef struct Node {
    int value;
    struct Node *next;   /* must use struct Node here, not just Node */
} Node;

typedef for Function Pointers

Function pointer syntax in C is notoriously hard to read. typedef is a lifesaver:

c
/* Without typedef */
void (*callback)(int, const char *);

/* With typedef */
typedef void (*Callback)(int, const char *);
Callback cb;

const Correctness

The const qualifier promises not to modify a value. It should be applied consistently to improve safety and enable compiler optimisations:

c
const int MAX = 100;                 /* constant integer */
const char *str = "hello";          /* pointer to const char (string unmodifiable) */
char * const ptr = buffer;          /* const pointer to char (address unmodifiable) */
const char * const cptr = "world";  /* both pointer and pointee are const */

A useful mnemonic: read the type right-to-left: const char *p = "p is a pointer to char that is const".

typedef vs #define for Types

Prefer typedef over #define for type aliases because:

  • typedef participates in the type system; the compiler understands it.
  • typedef respects scope; #define does not.
  • Pointer typedefs work correctly: typedef char *String; const String s; makes s a const pointer, not a pointer to const char — a subtle but important difference.

Code Examples

Enum for a State Machinec
#include <stdio.h>

typedef enum {
    STATE_IDLE,       /* 0 */
    STATE_RUNNING,    /* 1 */
    STATE_PAUSED,     /* 2 */
    STATE_ERROR,      /* 3 */
    STATE_COUNT       /* sentinel — always keep last */
} AppState;

const char *state_name(AppState s) {
    static const char *names[] = {
        "IDLE", "RUNNING", "PAUSED", "ERROR"
    };
    if (s < 0 || s >= STATE_COUNT) return "UNKNOWN";
    return names[s];
}

/* State transition table: allowed[current] = allowed next state mask */
static int valid_transition(AppState from, AppState to) {
    switch (from) {
        case STATE_IDLE:    return to == STATE_RUNNING;
        case STATE_RUNNING: return to == STATE_PAUSED || to == STATE_ERROR || to == STATE_IDLE;
        case STATE_PAUSED:  return to == STATE_RUNNING || to == STATE_IDLE;
        case STATE_ERROR:   return to == STATE_IDLE;
        default:            return 0;
    }
}

int main(void) {
    AppState current = STATE_IDLE;
    AppState transitions[] = { STATE_RUNNING, STATE_PAUSED, STATE_IDLE, STATE_ERROR };

    printf("Starting state: %s\n", state_name(current));
    for (int i = 0; i < 4; i++) {
        AppState next = transitions[i];
        if (valid_transition(current, next)) {
            printf("  %s -> %s  [OK]\n", state_name(current), state_name(next));
            current = next;
        } else {
            printf("  %s -> %s  [INVALID]\n", state_name(current), state_name(next));
        }
    }

    /* Enum used in switch — compiler warns if a case is missing */
    printf("\nFinal state: %s\n", state_name(current));
    return 0;
}

STATE_COUNT as a sentinel gives the number of states automatically — it always equals the count because it follows all real states. The state name lookup array is indexed directly by the enum value (which starts at 0). A compiler with -Wswitch warns if a case is missing in the switch.

typedef Struct Pattern (C99)c
#include <stdio.h>
#include <string.h>
#include <math.h>

/* C99 idiom: define the struct and its typedef together */
typedef struct {
    double x;
    double y;
} Vec2;

/* Self-referential struct requires a tag */
typedef struct ListNode {
    int value;
    struct ListNode *next;   /* must use the tag, not the typedef alias */
} ListNode;

/* Functions taking/returning typedef'd struct by value */
static inline Vec2 vec2_add(Vec2 a, Vec2 b) {
    return (Vec2){ a.x + b.x, a.y + b.y };
}

static inline double vec2_length(Vec2 v) {
    return sqrt(v.x * v.x + v.y * v.y);
}

/* Compound literal syntax (C99) for temporary struct values */
static inline Vec2 vec2(double x, double y) {
    return (Vec2){ x, y };
}

int main(void) {
    Vec2 a = vec2(3.0, 4.0);
    Vec2 b = { .x = 1.0, .y = 2.0 };   /* designated initialiser */
    Vec2 c = vec2_add(a, b);

    printf("a = (%.1f, %.1f)  |a| = %.3f\n", a.x, a.y, vec2_length(a));
    printf("b = (%.1f, %.1f)\n", b.x, b.y);
    printf("a+b = (%.1f, %.1f)\n", c.x, c.y);

    /* Linked list using the self-referential typedef */
    ListNode n3 = {30, NULL};
    ListNode n2 = {20, &n3};
    ListNode n1 = {10, &n2};

    printf("\nList: ");
    for (ListNode *p = &n1; p != NULL; p = p->next)
        printf("%d ", p->value);
    printf("\n");

    return 0;
}

The typedef lets you write Vec2 instead of struct Vec2 everywhere. Compound literals (Vec2){ x, y } create temporary struct values inline (C99). For the self-referential ListNode, the struct tag is required inside the struct body because the typedef alias isn't yet fully defined at that point.

typedef for Function Pointer Callbacksc
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* typedef makes function pointer types readable */
typedef int  (*Comparator)(const void *, const void *);
typedef void (*Transformer)(int *, int);   /* modifies array in place */

/* Comparators for qsort */
int cmp_ascending(const void *a, const void *b) {
    return *(const int *)a - *(const int *)b;
}

int cmp_descending(const void *a, const void *b) {
    return *(const int *)b - *(const int *)a;
}

/* Transformers */
void double_all(int *arr, int n) {
    for (int i = 0; i < n; i++) arr[i] *= 2;
}

void negate_all(int *arr, int n) {
    for (int i = 0; i < n; i++) arr[i] = -arr[i];
}

/* Higher-order function: accepts a function pointer */
void sort_and_transform(int *arr, int n, Comparator cmp, Transformer tfm) {
    qsort(arr, (size_t)n, sizeof(int), cmp);
    if (tfm) tfm(arr, n);
}

void print_arr(const int *arr, int n) {
    for (int i = 0; i < n; i++) printf("%d ", arr[i]);
    printf("\n");
}

int main(void) {
    int data[] = {5, 2, 8, 1, 9, 3};
    int n = 6;
    int copy[6];

    memcpy(copy, data, sizeof(data));
    sort_and_transform(copy, n, cmp_ascending, NULL);
    printf("Sorted ascending:  "); print_arr(copy, n);

    memcpy(copy, data, sizeof(data));
    sort_and_transform(copy, n, cmp_descending, double_all);
    printf("Sorted desc+2x:    "); print_arr(copy, n);

    /* Array of function pointers */
    Transformer ops[] = { double_all, negate_all };
    const char *names[] = { "double", "negate" };
    memcpy(copy, data, sizeof(data));
    for (int i = 0; i < 2; i++) {
        ops[i](copy, n);
        printf("After %s:     ", names[i]); print_arr(copy, n);
    }

    return 0;
}

typedef Comparator and typedef Transformer make the function pointer types as readable as any other type. sort_and_transform takes both a comparator and an optional transformer — a practical higher-order function pattern. The array of function pointers ops[] shows how you can select behavior at runtime.

Quick Quiz

1. What integer value does the third enumerator have in `enum { A, B, C, D };`?

2. Why does a self-referential struct (like a linked list node) require a struct tag even when using typedef?

3. What does `const char *p` mean in C?

4. What is one advantage of using typedef for a function pointer compared to a #define?

Was this lesson helpful?

PreviousNext