Free through lesson 3

C Intro Course

From understanding how compilation works and reading gcc warnings through variables and control flow, functions and arrays, pointers, malloc and free, structs and linked lists, files and make, and system calls. Across 50 lessons, you'll manage pointers and memory yourself and write a working simple shell. Every line of code, warning, and sanitizer report has been run live with gcc 16.2.0 (C23).

Curriculum

All 50 lessons are divided into 7 chapters. It's best to work through chapter 1 first, but you can also skip around to topics that interest you. Note: You can't run C in a browser, so use the gcc 16 Docker container we provide in lesson 2, or gcc on Linux/WSL. Chapter 7's system calls require Linux.

Chapter 1 — Getting Started with C (lessons 1–7)

Start with the 4 stages of compilation and gcc warnings, then move through variables and types, operators, standard I/O, conditional branching, and loops. Build the habit here of running your code through gcc and letting warnings and sanitizers catch mistakes.

1

What Is C? (The 4 Stages of Compilation)

Sort out where C runs, compile your first program with gcc, and stop at each stage (-E, -S, -c) to see what happens — preprocessing, compilation, assembly, and linking.

Free
2

Making gcc Warnings Work for You (-Wall -Wextra and C23)

Set up a practice Docker container, find mistakes that gcc silently passes with -Wall -Wextra, and see what changed in gcc 14 (functions need declarations) and gcc 15 (C23 is default).

Free
3

Variables and Types (Measure Size and Range with sizeof)

A type promises how many bytes and how to read them. Measure with sizeof and limits.h, meet stdint.h's fixed-width types, see 0.1 + 0.2's rounding error, try C23's constexpr, and catch init mistakes.

Free
4

Operators and Type Conversion (Division, unsigned, Undefined Behavior)

Integer division truncates toward zero, and where you cast changes the result. See why -1 < 1u is false and verify with UBSan that signed overflow is undefined behavior.

🔒 Basic
5

Standard I/O (printf Format and scanf, fgets)

Align output with printf width, left alignment, zero padding, and precision. Read one line with fgets and always check scanf's return value. Verify "just numbers" with fgets and sscanf.

🔒 Basic
6

Conditional Branching (if, switch, and [[fallthrough]])

else if stops at the first match, top to bottom. Learn short-circuit && / ||, switch/break, and leap-year logic. Mark fall-through with C23's [[fallthrough]] and catch missing break via warnings.

🔒 Basic
7

Loops (for, while, do-while, and break, continue)

Use for when you know the count, while for a condition, do-while for at least one run. Write FizzBuzz and times tables, count 0 as one digit with do-while, and break and continue to exit or skip.

🔒 Basic

Chapter 2 — Functions and Arrays (lessons 8–14)

Split code into functions and understand scope and lifetime. Move through arrays, strings, and multidimensional arrays, and verify that sanitizers catch reading beyond array bounds.

8

Functions (Prototype, Pass by Value, [[nodiscard]])

Name and extract code into functions. Write main after the prototype, understand that parameters are copies, and see C23's [[nodiscard]] and how () means "no arguments" now.

🔒 Basic
9

Scope and Storage (The 2 Meanings of static)

Scope is where a name is visible, storage duration is how long a variable lives. Compare global variables, function-local static, and regular variables, and spot shadowing with -Wshadow.

🔒 Basic
10

Arrays (Count with sizeof, Catch Out-of-Bounds)

Arrays store the same type by index. Count with sizeof, use C23's {} to zero everything, and see how i <= 5 typos read past the end as 65550. Catch with AddressSanitizer and UBSan.

🔒 Basic
11

