Lessons
Pointer arithmetic and multi-level pointers
p+n advances by n elements, pointer subtraction, void* and casting, char** (argv pattern), and const pointers vs pointer to const.
What to look for:
- p + n advances by n * sizeof(*p) bytes — type-aware arithmetic
- void* is a generic pointer — must cast before dereferencing
- const int *p: pointer to const int (can't modify value through p)
- int *const p: const pointer to int (can't change where p points)
Undefined behaviour and sanitisers
What UB is, the six most dangerous forms (buffer overflow, UAF, signed overflow, uninit reads), -Wall -Wextra, ASan, UBSan, and valgrind.
Prerequisite: Practitioner Track
What to look for:
- UB is not "implementation-defined" — the compiler makes no guarantee at all
- -fsanitize=address,undefined: the most impactful flag for catching bugs during development
- Signed overflow is UB; unsigned overflow is defined (wraps modulo 2^n)
- ASan has ~2–5x slowdown — suitable for debug builds; not production
Linked lists and dynamic data structures
Singly linked list (node_new, prepend, append, search, free_list), doubly linked list basics, and memory ownership contracts.
Prerequisite: Lesson 1
What to look for:
- Each node is a heap allocation: malloc(sizeof(Node)) — free every node on cleanup
- Self-referential struct: use struct Node (not typedef) in the pointer field definition
- Ownership: whoever allocates is responsible for freeing — document this clearly
- free_list walks the list freeing each node — save next before freeing current
Function pointers and the vtable pattern
Function pointer declaration, typedef for fn pointer types, passing callbacks to qsort, dispatch tables, and the Linux kernel vtable pattern.
Prerequisite: Lesson 3
What to look for:
- typedef int (*Comparator)(const void*, const void*) — cleaner fn pointer type
- qsort(arr, n, sizeof(int), cmp) — the canonical callback example in C
- Dispatch table: struct Ops { void (*open)(...); void (*read)(...); } — like a vtable
- Function pointers enable runtime polymorphism without classes — the Linux kernel uses this pattern throughout
POSIX basics: processes, signals, and sockets
fork/exec, waitpid, kill/signal, socket/bind/listen/accept basics, and why POSIX is the bedrock of Unix-like systems.
Prerequisite: Lessons 1–4
What to look for:
- fork(): creates a copy of the process; returns 0 in child, child PID in parent
- exec() family: replaces the current process image with a new program
- Signals: SIGINT (Ctrl-C), SIGTERM (graceful stop), SIGKILL (immediate, can't be caught)
- Sockets: socket() creates fd; bind() assigns address; listen() marks it as server; accept() blocks for connection
After this track: You understand C at the systems level. Good next steps: read the Linux kernel source (particularly a simple driver), work through Beej's Guide to Network Programming (beej.us/guide/bgnet/), or study K&R cover-to-cover. Consider learning C++ for RAII-based memory safety or Rust for memory safety without runtime cost.
Track quiz
4 questions to check your understanding.
What does p + 2 mean when p is an int*?
What is undefined behaviour in C?
In a linked list free function, why must you save next before calling free(current)?
In POSIX, what value does fork() return in the child process?