Before You Start
Fifty years old and still underneath nearly every operating system, database engine, and language runtime you will ever use. These two lessons explain why, and get a compiler installed.
What is C?
Dennis Ritchie built C at Bell Labs to rewrite UNIX — most OSes still run on it
C was created at Bell Labs in the early 1970s so UNIX could be rewritten in something more portable than assembly. That single decision is why C's fingerprints — and often its actual compiled code — sit underneath Linux, Windows, every major database, and the runtime of nearly every higher-level language you have used.
C's origin at Bell Labs rewriting UNIX, its influence on nearly every later systems language, and where C23 sits today.
Setup and Compiler
GCC and Clang are the two dominant compilers — both free, both production-grade
There is no single official C toolchain the way there is with some newer languages: GCC and Clang are both free, both mature, and both genuinely used in production, with mostly-compatible command-line flags. Getting one installed and a first file compiled is the entire goal of this lesson.
Installing GCC/Clang, the compile-then-run two-step C requires, basic flags, and editor/IDE setup.
Language Fundamentals
The syntax and habits that show up in every C file you will ever write: types, control flow, functions, arrays and strings, recursion, and the manual error-checking C requires of you. Six lessons before pointers properly begin.
Variables and Types
sizeof(int) isn't guaranteed to be 4 — C only guarantees minimums
C's standard guarantees only *minimum* sizes for its integer types, not exact ones — <code>int</code> is at least 16 bits, in practice almost always 32, but the standard never promises that. This portability-over-precision tradeoff runs through the whole type system and is worth internalizing early.
Integer and floating-point types, sizeof, signed vs unsigned, type conversion rules, and literal suffixes.
Control Flow
No real boolean type until C99 — 0 is false, everything else is true
C had no dedicated boolean type until C99's <code>_Bool</code> (later renamed <code>bool</code> via a header). Before and often still after that, <code>0</code> means false and anything nonzero means true — a convention that shows up constantly in real C code and is worth being fully comfortable with.
if/else, for/while/do-while, switch, the zero-is-false convention, and _Bool/bool in modern C.
Functions
Every argument passes by value — to modify a caller's variable, pass its address
C has no pass-by-reference parameters: every argument is copied by value into the function, full stop. The only way for a function to modify a caller's variable is for the caller to explicitly pass that variable's address, which the function receives as a pointer — the idea the rest of the course builds on.
Function declarations vs definitions, parameters and return types, pass-by-value, and function prototypes.
Arrays and Strings
A C string is just a char array ending in a zero byte
There is no separate string type in C at all: a string is a <code>char</code> array that happens to end with a zero byte, and every standard string function relies on that convention being upheld. Forgetting the terminator, or writing past the array's bounds, is one of the most common real C bugs.
Array declarations and indexing, the null-terminated string convention, common string.h functions, and buffer-size bugs.
Recursion
No tail-call optimization guarantee — deep recursion genuinely risks overflow
Unlike some languages, the C standard makes no guarantee that a compiler will optimize tail-recursive calls into a loop. A recursive function that looks clean on paper can genuinely overflow the call stack in C where an equivalent function in another language would not, which is worth knowing before you rely on it.
Recursive function structure, base cases, the call stack's role, and when to prefer an explicit loop instead.
Error Handling
No exceptions — checking a return value is entirely on the programmer
C has no exception mechanism: every function that can fail signals it through a return value, an out-parameter, or the global <code>errno</code>, and checking that signal after every single call is entirely the programmer's responsibility. Skipping a check is how real C bugs are born.
Return-value error conventions, errno, perror, and why every fallible call needs an explicit check.
Pointers and Memory — C's Defining Discipline
This is the stage that makes C, C: manual memory management, the pointer arithmetic underneath every array, the tools that catch what the compiler won't, and the undefined behaviour that punishes getting it wrong. Seven lessons, the heart of the course.
Pointers and Memory
Declaration, dereferencing, pointer arithmetic, stack vs heap
A pointer is just a variable holding a memory address, but that simplicity is deceptive: dereferencing, pointer arithmetic, and the stack/heap distinction combine into the single idea that separates C from almost every language you'll learn afterward. This lesson is the foundation the rest of the stage builds on.
Pointer declaration and dereferencing, the & and * operators, pointer arithmetic, and the stack-vs-heap distinction.
Dynamic Memory Allocation
malloc gives you heap memory; forgetting free() leaks it forever
<code>malloc</code> and <code>free</code> hand memory-management responsibility entirely to you — there is no garbage collector watching for memory you forgot to release. A leak in a long-running C program accumulates silently until it genuinely exhausts available memory.
malloc/calloc/realloc/free, common allocation patterns, and the manual bookkeeping heap memory requires.
Const and Volatile
const *p and const p* mean genuinely different things
Where the <code>const</code> keyword sits in a pointer declaration changes what it actually protects — a pointer to constant data reads completely differently from a constant pointer to mutable data, and mixing them up is a classic beginner mistake. <code>volatile</code> solves an entirely separate problem.
const placement and what each position protects, top-level vs low-level const, and volatile's role with hardware/signals.
Function Pointers
A function's code lives at an address too — store it, call through it
A compiled function's code sits at a memory address just like any other data, and C lets you store that address in a variable and call through it later. This is the mechanism behind callbacks, dispatch tables, and every plugin-style API C code has ever offered.
Function pointer syntax, typedefs for readability, callback patterns, and dispatch tables.
Memory Safety Tools
Valgrind and AddressSanitizer catch bugs the compiler never notices
The C compiler will happily accept code that reads freed memory or writes past an array's end — it has no obligation to catch either. Valgrind and AddressSanitizer are separate, genuinely different tools that instrument a running program to catch exactly these bugs at runtime instead.
Running Valgrind's memcheck, compiling with -fsanitize=address, and reading each tool's output.
Debugging with GDB
A segfault tells you nothing on its own — GDB finds the line and the pointer
A raw segmentation fault gives you almost no information by itself: which line, which pointer, which call led there. GDB is how you actually answer those questions — breakpoints, stepping, and inspecting a crashed program's stack and variables directly.
Breakpoints, stepping, inspecting variables and the call stack, and examining a core dump after a crash.
Undefined Behaviour, Tied Together
Buffer overflows, use-after-free, signed overflow — and the tools that catch them
This lesson is the synthesis for the stage: the specific ways C code can invoke undefined behaviour — buffer overflows, use-after-free, signed integer overflow, unsequenced expressions — and how the pointer, memory-allocation, and tooling lessons above combine into one coherent defensive practice. Read it once each piece is familiar on its own.
Common UB categories, why the compiler is allowed to assume UB never happens, and ASan/Valgrind as the practical defense.
Data Structures
The building blocks C hands you instead of a standard library full of collections: enums, unions, self-referential structs, and the array/struct/function-pointer combination that underlies nearly everything above. Four lessons.
Enums
A C enum is just named integers — no type safety at all
Unlike some languages' enums, a C <code>enum</code> is nothing more than a set of named integer constants — any <code>int</code> value, including ones with no matching name, can be assigned to an enum variable without complaint from the compiler. Knowing this prevents a false sense of type safety.
Enum declarations, implicit integer values, explicit value assignment, and the type-safety gap versus other languages.
Unions
Every member shares the exact same memory — write one, read another
A <code>union</code>'s members all overlap the same block of memory, so writing through one member and reading through another is a real, sometimes genuinely useful technique — and also a common source of subtle bugs when the active member isn't tracked carefully.
Union declarations, memory overlap between members, tagged unions for tracking the active member, and common uses.
Linked Lists
A struct holding a pointer to another instance of itself
A linked list is the classic proof that structs and pointers combine into something bigger than either alone: a struct holding a pointer to another instance of its own type, chained together one allocation at a time. Building one by hand is the single most common way this idea gets internalized.
Node struct design, insertion and traversal, freeing every node without leaking, and singly vs doubly linked lists.
Structs and Arrays, Tied Together
Definition, typedef, nested structs, array decay, and function pointers together
This lesson is the synthesis for the stage: struct definition and member access, <code>typedef</code> for readability, nested structs, how arrays decay to pointers when passed to a function, <code>sizeof</code>'s behavior on each, strings as char arrays, and function pointers as struct members — all of it combined into the kind of data-modeling code real C projects actually contain.
Struct definitions and typedef, nested structs, array-to-pointer decay, sizeof on structs vs arrays, and function pointers as struct fields.
Systems and Tooling
What surrounds a C source file before it becomes a running program: the preprocessor, headers, multi-file builds, Makefiles, and the lower-level operators and library functions that round out the language. Seven lessons.
Header Files
#include literally copy-pastes a header's text — no real module system
There is no module system underneath C's <code>#include</code>: it is a literal, unconditional text-substitution that pastes the named file's contents into yours before real compilation even starts. Understanding this explains include guards, forward declarations, and a surprising number of build errors.
Header file structure, include guards vs #pragma once, declarations vs definitions, and standard vs local headers.
Multi-File Projects
Each .c file compiles independently; the linker combines them afterward
A real C project splits code across multiple <code>.c</code> files, each compiled independently into its own object file, with the linker combining those object files into one executable only at the very end. This two-stage model is why a forward declaration in a header is enough to compile against code defined elsewhere.
Translation units, object files, the linker's job, extern declarations, and organizing a multi-file project.
Makefiles
A tab instead of spaces is the single most common frustration make causes
Make automates rebuilding only what actually changed, based on file timestamps and the dependency rules you write — but its syntax is unforgiving about one specific thing: recipe lines must be indented with a literal tab character, and a stray space there produces a notoriously confusing error.
Targets, dependencies, and recipes, variables in a Makefile, phony targets, and the tab-indentation requirement.
The Preprocessor
A text-substitution pass that runs before real compilation even starts
The preprocessor handles <code>#include</code>, <code>#define</code>, and conditional compilation directives as a pure text-substitution pass, entirely before the compiler proper ever sees your code. A macro missing parentheses around its parameters is a classic, genuinely real bug this lesson explains how to avoid.
#define object-like and function-like macros, conditional compilation (#ifdef/#ifndef), and common macro pitfalls.
Bitwise Operations
Six operators behind flags, masks, and embedded register access
C's six bitwise operators — AND, OR, XOR, NOT, and the two shifts — manipulate individual bits directly, which is exactly what flag combinations, bitmasks, and direct hardware register access all require. This is a genuinely low-level tool most higher-level languages hide from you entirely.
&, |, ^, ~, <<, >>, bitmasking and flag combinations, and setting/clearing/toggling individual bits.
Variadic Functions
printf accepts any number of arguments — stdarg.h makes that possible
<code>printf</code>'s ability to accept any number of arguments of varying types is not a compiler special case: it is built on <code>stdarg.h</code>'s macros, which any C function can use to accept its own variable-length argument list the same way.
va_list, va_start/va_arg/va_end, writing your own variadic function, and why the caller must communicate the count.
The Standard Library
A deliberately small set of headers covering only what's truly universal
C's standard library is intentionally minimal compared to most modern languages — stdlib.h, ctype.h, math.h and a handful of others cover only what the standard considers genuinely universal, leaving everything else to third-party libraries or the operating system directly.
stdlib.h (allocation, conversion, sorting), ctype.h, math.h, string.h recap, and the library's deliberately narrow scope.
Files, Concurrency and Standards
Reading and writing real files, running work across real threads, and the fifty-year history of standards that got C here. Three lessons to close out the course.
File I/O
fopen, fread, fwrite, fclose — four functions older than most languages
C's file I/O functions — <code>fopen</code>, <code>fread</code>, <code>fwrite</code>, <code>fclose</code> — predate nearly every other language's own file APIs, and most of those later APIs are themselves thin wrappers around similar underlying system calls. Learning these four functions well pays off widely.
fopen modes, fread/fwrite for binary data, fprintf/fscanf for text, and always checking for a NULL FILE*.
Concurrency
C11 added a standard threading library — but pthreads is what people actually use
C11 technically standardized <code><threads.h></code>, but in practice almost every real C codebase reaches for POSIX threads (pthreads) instead, since it predates the standard, is more widely supported, and is what existing code and tutorials already use.
pthread_create/join, mutexes for shared-state protection, and why <threads.h> hasn't displaced pthreads in practice.
C Standards History
From an unofficial 1978 book to a formal 2024 ISO standard
C's standards history runs from K&R's original 1978 book — never itself a formal standard — through ANSI C89/C90, C99, C11, C17, and now C23, each a genuinely different, ratified version with its own feature set. Knowing which era a piece of code targets explains a lot about why it looks the way it does.
K&R C, ANSI C89/C90, C99's major additions, C11, C17, and C23's most recent changes.
Concepts That Cross Languages
These pages explain ideas that are not specific to C — they apply to every language you will ever learn. Read them once and the next language costs you far less effort. They pair well with the lessons above rather than replacing them.
Build Real Things — 10 Projects
Reading is not enough. These 10 projects — five basic, five medium — are chosen because each one exercises something specific you learned above, and because they are genuinely idiomatic C rather than generic exercises. Each link below opens the project brief; the C implementation walkthrough is in progress.
scanf input parsing, a manual guess loop, and rand()/srand() for the secret number.
A small switch statement for the conversion direction, and printf format specifiers.
Reading a file byte-by-byte with fopen/fgetc, and a hand-rolled hash table for word frequency.
A struct array for tasks, and manual line-based persistence with fread/fwrite.
A flag-based character-set selection, and a review of why rand() is unsuitable for real security.
A raw TCP socket and hand-written HTTP request, since C ships no HTTP client at all.
A minimal HTTP server over raw sockets, with hand-parsed request lines and manual response formatting.
A tagged union for subcommands, and a switch over the tag the compiler can't check for exhaustiveness.
A fixed-size open-addressing hash table for the code-to-URL mapping, with pthread mutexes for thread safety.
Line-by-line parsing with sscanf, and a simple linked-list-based counter for top-N aggregation.
Practice & Experimentation
Ongoing, not a final step. Use these throughout the course — try every snippet you read, and run anything you are unsure about rather than assuming.
Run C code without installing anything, via the official C Playground.
Copy-ready idiomatic patterns to keep beside you while you build.
All 29 topic pages in one index, for looking things up later.