Passing Arrays to Functions (What's Passed Is the Address)

Arrays decay to the address of the first element, not a copy. See why sizeof inside a function returns 8, pass the count separately, and use const to promise you won't modify.

🔒 Basic
12

Strings Are char Arrays ('\0' and strlen)

C strings are char arrays with '\0' at the end. Learn the difference between sizeof and strlen, see how UTF-8 Japanese is 3 bytes per character, and watch strings without '\0' read beyond bounds.

🔒 Basic
13

String Functions (strcmp, snprintf, strchr)

Use string.h to compare, copy, concatenate, and find strings. Know why == compares addresses, strcpy's pitfall, and how to check strncpy's return to see if it was truncated.

🔒 Basic
14

Multidimensional Arrays (Row-Major and Variable-Length Array Arguments)

int m[3][4] is "3 arrays of 4 ints." Memory is arranged row by row (row-major). Pass matrices with variable-length array arguments taking rows and columns first, and check tic-tac-toe wins.

🔒 Basic

Chapter 3 — Pointers (lessons 15–23)

Work through addresses and pointers, pointer arithmetic, how arrays and strings relate, const, function pointers, and pointers to pointers. Finally, see how null and dangling pointers break things with sanitizers.

15

Addresses (Take with &, View with %p)

Memory is numbered boxes, one byte each. Take the address with & and see it with %p. See how array elements are separated by type size and ASLR randomizes addresses but keeps differences the same.

🔒 Basic
16

Pointers (int *p, *p, and swap)

Pointers are variables holding addresses. int *p = &x points to x, and *p reads and writes. Sort out the two meanings of *, and use pointers to pass locations so functions modify caller's variables.

🔒 Basic
17

Pointer Arithmetic (p + 1 and ptrdiff_t)

p + 1 moves one element ahead, not one byte. The distance is sized by type, pointer difference is element count (ptrdiff_t). Legal addresses are inside an array and one past the end.

🔒 Basic
18

Arrays and Pointers (a[i] Is *(a + i))

Array names decay to an address in expressions, so a[i] is *(a + i). sizeof and & don't decay — arrays aren't pointers. See why arrays can't be assigned, and why int a[] equals int *a in parameters.

🔒 Basic
19

Strings and Pointers (char[] vs. const char *)

Hold strings in arrays or point to them. String literals live in read-only memory (.rodata); writing to them crashes. Use -Wwrite-strings and const char *, and read *dst++ = *src++ correctly.

🔒 Basic
20

const and Pointers (const int * vs. int *const)

Pointers have two parts — the pointer and what it points to — and const's position changes meaning. Read right-to-left, see errors from broken promises; const params mean "don't modify contents."

🔒 Basic
21

Function Pointers (Select from a Table, Pass to qsort)

Store a function's address in a variable via function pointers. Read int (*op)(int, int), use typedef, build operation tables, understand callbacks, pass a comparator to qsort, and avoid a - b.

🔒 Basic
22

Pointers to Pointers (int ** and argv)

int ** holds a pointer's location. Follow * for each level, and int ** lets you change the caller's pointer. See null-terminated string arrays and the real nature of main's argv.

🔒 Basic
23

nullptr and Dangerous Pointers (null, Dangling, Uninitialized)

Null pointers point to nothing, dangling ones point to something gone, uninitialized ones point anywhere. Meet C23's nullptr, Segfaults, sanitizer reports, and gcc code that quietly returns 0.

🔒 Basic

Chapter 4 — Managing Memory Yourself (lessons 24–31)

Draw a map of stack and heap, borrow and return memory with malloc, calloc, realloc, and free. Catch leaks, use-after-free, double free, and buffer overflow with LeakSanitizer, valgrind, and AddressSanitizer.

24

Stack and Heap (Memory Map)

See where code, read-only, data, bss, heap, and stack live by address. Stack is fast but small (8 MB default); endless recursion exhausts it and Segfaults. Heap is borrowed — you must return it.

🔒 Basic
25

malloc and free (Check nullptr and ckd_mul)

Ask for a headcount, borrow space with malloc, and return it with free. Check malloc's nullptr return, and free even on early exit. Block input overflowing to size_t max with C23's ckd_mul.

🔒 Basic
26

calloc and realloc (Build a Growable Array)

Build a "growable array" when you don't know the count upfront. Keep len and cap, double with realloc when full. Avoid data = realloc(data, ...) and use calloc to borrow and zero.

🔒 Basic
27

Memory Leaks (LeakSanitizer and "Who Frees This?")

Losing a pointer without returning its memory causes a leak. Read LeakSanitizer's report, note stdout can vanish right after, and document who owns borrowed memory in function descriptions.

🔒 Basic
28

valgrind (Find Memory Mistakes Without Rebuilding)

Run your program on a "fake CPU" that watches memory access with valgrind. Read three kinds of reports: definitely lost, Invalid read, uninitialised value, and use --error-exitcode to add to tests.

🔒 Basic
29

Use-After-Free and Double Free (use-after-free, double free)

Using freed memory is use-after-free; freeing it twice is a double free. Both run silently with garbage output — catch with AddressSanitizer/glibc, and prevent by nulling pointers after free.

🔒 Basic
30

Buffer Overflow (Why strcpy Is Dangerous and snprintf)

Writing more data than a buffer holds is a buffer overflow. See why strcpy is dangerous, and how -fstack-protector, _FORTIFY_SOURCE, and AddressSanitizer catch it. Pass size to snprintf instead.

🔒 Basic
31

Dynamic Strings (getline and strdup)

Chapter 4 capstone: read lines of unknown length with getline without overflow, realloc char ** to match the count, and save lines with strdup. Free in two stages, or count upfront and borrow once.

🔒 Basic

Chapter 5 — Structs and Data Structures (lessons 32–38)

Group data with structs, see memory layout with padding, unions, and enums. Finally, build linked lists, stacks, and queues in C, combining pointers and memory management.

32

Structs (., ->, and qsort)

Group related values in a struct. Use designated initializers and compound literals, tell value from pointer passing (->), copy structs with =, note == won't compare them, and sort arrays with qsort.

🔒 Basic
33

Structs and Memory Layout (Padding and Alignment)

Struct size isn't just members added. char, int, char is 12 bytes; int, char, char is 8. Understand padding and alignment, fix size with static_assert, and spot padding with -Wpadded.

🔒 Basic
34

Enums and Unions (enum, union)

Named integers in enum, members sharing the same space in union. Catch case misses with -Wall, use union to see little-endian and float bits. Combine enum and union as tagged unions.

🔒 Basic
35

Linked Lists (Connect Nodes with next)

Connect malloc'd nodes via the next node's address in a linked list. Receive struct node ** since prepending changes head. When freeing all, save next before free to avoid use-after-free in a loop.

🔒 Basic
36

Linked List Operations (struct node ** Unifies Front and Middle)

Append, insert sorted, delete, reverse. "The next pointer's location" (struct node **) makes front and middle the same. *pp = victim->next deletes in one line; prev, cur, next reverse direction.

🔒 Basic
37

Stacks (Check Parenthesis Matching)

A stack (LIFO) pops what was last pushed, built with the growable array from lesson 26. Push opening parens, pop and match closing, and spot three kinds of errors: mismatched, missing open, unclosed.

🔒 Basic
38

Queues (Ring Buffer)

A queue (FIFO) pops what was pushed first, built with a circular array (ring buffer). Use % cap to wrap from the end back to the front, keep head and len to clearly tell full from empty.

🔒 Basic

Chapter 6 — Files and Separate Compilation (lessons 39–45)

Handle text and binary file I/O, separate compilation with headers, make, the preprocessor, and static and shared libraries. Finally, read command-line arguments with getopt and shape your program as a real tool.

39

File I/O (fopen, fgetc, and errno)

Open with fopen, get a FILE *, close with fclose. See why fgetc returns int, what EOF means, and compare wc to the real one. Report failures via errno, strerror, perror, and the "r"/"w"/"a" modes.

🔒 Basic
40

Binary Files (fwrite, fread, and fseek)

Save structs as they are in memory in binary. Write with fwrite, peek with od, verify little-endian byte order. Random access with fseek and ftell, and remember portability notes.

🔒 Basic
41

Separate Compilation and Headers (Read undefined reference)

Declare in headers, define in .c, compile each file and link to connect. Use include guards and extern and static, and read linking errors undefined reference and multiple definition to fix them.

🔒 Basic
42

make (Rebuild Only What Changed)

Write "target: ingredients" and indented recipes in Makefile, rebuild only what relates to changes. Use automatic variables and pattern rules, write header dependencies with -MMD, and use .PHONY.

🔒 Basic
43

The Preprocessor (#define, #if, #embed)

Lines starting with # are processed as text before compilation. See macro pitfalls with gcc -E, use # and ##, #ifdef and -DDEBUG, and C23's #embed to include files.

🔒 Basic
44

Libraries (Static .a and Shared .so)

Package your functions two ways. Static libraries (.a), made with ar, get copied into executables; shared ones (.so), made with -fPIC/-shared, load at runtime. Use LD_LIBRARY_PATH, nm, readelf.

🔒 Basic
45

Command-Line Arguments (getopt and strtol to Build head)

Read options and arguments like -n 3 file… with getopt, parse numbers safely with strtol not atoi. Build head-like commands, exit with 2 for usage errors and 1 for runtime failures.

🔒 Basic

Chapter 7 — Touching the System (lessons 46–50)

Peek at system calls open, read, write with strace, then operate on processes with fork, exec, pipe, and signals. Finish by bringing together everything and building a simple shell that uses pipes and redirects.

After you finish all 50 lessons, take what you've built with manual memory management and move to languages that protect memory for you in the Rust Intro Course, or explore what lies beyond system calls and shells in the Linux Intro Course and Linux System Admin Intro Course. Membership unlocks all courses.