How to use this guide
- Beginner (Part I): the toolchain, types, control flow, functions, arrays/pointers, strings, structs, the preprocessor. The three trickiest sections (pointers, strings, structs/macros) each end with a 🔎 Checkpoint — a handful of predict-the-output/spot-the-bug/write-it- yourself questions with collapsible answers, so you can confirm you're ready before moving on instead of finding out three sections later. Part I then ends with two terminal mini-projects.
- Intermediate (Part II): dynamic memory, multi-file programs, file I/O, debugging/sanitizers, a minimal test harness. Ends with a vector library + CSV parser, and a JSON parser.
- Advanced (Part III): data structures from scratch, intrusive
containers (the Linux-kernel trick), threads and atomics, sockets,
_Generic. Ends with a thread pool and a multi-client chat server. - Real-world builds (Part IV): production CLIs, a text editor, a Unix shell, a bump allocator.
- Security-focused C (Part V): undefined behavior, safe string handling, hardening flags, fuzzing, static analysis.
- Open-source walkthroughs (Part VI): SQLite, Redis, curl, cJSON/jq, Linux-kernel-style intrusive lists, uthash/klib — the same concepts you just learned, read out of real, battle-tested codebases.
- Expert (Part VII): the C11 memory model, writing a real allocator, ABI and API design, cross-compilation and build systems, profiling. Ends with a capstone: a mini Redis-like key-value store with a wire protocol.
- Writing safe, leak-free C (Part VIII): every memory-leak shape you'll
actually hit in production (not just "forgot to
free"), a defensive- programming checklist, and a bad-code/good-code catalog of the mistakes real C programmers make — read this even if you skip everything else. - Hardware & driver programming (Part IX): the senior-engineer layer —
talking to real hardware from user space (GPIO/I2C/SPI/serial),
memory-mapped registers and
volatile, writing and loading your first Linux kernel module, character device drivers, interrupts, and the concurrency rules that change once your code runs in kernel context. Ends with a capstone: one sensor, driven two ways. - Signals, non-local jumps, and mapped files (Part X): the three
general-purpose systems topics every "expert C" checklist expects that
the rest of this guide only mentions in passing —
signal/sigaction,setjmp/longjmp, andmmapfor real files (as opposed to Part VII's anonymous allocator mapping or Part IX's hardware-register mapping). - Appendices: C17→C23 feature diff, the UB cheat sheet, compiler flags, glossary, further reading.
Conventions:
- Code targets C17 (ISO/IEC 9899:2018) as the safe default and calls
out C23 (ISO/IEC 9899:2024) features explicitly where used (
nullptr,typeof,#embed,constexpr,[[attributes]],_BitInt, binary literals). Toolchain used throughout: recent Clang/Apple Clang and GCC releases. Newer compilers accept-std=c23; some older releases use-std=c2xfor the same in-progress standard mode. Where a snippet needs something GCC/Clang-specific beyond the standard (a builtin, an attribute), it's labeled. - "Real-world example" boxes ground a concept in something you'll actually write. "War story" boxes describe a real class of incident — the kind that shows up in CVE databases and postmortems — caused by getting that concept wrong.
- Every code block is meant to compile. Where a snippet is wrong on
purpose (to demonstrate a bug or UB), it's labeled
// BUGor// UB — do not do this.