# C From Zero

> Learn C from nothing to genuinely understanding it: install a compiler and write real programs, then the deep half - pointers, manual memory, the stack vs the heap, and undefined behavior - the things that separate writing C from understanding it.


---

# C From Zero

Almost every piece of software you've ever used has C somewhere underneath it. Your operating
system's kernel is written in C. So is the Python interpreter that runs your Python scripts, the
database engine behind your app, and a good chunk of the tools you run every day without thinking
about them. C isn't old news - it's the floor that everything else stands on.

C also has a reputation, and it's earned: it doesn't stop you from doing dangerous things. There's
no garbage collector cleaning up after you, no runtime checking your array accesses, no compiler
error when you read memory you shouldn't. In most modern languages, safety is the default and you
opt into danger. In C, it's the other way around - you are trusted with the sharp tools from line
one. That's not a design flaw, it's the whole point: C gives you direct, no-hand-waving control over memory
and hardware, and that control is exactly why an operating system can be written in it.

This guide takes you the whole way: from "I've never compiled a program" to actually understanding
what your code is doing to the computer's memory - not just enough syntax to make something run, but
the mental models that let you predict *why* it runs, or why it doesn't. We go mental-model-first the
whole way: before any command or keyword, you'll understand what the thing actually *is* and why C
works that way.

It's one zero-to-hero journey in two halves. **Phases 1-9 are the readable basics** - enough to
compile real programs, use pointers correctly, and structure a multi-file project. **Phases 10-14 are
the deep half** - dynamic memory, the stack vs the heap, pointer arithmetic and function pointers, the
standard library, and undefined behavior, the stuff that separates "writes C" from "understands C."
Phase 15 closes it out with where to go from here. Each phase carries a difficulty badge so you can
see the climb.

If you've never programmed at all, start with a gentler on-ramp first -
[Programming From Zero](/guides/programming-from-zero) - then come back here. C is unforgiving of
half-understood concepts, so it makes a rough *first* language. As a *second* language, once you
already know what a variable and a loop are, it's one of the most clarifying things you can learn -
it shows you what your other languages have been doing for you all along.

## How to read this

- **Brand new to C? Read 1-9 in order.** Each phase builds on the last. Phases 1-4 get you compiling
  and writing normal-looking programs. Phase 5 - pointers - is where C stops looking like "a language
  with weird syntax" and starts being C. Don't rush it; read it twice if you need to.
- **Already know another language?** Skim phases 1-4 to catch where C is *deliberately bare* (manual
  compilation, no built-in strings, no bounds checking), then **really slow down at phase 5.** Pointers
  are the idea everything after them depends on.
- **Past the basics already?** Jump to the deep half - [Phase 10: Dynamic Memory: malloc &
  free](10-dynamic-memory-malloc-and-free.md) onward is where pointers grow up into real memory
  management, and where you learn to see the footguns before they fire.

## The phases

**Part 1 - The basics (🟢 Basic → 🟡 Intermediate)**
1. **[Install, Compiling & Your First Program](01-install-compiling-and-your-first-program.md)** 🟢 - a compiler, `gcc`/`clang`, and what compiling actually does.
2. **[Syntax, Variables & Types](02-syntax-variables-and-types.md)** 🟢 - C's small, fixed set of types, and why they have exact sizes.
3. **[Control Flow](03-control-flow.md)** 🟢 - `if`, loops, `switch`, and how they compile to something close to the metal.
4. **[Functions & Program Structure](04-functions-and-program-structure.md)** 🟢 - declarations vs definitions, `main`, and multi-file programs.
5. **[Pointers I: The Mental Model](05-pointers-i-the-mental-model.md)** 🟡 - **the whole point of C:** what an address is, `*` and `&`, and why pointers aren't magic.
6. **[Arrays & Strings](06-arrays-and-strings.md)** 🟢 - arrays as contiguous memory, and C strings as "just bytes with a `\0` at the end."
7. **[Structs & Typedef](07-structs-and-typedef.md)** 🟢 - grouping data, `typedef`, and how structs actually sit in memory.
8. **[Header Files & the Preprocessor](08-header-files-and-the-preprocessor.md)** 🟢 - `#include`, `#define`, include guards, and what the preprocessor really does before compiling starts.
9. **[Build Tooling: Makefiles & Debugging](09-build-tooling-makefiles-and-debugging.md)** 🟡 - `make`, compiler flags, and finding bugs with `gdb` instead of guessing.

**Part 2 - Beyond the basics (🟡 Intermediate → 🔴 Advanced)**
10. **[Dynamic Memory: malloc & free](10-dynamic-memory-malloc-and-free.md)** 🔴 - asking for memory at runtime, and the discipline of giving it back.
11. **[The Stack vs the Heap](11-the-stack-vs-the-heap.md)** 🔴 - two totally different regions of memory, and why mixing them up crashes programs.
12. **[Pointers II: Arithmetic, Double & Function Pointers](12-pointers-ii-arithmetic-double-and-function-point.md)** 🔴 - pointer math, pointers to pointers, and pointers to code.
13. **[The Standard Library Essentials](13-the-standard-library-essentials.md)** 🟡 - `stdio.h`, `string.h`, `stdlib.h`, and the functions you'll reach for constantly.
14. **[Undefined Behavior & Common Footguns](14-undefined-behavior-and-common-footguns.md)** 🔴 - what "undefined" really means, and the mistakes that cause it.

**Finale**
15. **[Where to Go Next](15-where-to-go-next.md)** 🟢 - systems programming, embedded, contributing to C projects, and what to actually build.

> C's ecosystem (build systems beyond `make`, embedded toolchains, kernel development) is its own
> world - this guide makes the *language* make sense, top to bottom.


---

# Install, Compiling & Your First Program

C is the language most other languages are built on top of, or at least built to explain themselves
against. Python's interpreter is written in C. Your operating system's kernel is written in C. When people
say a language is "fast" or "low-level," C is usually the yardstick. Learning it doesn't just teach you a
language - it teaches you what's actually happening underneath the languages you already know.

That reputation comes with a warning label: C doesn't stop you from shooting yourself in the foot. It won't
check array bounds for you, it won't garbage-collect your memory, and it will happily compile code that
crashes or does something quietly wrong. That's not this phase's problem to solve (we'll get there, all the
way through to [Phase 14: Undefined Behavior & Common Footguns](14-undefined-behavior-and-common-footguns.md)) -
but it's worth naming up front, because it explains why C feels different from languages you may have
tried before. You are closer to the machine here, on purpose.

This phase gets you set up: a working compiler, a real understanding of what "compiling" means, and one
program you wrote, built, and ran yourself.

## The mental model: what a compiler actually does

**What it actually is.** C is a *compiled* language. That means the code you write in a `.c` file isn't
what runs - it gets translated, ahead of time, into a separate file full of raw machine instructions that
your CPU can execute directly. The program that does this translation is called a **compiler**. Two
compilers are used everywhere: **GCC** (the GNU Compiler Collection) and **Clang** (built on LLVM). They're
different programs with the same job, and for everything in this guide either one works the same way.

This is a genuinely different model from languages you may already know. If you've used Python or
JavaScript, you're used to an *interpreter* reading your source code line by line while the program runs.
C has no such thing at runtime. By the time your program starts, the translation is already done and
finished - there's no compiler anywhere in sight, just machine code running directly on the CPU.

**Why this matters.** It's the reason C programs start instantly and run at full hardware speed: there's no
translation step happening while the program executes, because it already happened. It's also why a
compile *error* stops you cold before the program ever runs at all - the compiler is reading the whole
program up front and refusing to produce an executable if it doesn't make sense.

📝 **Terminology.** **Source code** is the `.c` file you write. **Compiling** turns source code into
**machine code** - instructions in the CPU's native language. The result is an **executable** (or
**binary**) - a file you can run directly. **GCC** and **Clang** are compilers; you'll see both names in
the wild and either is fine to learn on.

## Install a compiler

Which command to run depends on your OS.

**macOS.** Apple ships Clang through its developer tools. Open a terminal and run:

```console
$ xcode-select --install
```

A dialog will prompt you to install the "Command Line Tools." Accept it - you don't need full Xcode, just
this smaller package, and it includes `clang`, `make`, and other tools you'll use throughout this guide.

**Linux.** Install GCC through your distribution's package manager:

```console
$ sudo apt install build-essential        # Debian/Ubuntu
$ sudo dnf install gcc                    # Fedora
```

`build-essential` on Debian-based systems pulls in `gcc`, `make`, and standard headers together - the
whole toolchain in one install.

**Windows.** The simplest path is **MSYS2**, which gives you a real GCC toolchain that behaves like the
Linux/macOS one (matching the terminal examples throughout this guide). Download it from
[msys2.org](https://www.msys2.org), run the installer, then from the MSYS2 terminal it opens:

```console
$ pacman -S mingw-w64-ucrt-x86_64-gcc
```

Close that terminal and use the "MSYS2 UCRT64" terminal from your Start menu from now on - it's the one
with GCC on its `PATH`. (The alternative path, Visual Studio's `cl.exe` compiler, works too, but its
command-line flags differ from what this guide uses.)

## Confirm it worked

One command tells you the compiler is installed and reachable:

```console
$ gcc --version
gcc (Ubuntu 13.2.0-4ubuntu3) 13.2.0
```

*What just happened:* `gcc` printed its version, meaning it's installed and on your `PATH` - the list of
places your terminal looks for commands. If you installed Clang instead, run `clang --version`. Either
output is fine; what matters is you get a version number back instead of a "command not found" error.

⚠️ **Gotcha.** If you just installed and get `command not found`, close and reopen your terminal. Installers
add the compiler to your `PATH`, but a terminal window that was already open doesn't know that until it
restarts.

## Write your first program

Create a new file called `hello.c` in any folder you like, with exactly this content:

```c
#include <stdio.h>

int main(void) {
    printf("Hello, world!\n");
    return 0;
}
```

Every line here is doing real work, so let's take them one at a time before you run anything:

- **`#include <stdio.h>`** pulls in declarations for the standard input/output functions - `printf` among
  them. C's standard library isn't built into the language itself; you ask for the pieces you need. This
  gets a full phase of its own in [Phase 8: Header Files & the Preprocessor](08-header-files-and-the-preprocessor.md).
- **`int main(void)`** defines `main`, the one function every C program must have - it's where execution
  begins. `int` is the type of value `main` hands back to the operating system when it finishes. `void`
  here means "takes no arguments."
- **`printf("Hello, world!\n")`** prints the string, and `\n` is an escape sequence for a newline character
  - it can't be typed directly inside the quotes, so `\n` stands in for it.
- **`return 0;`** hands `0` back to the operating system, the conventional way for a program to say "I
  finished successfully." Any other number signals some kind of failure - your shell can check this after
  the program exits.

## Compile it

```console
$ gcc hello.c -o hello
```

*What just happened:* Nothing printed - and that's a good sign. `gcc` read `hello.c`, translated it to
machine code, and wrote the result to a file named `hello` (`-o hello` means "output to this filename"; if
you omit `-o`, GCC defaults to a less friendly `a.out`). Silence means the compiler had nothing to
complain about.

Look at what appeared in your folder:

```console
$ ls
hello  hello.c
```

`hello` is a new file - your executable. It's not text anymore; it's machine code, meaningless if you open
it in an editor, but exactly what your CPU needs to run the program directly.

## Run it

```console
$ ./hello
Hello, world!
```

*What just happened:* `./hello` tells your shell "run the executable named `hello` sitting right here in
this folder" (the `./` is necessary on Linux/macOS - unlike some systems, they don't automatically look in
the current folder for commands, as a deliberate safety measure). Your CPU executed the machine code
directly - no compiler involved at this step at all, because compiling already happened.

## See it catch a mistake

Compiling isn't just a formality - it's the compiler reading your entire program and refusing to produce
an executable if something's structurally wrong. Try deleting the semicolon after the `printf` line and
compiling again:

```c
#include <stdio.h>

int main(void) {
    printf("Hello, world!\n")
    return 0;
}
```

```console
$ gcc hello.c -o hello
hello.c: In function 'main':
hello.c:5:5: error: expected ';' before 'return'
    5 |     return 0;
      |     ^~~~~~
      |     ;
```

*What just happened:* No executable was written this time - the compiler caught the missing semicolon and
told you exactly where it expected one, pointing at the line right after the mistake (C error messages
often point slightly *after* the actual problem, since the compiler doesn't realize something's missing
until it hits the next token). Clang words this a little differently and points at the end of the broken
line itself (`error: expected ';' after expression`, with the caret right after the `)`) rather than the
next line, but either way the reported spot is a signpost to look near, not always the exact character.
This is worth internalizing early: a compile error means the compiler refused to guess what you meant. Put
the semicolon back before moving on.

## Two commands you'll use constantly

- **`gcc file.c -o name`** - compile `file.c` into an executable called `name`. You'll type a version of
  this every time you change your code.
- **`./name`** - run the executable you just built. On Windows with MSYS2, the same commands work
  unchanged in the UCRT64 terminal.

There's no equivalent to `cargo run` or `python file.py` here that compiles and runs in one step - in C,
compiling and running are always two separate, explicit commands. That separation is the whole model: build
the machine code once, then execute it, as many times as you like, with no compiler involved on the second
step. Once your programs grow past a single file, typing the full `gcc` command by hand gets old fast -
[Phase 9: Build Tooling](09-build-tooling-makefiles-and-debugging.md) shows you how to automate it.

## Recap

1. **C is compiled, not interpreted** - your `.c` source gets translated ahead of time into machine code by
   a compiler (GCC or Clang), producing an executable that runs directly on the CPU.
2. **Install** the compiler for your OS: Xcode Command Line Tools (macOS), `build-essential` (Linux), or
   MSYS2 (Windows).
3. **`gcc file.c -o name`** compiles; **`./name`** runs the result. Two separate steps, always.
4. **`#include`** pulls in library declarations; **`main`** is where every C program starts; **`return 0`**
   signals success to the operating system.
5. A compile error means the compiler found something structurally wrong and refused to guess - read it as
   a pointer to exactly where to look, even if it points slightly after the real mistake.

You have a working compiler and a program you wrote, built, and ran by hand. Next: what those pieces inside
`main` actually are - variables, C's types, and how they differ from what you may already know.

## Quick check

Test yourself on the idea that separates C from languages you may already know - that compiling and running
are two distinct steps, not one:

```quiz
[
  {
    "q": "You're used to running Python with `python file.py`. Why doesn't C have an equivalent single command that runs a `.c` file directly?",
    "choices": [
      "C source is translated into machine code by the compiler ahead of time, so by the time the program runs there's no interpreter left to hand source code to",
      "C compilers are too slow to run a program immediately after reading it",
      "C programs don't have a `main` function to start from",
      "gcc requires an internet connection to execute a program"
    ],
    "answer": 0,
    "explain": "An interpreter reads and runs source line by line as the program executes; C's translation already happened before the program starts, so compiling and running are always two separate, explicit commands."
  },
  {
    "q": "You run `gcc hello.c -o hello` and nothing prints to the terminal. What does that silence mean?",
    "choices": [
      "The compile failed silently and no executable was written",
      "The compiler succeeded - it only prints when it has something to complain about",
      "The compiler is still working and you need to wait longer",
      "You forgot to add a flag that makes gcc report success"
    ],
    "answer": 1,
    "explain": "gcc has nothing to say when a compile goes fine; silence is the normal, successful outcome, and the new executable file is the proof it worked."
  },
  {
    "q": "You delete a semicolon and the compiler points its error at the line *after* the one you broke. What does that tell you about reading C compile errors?",
    "choices": [
      "The compiler is unreliable and often blames the wrong line",
      "The compiler doesn't realize a required token is missing until it reaches the next one, so the reported line can land just past the real mistake",
      "Only the last line of a file can ever contain an error",
      "The error means the file wasn't saved before compiling"
    ],
    "answer": 1,
    "explain": "The compiler reads forward and only notices something's missing once it hits the next token, so treat the reported line as a pointer to look near, not always the exact spot."
  }
]
```


---

# Syntax, Variables & Types

Last phase you got a C program to compile and run. Now open it up and figure out what every piece of it actually means. This phase is about the smallest building blocks: how you write a line of C, what a variable *is* to the machine, and why C makes you say up front exactly what kind of data you're storing.

## The mental model: a variable is a labeled box of a fixed size

In a lot of languages you're used to thinking of a variable as a name that can hold anything - a number today, a string tomorrow. C does not work that way, and understanding why unlocks almost everything else in this language.

**What a variable actually is.** When you declare a variable in C, you're telling the compiler to reserve a specific, fixed number of bytes in memory, and you're giving that spot a name and a rule for how to interpret the bits inside it. An `int` is (usually) 4 bytes, always interpreted as a whole number. A `char` is 1 byte, always interpreted as a small integer that usually represents a character. The *type* isn't a suggestion or a label you can peel off later - it's a promise to the compiler about the size and shape of the box, made before the program ever runs.

**Why this exists.** C was designed to map almost directly onto how a computer actually works: memory as a flat sequence of bytes, and a CPU that needs to know exactly how many bytes to read and how to interpret them. A dynamically-typed language like Python or JavaScript spends real work at runtime tracking what kind of value is in a variable right now. C skips all of that by deciding it once, at compile time, and baking it into the generated machine code. That's a big part of why C programs are fast: there is no "what type is this?" check happening while your program runs. The type was already decided when the program was built.

This is also why C is called **statically typed**: every variable's type is fixed the moment you declare it, and it never changes for the life of that variable.

## Declaring a variable

A declaration says three things at once: the type, the name, and (optionally) a starting value.

```c
int age = 30;
```

Read that left to right: "reserve a box shaped like an `int`, call it `age`, put `30` in it." You can declare without a value too, but the box's contents are then *garbage* - whatever bits happened to already be sitting in that memory. C will not clear it for you.

```c
int score;          // uninitialized - contains leftover garbage bits, not 0
score = 100;         // now it's meaningful
```

⚠️ **This is a real footgun, not a theoretical one.** Reading an uninitialized variable before assigning it is **undefined behavior** - the standard doesn't say what happens, so the compiler is free to do anything. It will usually just warn you rather than stop you, and the value you read is unpredictable: it might be `0` on one run and `47331` on the next, because it depends on whatever was in that memory before. (You'll meet "undefined behavior" properly in phase 14; for now, just treat it as a line you don't cross.) Get in the habit of initializing every variable when you declare it.

## The basic types

C gives you a small set of built-in types, each a different-sized box:

| Type | Typical size | Holds | Example |
|------|-------------|-------|---------|
| `int` | 4 bytes | Whole numbers | `int count = 42;` |
| `char` | 1 byte | A single character (really a small integer) | `char grade = 'A';` |
| `float` | 4 bytes | Decimal numbers, less precision | `float pi = 3.14f;` |
| `double` | 8 bytes | Decimal numbers, more precision | `double pi = 3.14159265;` |

📝 **Terminology.** "Typical size" because the C standard only guarantees *minimums*, not exact sizes - on almost every machine you'll touch today (including your laptop), these sizes hold, but C's portability across decades of wildly different hardware means the standard leaves a little room. If you ever need to know for certain, `sizeof(int)` tells you, on your machine, right now.

A `char` deserves a second look, because it surprises people. `'A'` looks like a letter, but C stores it as the integer 65 - its position in the ASCII table. `char` is really just a 1-byte integer that print functions *choose* to display as a letter. Proof:

```c
#include <stdio.h>

int main(void) {
    char grade = 'A';
    printf("As a character: %c\n", grade);
    printf("As a number: %d\n", grade);
    return 0;
}
```
```console
As a character: A
As a number: 65
```

Same byte, two different printf format specifiers, two different ways of looking at the same bits. That's the whole idea of a type in one example: the bits don't change, only how you've told the program to *read* them.

## Format specifiers: telling printf and scanf how to read the box

`printf` doesn't know what type its arguments are - C strips that information away by the time the function is called. So you tell it, with a **format specifier**, right there in the string:

| Specifier | For type |
|-----------|----------|
| `%d` | `int` |
| `%c` | `char` |
| `%f` | `float` or `double` |
| `%s` | string (a `char` array - more on that in phase 6) |

```c
#include <stdio.h>

int main(void) {
    int age = 30;
    float height = 5.9f;
    char initial = 'J';

    printf("Age: %d, Height: %.1f, Initial: %c\n", age, height, initial);
    return 0;
}
```
```console
Age: 30, Height: 5.9, Initial: J
```

`%.1f` means "print a float, one digit after the decimal point" - the number before the `f` controls precision.

Get a specifier wrong and C will not stop you. Write `%d` for a `float` and the compiler may warn you, but it will still run, reading the wrong number of bytes and interpreting them as the wrong type, printing nonsense. This is your first real taste of **undefined behavior** - a phrase you'll meet properly in phase 14 - where C trusts you to be right instead of checking. For now, the rule is simple: match the specifier to the type, every time.

Reading input works the same way, with `scanf`, except you also need `&` before the variable name:

```c
int age;
printf("Enter your age: ");
scanf("%d", &age);
```

That `&` means "the address of `age`" - `scanf` needs to know *where* in memory to write the number it reads, not just what's currently there. Addresses and `&` are the whole subject of phase 5 (Pointers I), so don't worry about fully absorbing it yet - just recognize the pattern: `printf` reads values, `scanf` writes into addresses.

## Constants: a box you promise not to change

Sometimes you want a named value that can never be reassigned - a safety rail, not just a convention. C gives you `const`:

```c
const float TAX_RATE = 0.08f;
```

Try to assign to `TAX_RATE` again anywhere later in the code and the compiler stops you cold, with an error at compile time - not a bug you discover at 2am. Use `const` for any value that's meant to stay fixed; it costs nothing and it turns a whole category of "wait, who changed this?" bugs into a compiler error instead.

## Naming rules and style

C's rules for names are strict but small: letters, digits, and underscores, must not start with a digit, and case matters (`age` and `Age` are different variables). Beyond that, it's convention rather than syntax - most C code uses `snake_case` (`total_score`, not `totalScore`), and that's what you'll see in this guide and in most C codebases you'll read.

## Recap

1. A variable is a fixed-size, fixed-interpretation box in memory - the type is decided once, at compile time, and never changes.
2. The core types - `int`, `char`, `float`, `double` - are different box sizes for different kinds of data; `char` is secretly just a 1-byte integer.
3. Uninitialized variables hold garbage, not zero - always give a starting value.
4. `printf`/`scanf` need a format specifier (`%d`, `%c`, `%f`, `%s`) that matches the variable's type, because the function itself has no idea what type you passed it.
5. `scanf` needs `&variable` (an address) so it knows where to write - your first hint of pointers, coming properly in phase 5.
6. `const` locks a value at compile time, turning accidental reassignment into an error instead of a bug.

### Check yourself

```quiz
[
  {
    "q": "You write `int age;` with no value. What does `age` actually contain until you assign it?",
    "choices": [
      "0, C always zero-initializes local variables",
      "Whatever leftover bits were already sitting in that memory - unpredictable garbage",
      "A compile error - C refuses to declare a variable without a value",
      "The same value every time you run the program, guaranteed by the compiler"
    ],
    "answer": 1,
    "explain": "C never clears memory for you. An uninitialized local variable holds whatever bits were already there, which can differ from run to run."
  },
  {
    "q": "`char grade = 'A';` prints as `A` with `%c` and as `65` with `%d`. What's really going on?",
    "choices": [
      "char secretly stores two values, one for each format specifier",
      "printf converts the character to a number behind the scenes when it sees %d",
      "The byte in memory never changes - char is just a 1-byte integer, and the format specifier only changes how that same byte is displayed",
      "This only works because 'A' is a special case in ASCII"
    ],
    "answer": 2,
    "explain": "A char is a 1-byte integer holding 65. Nothing about the stored bits changes between the two printf calls - only the specifier tells printf how to read and display them."
  },
  {
    "q": "In C, once you declare `int age = 30;`, what does the type `int` actually fix?",
    "choices": [
      "Nothing permanent - like Python, C can let age hold a float or string later if you reassign it",
      "Only how printf formats the value when you print it",
      "A compile-time promise about the exact size and byte-interpretation of that memory, which never changes for age's lifetime",
      "The maximum number of times age can be reassigned"
    ],
    "answer": 2,
    "explain": "C decides a variable's size and interpretation once, at compile time, and bakes that into the generated machine code - that's what 'statically typed' means here."
  }
]
```


---

# Control Flow

Right now, a C program you write runs exactly one way: top to bottom, one line after another, no
decisions, no repetition. That's true but useless - almost nothing you actually want to build works
that way. You need to skip code when a condition is false, repeat code while something holds, and choose
between several paths. That's control flow, and it's the last piece you need before your programs can
actually *do* something interesting.

## The mental model: the program counter takes detours

**What it actually is.** Underneath, the CPU has a program counter - a register holding the address of
the next instruction to run. By default it just increments: run this line, move to the next, run that
one, move to the next. Every control flow construct in C - `if`, `while`, `for`, `switch` - compiles down
to one thing: an instruction that changes the program counter based on a condition, instead of letting it
fall through to the next line. `if (x > 0) { ... }` is, underneath, "check `x > 0`; if false, jump past
this block." A `while` loop is "check the condition; if false, jump past the loop; otherwise run the body,
then jump *back* to the check."

Once you see it that way, none of these keywords are separate magic - they're just different shapes of
"conditionally jump."

## Truthiness in C: there's no real boolean

**What it actually is.** Before you write a single `if`, you need to know what C considers "true." In
plain C, there is no readable boolean type until you `#include <stdbool.h>`. Every condition is just an integer expression: **`0`
means false, and any nonzero value means true.** `if (5)` is true. `if (-1)` is true. `if (0)` is the only
way to get false.

**Why people get this wrong.** Coming from a language with a real `bool`, it's tempting to assume C has
one too, and to write `if (x = 1)` meaning `if (x == 1)`, which we'll get to below - it compiles, it just
doesn't mean what you think.

📝 **Terminology.** C99 (a 1999 revision of the language) added `#include <stdbool.h>`, which gives you
`bool`, `true`, and `false` as readable names. Under the hood `bool` is still just a small integer and
`true`/`false` are still `1`/`0` - the header is a readability convenience, not a new type of value. Most
modern C code includes it; we will too from here on.

```c
#include <stdio.h>
#include <stdbool.h>

int main(void) {
    bool is_ready = true;
    if (is_ready) {
        printf("go\n");
    }
    return 0;
}
```
```console
$ gcc -Wall -o prog prog.c && ./prog
go
```
*What just happened:* `is_ready` holds `1` (that's all `true` is). `if (is_ready)` checks "is this
nonzero?" - it is, so the block runs.

## `if` / `else if` / `else`

**What it does in real life.** C checks conditions top to bottom and runs the *first* branch whose
condition is true, skipping the rest entirely.

```c
int score = 72;

if (score >= 90) {
    printf("A\n");
} else if (score >= 80) {
    printf("B\n");
} else if (score >= 70) {
    printf("C\n");
} else {
    printf("F\n");
}
```
```console
$ gcc -Wall -o prog prog.c && ./prog
C
```
*What just happened:* C checked `score >= 90` (false), then `score >= 80` (false), then `score >= 70`
(true) - printed `C` and skipped `else`. It never even looks at branches after the first true one.

```mermaid
flowchart TD
  A{score >= 90?} -- yes --> B[print A]
  A -- no --> C{score >= 80?}
  C -- yes --> D[print B]
  C -- no --> E{score >= 70?}
  E -- yes --> F[print C]
  E -- no --> G[print F]
```

⚠️ **The gotcha that bites everyone once: `=` vs `==`.** `=` assigns; `==` compares. Both are valid inside
an `if`'s parentheses, so the compiler won't stop you from typing the wrong one.

```c
int x = 0;
if (x = 5) {          // assigns 5 to x, then checks "is 5 truthy?" - yes, always
    printf("this always runs\n");
}
```
```console
$ gcc -Wall -o prog prog.c && ./prog
prog.c:2:9: warning: suggest parentheses around assignment used as truth value [-Wparentheses]
    2 |     if (x = 5) {
      |         ~~^~~
this always runs
```
*What just happened:* `x = 5` is itself an expression that evaluates to `5` (the value just assigned),
and `5` is nonzero, so the branch runs *every time*, and `x` has silently been overwritten. `-Wall` (which
you should always compile with) catches this and warns you - don't ignore that warning.

⚠️ **The dangling-else gotcha.** Without braces, `else` always binds to the *nearest* unmatched `if`, which
is not always the one that lines up visually:

```c
if (a > 0)
    if (b > 0)
        printf("both positive\n");
else
    printf("a is not positive\n");   // misleading indentation!
```

Despite the indentation, that `else` belongs to `if (b > 0)`, not `if (a > 0)` - so if `a` is `-1`, this
prints *nothing at all*. **Why this saves you later:** always write braces on multi-statement or nested
`if`s, even when C doesn't require them. It costs you two characters and removes an entire category of bug.

## Loops: `while`, `do-while`, `for`

**What it actually is.** All three loops are the same idea - repeat a block while a condition holds - with
different places for the setup and the check.

**`while`** checks the condition *before* every iteration, including the first:

```c
int n = 3;
while (n > 0) {
    printf("%d\n", n);
    n--;
}
```
```console
$ gcc -Wall -o prog prog.c && ./prog
3
2
1
```

**`do-while`** checks *after* the body, so the body always runs at least once - useful for things like
"prompt the user, then re-prompt while their input is bad":

```c
int n = 0;
do {
    printf("ran once even though n starts at 0\n");
} while (n > 0);
```
```console
$ gcc -Wall -o prog prog.c && ./prog
ran once even though n starts at 0
```

**`for`** bundles init, condition, and post-step into one line - it's the loop you reach for when you know
how many times you're iterating:

```c
for (int i = 0; i < 3; i++) {
    printf("%d\n", i);
}
```
```console
$ gcc -Wall -o prog prog.c && ./prog
0
1
2
```

```mermaid
flowchart LR
  I[init: i = 0] --> C{i < 3?}
  C -- yes --> B[run body] --> P[i++] --> C
  C -- no --> D[loop exits]
```

*What just happened:* `int i = 0` runs once, before anything else. Then C checks `i < 3`; if true, it
runs the body, runs `i++`, and checks the condition again. It exits the moment the check fails - the body
never runs a fourth time.

⚠️ **The gotcha that costs people an hour: a stray semicolon.** This compiles cleanly and silently loops
forever, doing nothing:

```c
int n = 0;
while (n < 5);      // <-- that semicolon is the entire loop body!
{
    printf("n is %d\n", n);   // this block is NOT part of the loop
    n++;
}
```

The `;` right after `while (n < 5)` *is* the loop body - an empty statement that does nothing, checked
forever, since `n` never changes. The `{ ... }` that follows just runs once, separately, after the (never
happening) loop ends. This is one of the most common "my program hung" bugs in C, and the fix is simply:
never put a semicolon directly after a loop's condition unless you mean an empty body on purpose.

## `break` and `continue`

`break` exits the nearest loop (or `switch`) immediately. `continue` skips straight to the next iteration's
condition check (in a `for` loop, the update step - like `i++` - runs first), without running the rest of the body.

```c
for (int i = 0; i < 10; i++) {
    if (i == 3) continue;   // skip printing 3
    if (i == 6) break;      // stop entirely at 6
    printf("%d\n", i);
}
```
```console
$ gcc -Wall -o prog prog.c && ./prog
0
1
2
4
5
```
*What just happened:* at `i == 3`, `continue` jumped straight back to `i++` and the condition check,
skipping the `printf`. At `i == 6`, `break` left the loop entirely - `4` and `5` printed, but nothing from
`6` onward.

## `switch`: choosing between many exact values

**What it actually is.** `switch` compares one value against a list of exact constants and jumps to the
matching `case`. It exists (rather than everyone just writing a chain of `else if`s) because a compiler can
often turn a `switch` on a dense range of values into a *jump table* - one lookup straight to the right
code, instead of testing conditions one by one. That's the design trade-off: `switch` is less flexible than
`if`/`else if` (only exact-value matches, no ranges or `>`/`<`) in exchange for being potentially faster.

**The gotcha that defines this construct: fallthrough.** Unlike `if`/`else if`, a `switch` does *not* stop
after the matching case - it keeps running every case *below* it too, until it hits a `break` or the end
of the block.

```c
int day = 6;

switch (day) {
    case 6:
        printf("Saturday\n");
    case 7:
        printf("Sunday\n");
        break;
    default:
        printf("Weekday\n");
}
```
```console
$ gcc -Wall -o prog prog.c && ./prog
Saturday
Sunday
```
*What just happened:* `day` matched `case 6`, printed `Saturday`, and because that `case` has no `break`,
execution fell straight through into `case 7` and printed `Sunday` too, only stopping at *its* `break`.
This is intentional C behavior, not a bug in the language - but forgetting a `break` when you didn't mean
to fall through is one of the most common `switch` mistakes there is. **Why this saves you later:** when
you write a `switch`, put a `break` at the end of every case unless you are deliberately using
fallthrough (and if you are, a `// falls through` comment tells the next reader it's on purpose).

## Short-circuit evaluation: `&&` and `||`

**What it does in real life.** C evaluates `&&` and `||` left to right and stops as soon as the overall
result is decided - it never evaluates the right side if the left side already settled the answer.

```c
int *p = NULL;

if (p != NULL && *p > 0) {
    printf("positive\n");
}
```
```console
$ gcc -Wall -o prog prog.c && ./prog
$
```
*What just happened:* nothing printed, and nothing crashed. `p != NULL` was false, and because `&&` short-
circuits, C never evaluated `*p > 0` at all - it skipped dereferencing a null pointer, which would have
crashed the program. This pattern (checking a pointer is valid *before* using it, in the same `if`, relying
on short-circuiting) is everywhere in real C code. `||` short-circuits the same way in the other direction:
if the left side is already true, the right side never runs.

## Recap

1. Every control flow construct is a conditional jump on the program counter - `if`, `while`, `for`, and
   `switch` are all shapes of the same idea.
2. C has no true boolean at its core: `0` is false, anything else is true. `stdbool.h` gives you readable
   names for the same integers.
3. `if`/`else if`/`else` runs the first true branch and skips the rest; always brace nested `if`s to avoid
   the dangling-else trap.
4. `=` inside a condition assigns and is (almost) always a bug - `-Wall` will warn you, so always compile
   with it.
5. `while` checks before the body, `do-while` checks after (so it runs at least once), `for` bundles
   init/condition/post into one line.
6. A semicolon right after a loop's condition creates an empty body and an infinite, do-nothing loop.
7. `switch` falls through every case below the match unless you `break` - useful on purpose sometimes,
   a common bug the rest of the time.
8. `&&` and `||` short-circuit, which is what makes `p != NULL && *p > 0` safe.

Test yourself on the ideas that trip people up most in this phase:

```quiz
[
  {
    "q": "What happens when you compile and run `int x = 0; if (x = 5) { printf(\"yes\\n\"); }`?",
    "choices": [
      "It prints \"yes\", because `x = 5` assigns 5 to x and then evaluates to 5, which is truthy",
      "It doesn't compile, because `=` isn't allowed inside an `if`'s parentheses",
      "It prints nothing, because `x = 5` is treated as a comparison and 5 doesn't equal x's old value",
      "It's undefined behavior and could do anything"
    ],
    "answer": 0,
    "explain": "`=` assigns and the assignment expression evaluates to the value assigned; 5 is nonzero, so the branch runs every time regardless of x's prior value - this is why -Wall's warning about it matters."
  },
  {
    "q": "In a `switch` where `case 6:` matches but has no `break`, what happens next?",
    "choices": [
      "C jumps straight out of the switch, since a match was already found",
      "C keeps running the code in the cases below it until it hits a `break` or the end of the switch",
      "C raises a compile error, since every case needs a `break`",
      "C re-checks the switch value against the next case's condition before deciding whether to run it"
    ],
    "answer": 1,
    "explain": "Unlike if/else if, a matched case with no break falls through and runs every subsequent case's code unconditionally, regardless of whether their labels also match."
  },
  {
    "q": "Why does `if (p != NULL && *p > 0)` safely avoid crashing when `p` is `NULL`?",
    "choices": [
      "C evaluates both sides first and only crashes if both are true",
      "`&&` short-circuits: since `p != NULL` is false, C never evaluates `*p > 0` at all",
      "Dereferencing a NULL pointer in an `if` condition never crashes in C",
      "The compiler reorders the checks so the safer one always runs first"
    ],
    "answer": 1,
    "explain": "`&&` evaluates left to right and stops as soon as the result is decided, so a false left side skips the right side entirely - that's what makes the null-check-then-use pattern safe."
  }
]
```


---

# Functions & Program Structure

You've already written functions without calling them that - `main` is one. Phase 3 gave you the tools to make decisions and repeat work *inside* a function. This phase is about the next size up: how you split a program into named, reusable pieces, and one C-specific rule that trips up almost everyone coming from a language that doesn't have it - **C reads your file top to bottom, and it needs to know a function exists before you call it.**

## What a function actually is

**What it actually is.** A function is a named block of code with its own tiny workspace: a set of parameters it receives, local variables that live only while it runs, and (usually) a value it hands back when it's done. Calling a function is a detour - your program jumps to the function's code, runs it, and jumps back to right after the call, carrying the return value with it.

**Why this exists.** Without functions, every program is one long list of instructions, and any logic you need twice you have to retype twice - and fix twice when it has a bug. A function names a piece of logic once. From then on you refer to the *name*, not the mechanics, which is exactly what "goes to the store" means to you without re-explaining what a store is.

Here's the shape:

```c
return_type function_name(parameter_type parameter_name, ...) {
    // body
    return value;   // only if return_type isn't void
}
```

A concrete one:

```c
int square(int n) {
    return n * n;
}
```

Read that signature like a sentence: "`square` takes an `int` named `n`, and gives back an `int`." The name, the inputs, and the output are all part of the contract - anyone calling `square` knows exactly what to hand it and what they'll get.

## Declaring, defining, and calling

In C, "declaring" a function (telling the compiler it exists and what its signature is) and "defining" it (writing its body) are two different things that are easy to conflate at first. The version above is a **definition** - it has a body, so it's both a declaration and a definition at once.

To *use* a function, you call it by name with arguments in parentheses:

```c
#include <stdio.h>

int square(int n) {
    return n * n;
}

int main(void) {
    int result = square(5);
    printf("%d\n", result);   // 25
    return 0;
}
```
```console
$ gcc squares.c -o squares && ./squares
25
```

*What just happened:* `square(5)` jumped into `square` with `n` set to `5`, ran `return n * n;`, and handed `25` back to the spot that called it. `result` now holds `25`, just like any other `int`.

## Parameters are copies: pass by value

**What it actually is.** When you call `square(5)`, C doesn't hand `square` a live connection to whatever variable you passed - it copies the value into `n`. `n` is a brand-new local variable that happens to start with the same value. Anything `square` does to `n` has zero effect on the caller's variable.

**Why this matters.** This is the single most common surprise for beginners, so let's watch it happen:

```c
#include <stdio.h>

void try_to_double(int x) {
    x = x * 2;
    printf("inside try_to_double: x = %d\n", x);
}

int main(void) {
    int num = 10;
    try_to_double(num);
    printf("back in main: num = %d\n", num);
    return 0;
}
```
```console
$ gcc double.c -o double && ./double
inside try_to_double: x = 20
back in main: num = 10
```

*What just happened:* `x` inside `try_to_double` is a completely separate variable from `num`. Doubling `x` doubles `x` - `num` back in `main` never even knew the function ran. This is **pass by value**: every argument you pass to a C function is copied.

This isn't a limitation you have to route around forever - it's the rule that makes reading code predictable: a function can never surprise you by silently rewriting a variable you passed it, unless you explicitly hand it a pointer to that variable. That's exactly what Phase 5 (Pointers I) is about, and this is precisely the problem pointers solve. File that feeling away - you'll want it back in one phase.

## Return values, and functions that return nothing

A function returns at most **one** value, and its type has to match the `return_type` in the signature:

```c
double average(int a, int b) {
    return (a + b) / 2.0;
}
```

If a function doesn't hand anything back - it just does something, like printing - its return type is `void`:

```c
void greet(const char *name) {
    printf("Hello, %s!\n", name);
    // no return statement needed, or a bare `return;` to exit early
}
```

`void` isn't "nothing happened" - it's the type-level promise "don't try to use this call as a value," which is why `int x = greet("Sam");` won't compile.

## The rule that catches everyone: declare before you call

C reads your file top to bottom. When it reaches a function call, it needs to already know that function's signature - what it returns, what it takes - to check your call is correct. If `main` is at the top of your file and it calls a function defined further down, the compiler hasn't seen that function yet:

```c
#include <stdio.h>

int main(void) {
    printf("%d\n", square(5));   // square isn't known yet!
    return 0;
}

int square(int n) {
    return n * n;
}
```

Older compilers would silently guess and often get it wrong; modern `gcc`/`clang` reject this outright:

```console
$ clang oops.c -o oops
oops.c:4:20: error: call to undeclared function 'square'; ISO C99 and later do not
support implicit function declarations [-Wimplicit-function-declaration]
    4 |     printf("%d\n", square(5));
      |                    ^
```

The fix is a **function prototype**: the signature alone, with a semicolon instead of a body, placed above where it's first used. It's a promise to the compiler that the full definition is coming:

```c
#include <stdio.h>

int square(int n);   // prototype: "trust me, this exists"

int main(void) {
    printf("%d\n", square(5));   // now the compiler can check the call
    return 0;
}

int square(int n) {              // the real definition, anywhere after
    return n * n;
}
```

This single rule is *why* C programs get structured the way they do: put `main` last (it usually calls everything else), or put prototypes up top so order stops mattering. Phase 8 (Header Files & the Preprocessor) takes this further - it shows you how prototypes get collected into `.h` files so multiple `.c` files can share them, which is how real C programs are organized across files instead of one giant one.

## Structuring a small program

Put it together and a real pattern emerges: `main` becomes a short list of calls to well-named functions, and the details live inside each one.

```c
#include <stdio.h>

int square(int n);
int cube(int n);
void print_result(const char *label, int value);

int main(void) {
    int n = 4;
    print_result("square", square(n));
    print_result("cube", cube(n));
    return 0;
}

int square(int n) {
    return n * n;
}

int cube(int n) {
    return n * n * n;
}

void print_result(const char *label, int value) {
    printf("%s is %d\n", label, value);
}
```
```console
$ gcc shapes.c -o shapes && ./shapes
square is 16
cube is 64
```

*What just happened:* the prototypes at the top let `main` call functions defined below it. Reading `main` now reads like a summary of the program - "compute the square, print it; compute the cube, print it" - and each function's own logic is off doing one clear job. This is the shape almost every C program grows into: small functions, a `main` that orchestrates them, and prototypes bridging the gap between "where it's used" and "where it's defined."

## Recap

1. A function packages a name, parameters, and (usually) a return type around a block of code you can call from anywhere.
2. **C passes arguments by value** - every argument is copied into the function's local parameter; changes inside a function never reach the caller's variable. Pointers (Phase 5) are how you opt out of this on purpose.
3. `void` marks a function that returns nothing; a return type otherwise has to match what you actually `return`.
4. **C compiles top to bottom** and needs a function's signature before you call it - a **prototype** (`int square(int n);`) declares it early so the definition can live anywhere after.
5. Real C programs read like a short `main` calling well-named helper functions, with prototypes up top making the call order independent of the definition order.

## Quick check

Test yourself on the two ideas that trip people up most: what a function call actually copies, and why order matters when C reads your file.

```quiz
[
  {
    "q": "In `try_to_double(num)`, the function doubles its parameter `x` but `num` back in `main` is unchanged. Why?",
    "choices": [
      "`x` is a separate local variable that started with a copy of `num`'s value - changing `x` never touches `num`",
      "`try_to_double` has a bug and forgot to return the new value",
      "`num` is declared `const`, so it can't be changed",
      "C only copies values for `int`, not for other types"
    ],
    "answer": 0,
    "explain": "C passes arguments by value: every call copies the argument into the parameter, so the function works on its own private variable."
  },
  {
    "q": "A `main` at the top of the file calls `square(5)`, but `square` is defined further down with no prototype above `main`. What happens?",
    "choices": [
      "The compiler rejects it - it hasn't seen `square`'s signature yet and can't check the call",
      "It compiles and runs fine, since C looks through the whole file before running anything",
      "It compiles but crashes at runtime when `square` is reached",
      "It only fails if `square` returns something other than `int`"
    ],
    "answer": 0,
    "explain": "C reads your file top to bottom, so a function's signature (a prototype or full definition) has to appear before any call to it."
  },
  {
    "q": "What does declaring a function `void` actually promise?",
    "choices": [
      "That the function returns nothing, so its call can't be used as a value",
      "That the function takes no parameters",
      "That the function ran successfully with no errors",
      "That the function is faster than one with a return type"
    ],
    "answer": 0,
    "explain": "`void` is a type-level statement about the return: there is no value to hand back, so `int x = greet(...);` won't compile."
  }
]
```


---

# Pointers I: The Mental Model

This is the phase people warn you about. Maybe someone already told you pointers are where C gets hard, and you're bracing for a fight. Here's the reframe that changes everything: **a pointer is not a strange new kind of thing. It's just a number - the address of a byte in memory - with a type attached so C knows how to read what's there.** That's the whole idea. Everything else in this phase is you getting comfortable with the syntax for saying "give me that address" and "go look at what's at that address."

Every variable you've written since Phase 2 already lives at an address. You've just never asked for it. Pointers are what happen when you do.

## Memory is one long street of numbered boxes

Picture your computer's memory as a very long street of mailboxes, each one holding one byte, each one with a number painted on it - its **address**. When you write:

```c
int age = 30;
```

C finds an empty box (or four consecutive boxes, since an `int` is usually 4 bytes), writes `30` into it, and privately remembers which address that was so that whenever you write `age` in your code, it goes and reads from that spot. You never see the address. `age` is a friendly nickname for "whatever is at address 0x7ffee2a1c."

A pointer is what you get when you stop letting C hide that address from you and ask for it directly.

## The address-of operator: `&`

`&variable` means "give me the address where this variable lives," not its value.

```c
#include <stdio.h>

int main(void) {
    int age = 30;
    printf("value of age:   %d\n", age);
    printf("address of age: %p\n", (void *)&age);
    return 0;
}
```
```console
$ gcc main.c -o main && ./main
value of age:   30
address of age: 0x7ffee2a1c9dc
```

That address will look different every time you run it (the OS places your program's memory in a slightly different spot each run, on purpose, for security). That's fine. The point isn't the specific number - it's that `age` genuinely lives *somewhere*, and `&age` is how you find out where.

## Declaring a pointer and storing an address

A pointer is a variable whose job is to hold an address instead of holding an `int` or a `char` directly. You declare one with a `*` in the type:

```c
int age = 30;
int *p = &age;   // p holds the address of age
```

Read `int *p` as "`p` is a pointer to an `int`." The type matters: it tells C how many bytes live at that address and how to interpret them, the same way `int` vs `char` tells C how to interpret a plain variable. A pointer without a type is just a bare number with no idea what's stored there.

## The dereference operator: `*`

`&` gets you an address from a variable. `*` does the opposite: given a pointer, it gets you the value sitting at the address it holds. This is called **dereferencing**.

```c
#include <stdio.h>

int main(void) {
    int age = 30;
    int *p = &age;

    printf("p holds address:      %p\n", (void *)p);
    printf("*p reads what's there: %d\n", *p);

    *p = 31;   // follow the pointer, change the value there
    printf("age is now:            %d\n", age);
    return 0;
}
```
```console
$ gcc main.c -o main && ./main
p holds address:      0x7ffee2a1c9dc
*p reads what's there: 30
age is now:             31
```

*What just happened:* `*p = 31` didn't touch `p` itself - `p` still holds the same address. It walked to that address and overwrote what was there. Since `p` points at `age`, changing `*p` changes `age`. This is the entire reason pointers exist: they let you reach out and touch a specific piece of memory from somewhere else in your program, instead of only being able to touch a value through its one original name.

Notice the same symbol, `*`, means two different things depending on where it appears, and this trips up everyone at first:

| Where you see `*` | What it means |
|---|---|
| `int *p` (in a declaration) | "`p` is a pointer to an `int`" - this is a type |
| `*p` (in an expression) | "the value at the address `p` holds" - this is dereferencing |

The rule of thumb: if `*` shows up right after a type name while you're declaring something, it's part of the type. Anywhere else, it means "go to that address."

## Why pointers exist at all

You might reasonably ask: why not just use the variable directly? Two reasons come up constantly once you start writing real programs, and both will get their own phase later - this is the preview so the *shape* of the idea is already in your head.

**Reason one: functions copy their arguments.** In C, when you call a function, every argument is copied in. If you pass `age` to a function and that function changes its local copy, `age` back in `main` never moves. If you want a function to actually modify a variable that lives in the caller, you pass a pointer to it instead - you hand the function the *address*, and it dereferences that address to make the change stick.

```c
void birthday(int *age_ptr) {
    *age_ptr = *age_ptr + 1;   // change the caller's variable, not a copy
}

int main(void) {
    int age = 30;
    birthday(&age);
    printf("%d\n", age);   // 31 - the real variable changed
    return 0;
}
```

**Reason two: some things are too big to copy everywhere, or need to be shared.** Passing a pointer means passing one small address instead of duplicating a large chunk of data every time you hand it to a function. You'll see this constantly once arrays and structs show up in the next two phases - both are usually passed around by pointer for exactly this reason.

## `NULL`: a pointer that points at nothing

A pointer is just an address, and sometimes you need to say "this pointer isn't pointing at anything valid right now." C's convention for that is `NULL`, defined in `<stddef.h>` (and pulled in by most standard headers) as address zero:

```c
#include <stddef.h>

int *p = NULL;   // p deliberately points at nothing
```

Dereferencing a `NULL` pointer - writing `*p` when `p` is `NULL` - is undefined behavior and almost always crashes your program (a "segmentation fault"). That's not a bug in C; it's the operating system protecting you: address zero is deliberately left unmapped so this mistake fails loudly instead of quietly corrupting something. You'll meet this crash for real the first time you forget to check a pointer before using it, and now you'll know exactly what it means: *something handed you a pointer that isn't pointing anywhere, and you dereferenced it anyway.*

A pointer that's declared but never given a value isn't automatically `NULL` - it holds garbage, a leftover address from whatever used that memory before. Always initialize a pointer, either to a real address or explicitly to `NULL`, before you use it.

## Picture it, one more time

```
Memory (a long street of addressed boxes):

  address:  0x1000   0x1004   0x1008   0x100c
            +------+ +------+ +------+ +------+
  contents: |  30  | | ???  | | ???  | |0x1000| <- p lives here
            +------+ +------+ +------+ +------+
              age                        p

  &age  ==  0x1000              (the address of age)
   p    ==  0x1000              (what p holds: age's address)
  *p    ==  30                  (what's stored at that address)
```

`age` and `p` are both variables with their own boxes. `age`'s box holds a number you care about. `p`'s box also holds a number - but that number is itself an address, pointing back at `age`'s box. `*p` means "don't stop at `p`'s box, follow the address inside it, and read that box instead."

## Recap

1. Every variable lives at an **address** in memory; `&variable` gets you that address.
2. A **pointer** is a variable that stores an address, declared with `type *name`.
3. `*pointer` **dereferences** it - follows the address to read or write the value there.
4. `*` means two different things depending on context: part of a type in a declaration, "follow this address" in an expression.
5. Pointers exist so functions can modify a caller's variable (pass `&x`, not `x`) and so large data can be shared without copying.
6. `NULL` is the address that means "points at nothing." Dereferencing it is undefined behavior - almost always an instant crash.

This is the model everything else in C builds on. Arrays, strings, structs, dynamic memory - all of it is pointers doing more elaborate versions of exactly what you just did with `age` and `p`.

### Check yourself

```quiz
[
  {
    "q": "What does the `*` mean in `*p = 31;` versus in `int *p;`?",
    "choices": [
      "In both cases it declares p as a pointer type",
      "In `int *p` it's part of the type (pointer to int); in `*p = 31` it dereferences - follows the address to write there",
      "In both cases it dereferences p and writes 31",
      "It doesn't matter, both are the same operation"
    ],
    "answer": 1,
    "explain": "The same symbol means two different things depending on context: part of a type in a declaration, or 'follow this address' in an expression."
  },
  {
    "q": "Why does `void birthday(int *age_ptr)` change the caller's `age`, while a plain `void birthday(int age)` wouldn't?",
    "choices": [
      "C copies arguments by default; passing the address lets the function dereference it and modify the original variable's memory directly",
      "Pointers are passed by reference automatically, unlike normal variables",
      "int *age_ptr somehow shares the same variable name as age",
      "It only works because age_ptr is declared inside main"
    ],
    "answer": 0,
    "explain": "Arguments are always copied in C; passing &age hands over the address so *age_ptr dereferences to the same memory as age, instead of a disposable local copy."
  },
  {
    "q": "A pointer is declared but never assigned a value, like `int *p;` with no initializer. What does p hold?",
    "choices": [
      "NULL, automatically, like most other languages",
      "Garbage - whatever leftover address happens to be in that memory, which is why you should always initialize it",
      "The address of the nearest existing variable",
      "0, the same as an uninitialized int"
    ],
    "answer": 1,
    "explain": "C doesn't zero-initialize pointers for you; an uninitialized pointer holds whatever leftover bits were already there, so dereferencing it before you set it to a real address or NULL is dangerous."
  }
]
```


---

# Arrays & Strings

You've met pointers now, so this phase is where they stop being an abstract idea and start doing real work. An array in C is nothing but a run of memory. A string in C is nothing but an array with a rule attached. Once you see both of those clearly, a huge amount of "why does C do it this way" clicks into place - including why `strcpy` can silently wreck your program if you're not careful.

## What an array actually is

**The mental model.** A C array is a fixed number of elements, laid out back to back in memory, with no gaps and no bookkeeping. That's it. There's no hidden length field, no bounds check, no metadata riding along with it. If you declare:

```c
int scores[5];
```

C reserves enough space for five `int`s in a row and calls the whole block `scores`. Picture five boxes glued together, numbered 0 through 4. `scores[2]` means "start at the beginning of this block, skip past two `int`-sized boxes, and read what's there." That skip-and-read is not a metaphor - it's literally what the compiler generates. `scores[2]` and `*(scores + 2)` compile to the same instructions.

```c
#include <stdio.h>

int main(void) {
    int scores[5] = {10, 20, 30, 40, 50};

    for (int i = 0; i < 5; i++) {
        printf("scores[%d] = %d\n", i, scores[i]);
    }

    printf("scores[2] and *(scores + 2): %d %d\n", scores[2], *(scores + 2));
    return 0;
}
```

```console
$ gcc scores.c -o scores && ./scores
scores[0] = 10
scores[1] = 20
scores[2] = 30
scores[3] = 40
scores[4] = 50
scores[2] and *(scores + 2): 30 30
```

**Why this matters.** Because there's no bounds check, `scores[5]` or `scores[-1]` doesn't error - it just reads (or writes) whatever memory happens to sit past the array. This compiles cleanly and might even "work" by accident. This is undefined behavior, and it's one of C's sharpest edges; you'll dig into it fully in phase 14, but keep it in the back of your mind starting now: **an array index is a promise you make to the compiler, not a rule it enforces for you.**

## Arrays decay to pointers

Here's the fact that explains half of C's function signatures: when you pass an array to a function, C doesn't copy the array. It hands the function a pointer to the array's first element. This is called **array decay**.

```c
#include <stdio.h>

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

int main(void) {
    int nums[4] = {1, 2, 3, 4};
    print_all(nums, 4);   // nums decays to &nums[0]
    return 0;
}
```

Notice `print_all` needed a separate `len` parameter. This is the direct consequence of decay: once `nums` becomes a bare pointer, the function has no way to know how many elements follow it. `sizeof(nums)` inside `main` gives you the full array's byte size (16, for four `int`s) - but `sizeof(arr)` inside `print_all` gives you the size of *a pointer* (8 on a 64-bit machine), because by the time it gets there, `arr` really is just a pointer. **You must always carry the length alongside the array once it crosses a function boundary.** This one habit prevents a large share of beginner C bugs.

## Strings: arrays with a sentinel

C has no string type. What C calls a "string" is a plain `char` array with one convention layered on top: **the string ends at the first byte that is `0`** (written `'\0'`, the null terminator). Everything before that byte is the text; that byte itself marks "stop here."

```c
char greeting[] = "Hi!";
```

This doesn't create a 3-character array. It creates a 4-character array: `{'H', 'i', '!', '\0'}`. The compiler adds that trailing zero for you automatically whenever you write a string literal. This is why `strlen("Hi!")` returns `3`, not `4` - `strlen` counts characters *up to* the terminator, not including it.

```c
#include <stdio.h>
#include <string.h>

int main(void) {
    char greeting[] = "Hi!";
    printf("bytes reserved: %zu\n", sizeof(greeting));   // 4 - includes '\0'
    printf("strlen: %zu\n", strlen(greeting));            // 3 - stops at '\0'
    return 0;
}
```

**Why this design.** Instead of a length prefix or a struct, C strings carry their end *inside themselves*, as one extra byte. It's cheap and simple - but it means every string function has to walk the bytes one at a time looking for that zero, and if the zero is missing (or in the wrong place), the function keeps reading into whatever memory comes next. That's not a corner case; it's the single most common source of C bugs in the wild.

## The `string.h` toolkit, and where it bites

`<string.h>` gives you the basic operations. The mental model for every one of them: **it walks bytes until it finds `'\0'`, and it does not know how big your destination buffer is.**

```c
#include <stdio.h>
#include <string.h>

int main(void) {
    char name[20] = "Ada";

    printf("length: %zu\n", strlen(name));       // 3
    strcat(name, " Lovelace");                    // appends onto name
    printf("after strcat: %s\n", name);            // "Ada Lovelace"

    char copy[20];
    strcpy(copy, name);                            // copies name into copy
    printf("copy: %s, equal? %d\n", copy, strcmp(name, copy) == 0);
    return 0;
}
```

```console
$ gcc names.c -o names && ./names
length: 3
after strcat: Ada Lovelace
copy: Ada Lovelace, equal? 1
```

That worked because `name[20]` had room. Change `name` to `char name[8]` and `strcat` would write past the end of the array - into memory that belongs to something else - without any warning at compile time or runtime. This is a **buffer overflow**, and `strcpy`/`strcat`/`gets` are the classic culprits because none of them take a size limit. The safer habit is to use the bounded versions, `strncpy` and `strncat`, and to always know how big your destination buffer actually is:

```c
char name[8];
strncpy(name, "Grace Hopper", sizeof(name) - 1);
name[sizeof(name) - 1] = '\0';   // strncpy doesn't guarantee a terminator - add it yourself
```

That last line matters: `strncpy` stops writing at the limit you give it, but if the source was longer than the buffer, it never writes a `'\0'` at all. You have to place one yourself, or every string function that touches `name` afterward will keep reading past it looking for a terminator that isn't there.

`strncpy` was never really designed as a safe `strcpy` (that manual terminator step is exactly why), so the more modern idiom is `snprintf(name, sizeof(name), "%s", source)`, which always null-terminates for you. Some platforms also offer `strlcpy` with the same guarantee.

## Multi-dimensional arrays

A 2D array like `int grid[3][4]` is not an array of pointers - it's one contiguous block of 12 `int`s, laid out row by row, that you address with two indices for convenience:

```c
int grid[2][3] = {{1, 2, 3}, {4, 5, 6}};
printf("%d\n", grid[1][2]);   // 6 - row 1, column 2
```

Under the hood, `grid[1][2]` is computed as "skip 1 full row (3 ints), then skip 2 more ints" - the same skip-and-read idea as a 1D array, just with an extra multiply. Keep that picture in mind and 2D arrays stop feeling like a separate feature; they're the same memory model, one dimension up.

## Recap

- An array is a contiguous, fixed-size block of memory with no bounds checking - `arr[i]` is `*(arr + i)` in disguise.
- Passing an array to a function decays it to a pointer to its first element; you lose the length and must pass it separately.
- A C string is a `char` array where the text ends at the first `'\0'` byte - there's no separate string type.
- `string.h` functions (`strlen`, `strcpy`, `strcat`, `strcmp`) walk bytes looking for `'\0'` and don't know your buffer's size - use the bounded `strn*` variants and terminate manually when needed.
- A 2D array is one flat block addressed with two indices, not an array of arrays of pointers.

Arrays and strings are where "C is just memory with syntax on top" stops being a slogan and starts being something you can predict. Next up: bundling related values together with structs.

## Quick check

Test yourself on the two ideas that trip up most beginners: array decay losing the length, and the null terminator being a convention, not a guarantee.

```quiz
[
  {
    "q": "Why does `print_all(int *arr, int len)` need a separate `len` parameter instead of just using `sizeof(arr)`?",
    "choices": [
      "Once an array is passed to a function it decays to a pointer, so `sizeof(arr)` gives the pointer's size, not the array's",
      "`sizeof` only works on arrays declared with `int`, not other types",
      "C requires every function parameter to have an explicit length, even for non-array types",
      "`len` is needed for the loop to run faster, not for correctness"
    ],
    "answer": 0,
    "explain": "Array decay means the function only ever receives a pointer to the first element - the size information is left behind in the caller, so it has to be passed explicitly."
  },
  {
    "q": "`char greeting[] = \"Hi!\";` followed by `strlen(greeting)` returns 3. Why not 4?",
    "choices": [
      "`strlen` counts bytes up to but not including the `'\\0'` terminator, while `sizeof(greeting)` (4) counts the terminator too",
      "The compiler drops the last character of every string literal",
      "`strlen` and `sizeof` are the same function under different names, so this is a bug",
      "`\"Hi!\"` is stored as 3 bytes because `!` doesn't count as a character"
    ],
    "answer": 0,
    "explain": "The literal is stored as 4 bytes (`'H'`, `'i'`, `'!'`, `'\\0'`), but strlen walks the array and stops counting the moment it hits the null byte."
  },
  {
    "q": "After `strncpy(name, \"Grace Hopper\", sizeof(name) - 1);` on `char name[8]`, why is the next line `name[sizeof(name) - 1] = '\\0';` necessary?",
    "choices": [
      "`strncpy` stops writing at the given limit but won't add a `'\\0'` itself if the source was longer than that limit, so one must be placed manually",
      "It isn't necessary - `strncpy` always null-terminates its destination",
      "It resets `name` to an empty string before the copy happens",
      "It's only needed on some compilers, not as a rule of the language"
    ],
    "answer": 0,
    "explain": "strncpy's size limit protects against overflow, but if the source text is longer than that limit it just stops writing - it never guarantees a trailing '\\0', so leaving that line out can leave name un-terminated."
  }
]
```


---

# Structs & Typedef

Every type you've used so far holds one kind of thing: an `int` holds a number, a `char[]` holds a run of characters. But real data is rarely that simple. A point on a screen is an x *and* a y. A person is a name *and* an age *and* a balance. You could track these as separate variables - `int x; int y;` - but nothing stops you from updating `x` and forgetting `y`, and you can't pass "a point" to a function as one thing. You'd have to pass two arguments and hope you keep them in sync forever.

**A struct is C's answer: a new type you define, made of other types glued together, that the compiler treats as one value.** Move the struct, and every field moves with it. Pass it to a function, and all its fields go together, correctly, every time. This is the last basic building block before the deep half of this guide, and it matters more than it looks: chapter 10 onward, you'll build linked lists and trees out of structs that point to other structs.

## Defining and using a struct

Here's a struct for a point in 2D space:

```c
#include <stdio.h>

struct point {
    int x;
    int y;
};

int main(void) {
    struct point p1 = { 3, 4 };   // fields set in order: x=3, y=4
    printf("p1 = (%d, %d)\n", p1.x, p1.y);

    p1.x = 10;                    // dot (.) accesses a field on the struct itself
    printf("p1 = (%d, %d)\n", p1.x, p1.y);

    return 0;
}
```
```console
$ gcc point.c -o point && ./point
p1 = (3, 4)
p1 = (10, 4)
```

*What just happened:* `struct point { ... };` defines a new type called `struct point` - notice the keyword `struct` is part of the type's name, not optional decoration. `p1.x` reads "the `x` field of `p1`", using the dot operator. That's the whole model: a struct is a labeled box of smaller boxes, and the dot reaches into one of them.

## Struct assignment copies everything

This is the detail that surprises people coming from other languages, and it connects straight back to what you learned about arrays in phase 6: a plain `struct point p2 = p1;` does **not** make `p2` point at `p1`'s data. It copies every field, field by field, into a brand new struct.

```c
struct point p1 = { 3, 4 };
struct point p2 = p1;   // full copy: p2.x = 3, p2.y = 4

p2.x = 99;
printf("%d %d\n", p1.x, p2.x);   // 3 99 - p1 was never touched
```

Unlike an array, a struct is a value you can assign, copy, and return from a function whole - the compiler generates the field-by-field copy for you. That's genuinely convenient for small structs. For big ones (say, a struct holding a 10,000-element array), it means `p2 = p1` silently copies a lot of bytes, which is worth knowing before you do it in a hot loop.

## Passing structs to functions: by value, or by pointer

Because assignment copies, passing a struct as a function argument copies it too - the function gets its own private copy and any changes it makes vanish when it returns:

```c
void move_wrong(struct point p) {
    p.x += 1;   // only changes the local copy
}

void move_right(struct point *p) {
    p->x += 1;  // changes the original, through the pointer
}

int main(void) {
    struct point p1 = { 3, 4 };
    move_wrong(p1);
    printf("%d\n", p1.x);   // 3 - unchanged, move_wrong only touched a copy

    move_right(&p1);
    printf("%d\n", p1.x);   // 4 - the real thing changed
}
```

`move_right` takes a `struct point *` - a pointer to a point, exactly the pointer mental model from phase 5. Inside, `p->x` means "follow the pointer, then access field `x`" - it's shorthand for `(*p).x`, which is clunky enough that C gives you the arrow operator instead. **Rule of thumb: pass small structs by value when you just need to read them; pass a pointer when the function needs to modify the original, or when the struct is large enough that copying it is wasteful.**

| Passing style | Syntax | Function sees | Use when |
|---|---|---|---|
| By value | `f(struct point p)` | a copy | struct is small, read-only use |
| By pointer | `f(struct point *p)` | the original, via `p->field` | need to modify it, or it's large |

## Nested structs

Structs can contain other structs. A rectangle is naturally two points:

```c
struct rect {
    struct point top_left;
    struct point bottom_right;
};

struct rect r = { { 0, 0 }, { 10, 5 } };
printf("width = %d\n", r.bottom_right.x - r.top_left.x);   // 10
```

Chain the dots to reach through the nesting: `r.bottom_right.x` means "in `r`, get `bottom_right`, then get its `x`." Nothing new is happening here, it's the same dot operator, just applied twice.

## typedef: giving a struct a shorter name

Notice that every declaration above had to say `struct point`, not just `point`. That's because in C, defining `struct point` only creates the tag `point` under the `struct` namespace - it doesn't create a plain type name you can use on its own. Writing `struct` everywhere works fine, but it's noisy, and almost every real C codebase uses `typedef` to skip it:

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

int main(void) {
    Point p1 = { 3, 4 };   // no "struct" needed
    printf("(%d, %d)\n", p1.x, p1.y);
    return 0;
}
```

**What `typedef` actually is: it defines an alias for a type, nothing more.** `typedef struct { ... } Point;` says "define an anonymous struct, then let `Point` be another name for it." It doesn't change how the struct works, doesn't add behavior, doesn't cost anything at runtime - it's purely a name the compiler substitutes at compile time. You'll see this exact `typedef struct { ... } Name;` pattern constantly; it's the standard way to define a struct type in C, precisely so you never have to type `struct` again.

You can also `typedef` a *named* struct separately, which matters once structs start referencing themselves (linked lists, trees - coming in the deep half of this guide):

```c
typedef struct point Point;   // Point is now an alias for struct point
struct point { int x; int y; };
```

Both spellings show up in real code; the anonymous version above is more common for simple data-only structs.

## A quick look ahead: self-referential structs

One thing a struct *can't* contain is itself, directly - `struct node { struct node next; }` would need infinite memory, since every `node` would contain another whole `node` forever. But a struct absolutely can contain a *pointer* to its own type, because a pointer is always the same fixed size regardless of what it points to:

```c
typedef struct node {
    int value;
    struct node *next;   // a pointer to another node - this is legal
} Node;
```

This one pattern - a struct holding a pointer to its own type - is the entire foundation of linked lists, trees, and most of the dynamic data structures you'll build once you reach dynamic memory in phase 10. File it away; you'll use it constantly from here on.

## Recap

1. A **struct** bundles different types into one named type; access fields with `.`
2. **Assigning a struct copies every field.** It's a real value, not a reference - unlike an array.
3. Pass structs **by value** to give a function a private copy, or **by pointer** (`->` to access fields) so it can modify the original or avoid copying a large struct.
4. Structs can **nest**; chain `.` to reach through the layers.
5. **`typedef`** just creates an alias for a type - `typedef struct { ... } Name;` is the standard way to skip writing `struct` everywhere.
6. A struct can hold a **pointer to its own type** (not itself directly) - the pattern behind every linked data structure to come.

### Check yourself

```quiz
[
  {
    "q": "After `struct point p2 = p1;` followed by `p2.x = 99;`, what happens to `p1.x`?",
    "choices": [
      "It also becomes 99, since p2 points at p1's data",
      "It stays whatever it was before - p2 is a separate copy",
      "It's undefined behavior",
      "It becomes 0"
    ],
    "answer": 1,
    "explain": "Struct assignment copies every field into a brand new struct, unlike an array; p1 and p2 share nothing after the copy."
  },
  {
    "q": "Why does `move_wrong(struct point p)` fail to change the caller's struct, while `move_right(struct point *p)` succeeds?",
    "choices": [
      "move_wrong has a bug in its syntax",
      "move_wrong receives a copy of the struct, so changes to it vanish when the function returns; move_right receives a pointer to the original",
      "Structs can never be modified inside a function",
      "move_right is faster, which is the only real difference"
    ],
    "answer": 1,
    "explain": "Passing a struct by value copies it just like assignment does; passing a pointer lets the function reach through to the original via ->."
  },
  {
    "q": "Why can `struct node { struct node *next; }` compile, when `struct node { struct node next; }` cannot?",
    "choices": [
      "The pointer version is a stylistic choice with no real difference",
      "A pointer is always a fixed size no matter what it points to, so it doesn't require infinite memory; a direct struct-inside-itself would",
      "C only allows pointers to be named next",
      "The non-pointer version is legal too, just slower"
    ],
    "answer": 1,
    "explain": "A struct containing itself directly would need infinite memory, since each copy holds another whole copy; a pointer is a fixed-size address regardless of what type it points to."
  }
]
```


---

# Header Files & the Preprocessor

Every real C program you've seen starts with a line like `#include <stdio.h>`. You've been typing it since
Phase 1 without asking what it means. Now that you can write functions (Phase 4) and structs (Phase 7),
it's time to answer that question properly - because the moment your program grows past one file, you
can't avoid understanding it.

**The mental model first, before any syntax:** C compiles one file at a time, and each file is compiled in
total isolation from every other file. If `main.c` calls a function defined in `math_utils.c`, the compiler
building `main.c` has never seen `math_utils.c` and never will. Header files and the preprocessor exist
to solve exactly that problem - and nothing more. Once that clicks, the rest of this phase is just
mechanics.

## The preprocessor: a text editor that runs before the compiler

Before your compiler reads a single line of C grammar, a separate pass called the **preprocessor** runs
over your source file and rewrites it as plain text. It doesn't understand types, functions, or scope - it
understands lines starting with `#`, and it does simple text substitution. Only after the preprocessor is
done does the actual compiler see the result.

You can watch this happen. Take this file:

```c
#define PI 3.14159

int main(void) {
    double area = PI * 2 * 2;
    return 0;
}
```

Run just the preprocessor step (most compilers support this):

```console
$ gcc -E main.c
```

```c
int main(void) {
    double area = 3.14159 * 2 * 2;
    return 0;
}
```

`PI` is gone, replaced everywhere by `3.14159`, before the compiler even starts. That's the whole
preprocessor in one example: it's a text substitution pass, not a programming language feature. Keep that
model in your head for everything below.

## `#define`: macros

`#define NAME value` creates a macro - every later occurrence of `NAME` gets replaced with `value`, purely
textually.

```c
#define MAX_USERS 100
#define GREETING "Hello, friend"

int users[MAX_USERS];
```

Macros can also take arguments, acting like a function that's expanded inline:

```c
#define SQUARE(x) ((x) * (x))

int result = SQUARE(5);       // expands to ((5) * (5))
```

Notice the parentheses around `x` and around the whole expression. This isn't style - it's a real trap.
Without them:

```c
#define SQUARE(x) x * x

int result = SQUARE(2 + 3);   // expands to 2 + 3 * 2 + 3 = 11, not 25!
```

Because the preprocessor does dumb text substitution, `SQUARE(2 + 3)` literally becomes `2 + 3 * 2 + 3`,
and normal operator precedence takes over. Wrapping every parameter (and the full expression) in
parentheses is the standard defense: `((x) * (x))` expands to `((2 + 3) * (2 + 3))`, which is correct.

**When to reach for a macro vs. a real function.** Prefer a real function almost always - it type-checks
arguments, you can step through it in a debugger, and it doesn't have text-substitution surprises. Macros
still earn their keep for a few things a function can't do: defining constants used in array sizes (like
`MAX_USERS` above), conditional compilation (next section), and the rare case where you need code to work
across multiple types without templates (C has no generics). If a function would do the job, use a
function.

## Conditional compilation

`#ifdef`, `#ifndef`, `#if`, `#else`, and `#endif` let the preprocessor include or exclude chunks of code
before compilation even happens - useful for platform-specific code or debug-only logging:

```c
#define DEBUG

int main(void) {
#ifdef DEBUG
    printf("debug: starting up\n");
#endif
    printf("Hello, World!\n");
    return 0;
}
```

If `DEBUG` isn't defined, the preprocessor deletes the `printf("debug: ...")` line entirely - it's not
"skipped at runtime," it never reaches the compiler at all. This is exactly the mechanism header guards
use, which brings us to the actual point of this phase.

## Why header files exist

Back to the isolation problem. Say you split your code into two files:

```c
/* math_utils.c */
int add(int a, int b) {
    return a + b;
}
```

```c
/* main.c */
int main(void) {
    int result = add(2, 3);   // compiler has never seen add()!
    return 0;
}
```

When `gcc` compiles `main.c`, it hits `add(2, 3)` with no idea what `add` is - what it returns, what
arguments it takes, whether it even exists. It's not a linking problem yet; it's that the compiler needs a
**declaration** of `add` before it can generate correct code for the call.

A header file is nothing but a place to put those declarations, so any `.c` file can `#include` them:

```c
/* math_utils.h */
int add(int a, int b);   // declaration only - no body, ends in a semicolon
```

```c
/* math_utils.c */
#include "math_utils.h"

int add(int a, int b) {  // the actual definition
    return a + b;
}
```

```c
/* main.c */
#include "math_utils.h"

int main(void) {
    int result = add(2, 3);   // compiler now knows add's signature
    return 0;
}
```

Remember what `#include` actually does: it's the preprocessor, so `#include "math_utils.h"` is replaced,
textually, by the entire contents of `math_utils.h`, pasted right there. `main.c` after preprocessing
literally contains the line `int add(int a, int b);` before `main`. That's the entire mechanism - no
magic, no special compiler knowledge of "headers." It's copy-paste, done before compilation.

Now `main.c` compiles fine, because it has `add`'s signature. But `main.c` alone doesn't have `add`'s
*body* - that's in `math_utils.c`. The compiler produces an object file for `main.c` with a placeholder
that says "something named `add` goes here," and the **linker** (Phase 9 covers this) stitches the two
object files together, matching that placeholder to `add`'s real body from `math_utils.c`'s object file.
Build it like this:

```console
$ gcc -c math_utils.c -o math_utils.o
$ gcc -c main.c -o main.o
$ gcc math_utils.o main.o -o program
$ ./program
```

**The rule that falls out of this:** a header holds *declarations* (function signatures, struct
definitions, `#define` constants) - things a caller needs to know to use your code. The `.c` file holds
*definitions* - the actual function bodies. This split is what lets `main.c` and `math_utils.c` be compiled
completely separately, in any order, and still work together.

## `<angle brackets>` vs `"quotes"`

You've used both without thinking about the difference:

```c
#include <stdio.h>      // system/standard library headers
#include "math_utils.h" // your own project headers
```

`<...>` tells the preprocessor to search the compiler's standard system directories. `"..."` tells it to
look in your project's own directory first (then fall back to the system paths). Use quotes for headers
you wrote; angle brackets for the standard library and installed libraries.

## Structs and constants belong in headers too

Anything another file needs to know the *shape* of goes in the header, not just functions:

```c
/* point.h */
#ifndef POINT_H
#define POINT_H

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

Point point_add(Point a, Point b);

#endif
```

Every `.c` file that includes `point.h` now knows exactly how big a `Point` is and what fields it has,
which it needs at compile time - the compiler must know a struct's layout to generate code that touches
its fields, even before the linker does its job.

## Header guards: the problem of including the same header twice

Here's a real trap: if two headers both `#include "point.h"`, and a `.c` file includes both of *them*, the
preprocessor pastes `point.h`'s contents in twice. The compiler then sees `typedef struct { ... } Point;`
defined twice in the same file and rejects it - a redefinition error.

The fix is the `#ifndef` / `#define` / `#endif` pattern you saw above, called a **header guard**:

```c
#ifndef POINT_H
#define POINT_H

/* ... header contents ... */

#endif
```

Walk through what happens on the second inclusion. First time: `POINT_H` isn't defined yet, so
`#ifndef POINT_H` is true, the preprocessor defines `POINT_H` and includes the body. Second time (in the
same translation unit): `POINT_H` *is* now defined, so `#ifndef POINT_H` is false, and the preprocessor
skips straight to `#endif` - the body is never pasted in again. The name `POINT_H` is just a convention
(header name, uppercased, with `_` for `.`/`/`) - pick anything unique in your project, but match that
convention so nobody collides with it by accident.

Most compilers also support `#pragma once` as a shorter, non-standard alternative that does the same job
in one line at the top of the file. It's supported everywhere that matters in practice, but the classic
`#ifndef` guard is the one guaranteed by the C standard and the one you'll see in most real codebases, so
it's worth knowing both.

## Putting it together

A small multi-file project looks like this:

```
point.h        - declares the Point struct and point_add's signature
point.c        - #includes point.h, defines point_add's body
main.c         - #includes point.h, calls point_add
```

```c
/* point.h */
#ifndef POINT_H
#define POINT_H

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

Point point_add(Point a, Point b);

#endif
```

```c
/* point.c */
#include "point.h"

Point point_add(Point a, Point b) {
    Point result;
    result.x = a.x + b.x;
    result.y = a.y + b.y;
    return result;
}
```

```c
/* main.c */
#include <stdio.h>
#include "point.h"

int main(void) {
    Point p1 = {1, 2};
    Point p2 = {3, 4};
    Point sum = point_add(p1, p2);
    printf("(%d, %d)\n", sum.x, sum.y);
    return 0;
}
```

```console
$ gcc -c point.c -o point.o
$ gcc -c main.c -o main.o
$ gcc point.o main.o -o program
$ ./program
(4, 6)
```

Neither `.c` file ever saw the other's source code. `point.h` was the entire contract between them - and
the preprocessor is what made that contract visible to both, by pasting it in before compilation started.

## Recap

1. The **preprocessor** runs before the compiler and does text substitution - `#define`, `#include`,
   `#ifdef` are all preprocessor directives, not C language features.
2. `#include "file.h"` literally pastes that file's contents in, right there.
3. Headers hold **declarations** (what a caller needs to know); `.c` files hold **definitions** (the actual
   code) - that split is what lets files compile independently and get linked together after.
4. Wrap macro parameters and expressions in parentheses, or precedence will bite you.
5. **Header guards** (`#ifndef`/`#define`/`#endif`, or `#pragma once`) stop the same header from being
   pasted into a file twice and causing redefinition errors.

Phase 9 covers the tools that turn multiple `.c` files into one program automatically - Makefiles, and how
to actually debug the thing once it's built.

## Quick check

Test yourself on the ideas that make the rest of this guide's multi-file examples make sense:

```quiz
[
  {
    "q": "What does `#include \"math_utils.h\"` actually do?",
    "choices": [
      "The preprocessor pastes the entire text of math_utils.h into that spot, before compilation starts",
      "It tells the linker to look for math_utils.c when building the final program",
      "It imports the compiled math_utils.o object file into this source file",
      "It tells the compiler to search math_utils.c for any function it can't find"
    ],
    "answer": 0,
    "explain": "#include is a preprocessor directive, so it's a text substitution: the header's contents are copy-pasted in place before the compiler ever runs, not a special import or linking step."
  },
  {
    "q": "Why does `SQUARE(x)` need to be defined as `((x) * (x))` instead of `x * x`?",
    "choices": [
      "Because the preprocessor substitutes text literally, so SQUARE(2 + 3) without parentheses expands to 2 + 3 * 2 + 3 and normal operator precedence gives the wrong answer",
      "Because C requires all macro arguments to be wrapped in parentheses or it won't compile",
      "Because it makes the macro run faster at runtime",
      "Because otherwise SQUARE would be treated as a real function call"
    ],
    "answer": 0,
    "explain": "The preprocessor does dumb text substitution with no notion of precedence, so unparenthesized macro bodies and arguments can silently combine with surrounding operators in the wrong order."
  },
  {
    "q": "What problem do header guards (`#ifndef`/`#define`/`#endif`) actually solve?",
    "choices": [
      "They stop the same header's contents from being pasted into one file twice, which would otherwise redefine the same struct or type and fail to compile",
      "They prevent two different .c files in the project from both including the same header",
      "They make the linker skip duplicate function bodies across object files",
      "They speed up compilation by skipping headers that haven't changed"
    ],
    "answer": 0,
    "explain": "Without a guard, one file including two headers that both include a third header causes the preprocessor to paste that third header's contents in twice, and the compiler rejects the resulting duplicate struct/type definition."
  }
]
```


---

# Build Tooling: Makefiles & Debugging

So far every program in this guide has been one file, compiled with one `gcc` command you typed by hand. That works for a fifty-line program. It stops working the moment your project has ten source files, each depending on a couple of headers, and you're tired of remembering the exact incantation - which files to list, which flags, which order - every single time you change one line.

It also stops working the moment a bug doesn't show itself through `printf`. You've been debugging by scattering print statements through your code, rebuilding, and staring at the output. That's a real technique and you'll use it forever, but it has a ceiling: it only shows you what you thought to print, at the moment you thought to print it. Sometimes you need to pause the program mid-crash and actually look around.

This phase covers both, because they're really the same problem: **you've outgrown typing raw commands, and you need better tools than your eyes.**

## What `make` actually is

**The mental model.** A Makefile is a list of *targets* - things you want to produce - each with the *files it depends on* and the *shell commands that produce it*. When you run `make`, it doesn't blindly rebuild everything. It checks file timestamps: if a target's output already exists and is newer than everything it depends on, `make` skips it. Change one `.c` file, and `make` recompiles only that file (and re-links), not the whole project. That's the entire value proposition: **stop rebuilding what hasn't changed, and stop retyping what you already told the computer once.**

Here's the shape of a rule:

```makefile
target: dependencies
	command
```

That indentation before `command` **must be a real tab character**, not spaces. This is the single most common reason a Makefile mysteriously fails - `make: *** missing separator. Stop.` means you typed spaces where `make` demands a tab.

A minimal Makefile for a small project:

```makefile
CC = gcc
CFLAGS = -Wall -Wextra -g

myapp: main.o helpers.o
	$(CC) $(CFLAGS) -o myapp main.o helpers.o

main.o: main.c helpers.h
	$(CC) $(CFLAGS) -c main.c

helpers.o: helpers.c helpers.h
	$(CC) $(CFLAGS) -c helpers.c

clean:
	rm -f myapp main.o helpers.o
```

*What just happened:* `myapp` depends on two `.o` files; each `.o` depends on its `.c` file and the shared header. Run `make` and it builds bottom-up: compile `main.c` to `main.o`, compile `helpers.c` to `helpers.o`, link both into `myapp`. Edit only `helpers.c` and run `make` again - it recompiles `helpers.o` and re-links, but leaves `main.o` alone, because `main.c` didn't change. `clean` is a target with no real output file; it's just a name for "run this command," which is why it's called a *phony* target.

```console
$ make
gcc -Wall -Wextra -g -c main.c
gcc -Wall -Wextra -g -c helpers.c
gcc -Wall -Wextra -g -o myapp main.o helpers.o
$ make
make: 'myapp' is up to date.
$ touch helpers.c
$ make
gcc -Wall -Wextra -g -c helpers.c
gcc -Wall -Wextra -g -o myapp main.o helpers.o
```

`CC` and `CFLAGS` are variables - by convention `CC` is the compiler and `CFLAGS` the flags, and `make` treats them specially, but you're free to add your own. The two flags in `CFLAGS` here matter enough to call out on their own:

- **`-Wall -Wextra`**: turns on a broad set of the compiler's most useful warnings (not literally all of them - flags like `-Wconversion` and `-Wshadow` stay off unless you ask). C will happily compile code with a use-before-init, a mismatched type, or a comparison that can never be true - warnings catch these *before* they become the kind of bug you spend an afternoon hunting. Treat warnings as bugs waiting to happen. Real projects often add `-Werror` too, which turns every warning into a hard build failure.
- **`-g`**: embeds debug information (variable names, line numbers) into the binary. Without it, a debugger can only show you raw memory addresses. With it, a debugger can show you `x = 12` on `main.c:7`. You want `-g` on every build until you ship.

For anything past a toy project, people reach for [CMake](https://cmake.org) instead of hand-writing Makefiles - it generates the actual build files for you and handles cross-platform quirks Make doesn't. But CMake generates *a* Makefile under the hood, so understanding targets and dependencies here is what makes CMake's output legible later, not wasted effort.

## What a debugger actually is

**The mental model.** A debugger doesn't run your program differently - it runs the exact same binary, but it can *pause* it at any instruction, show you the value of every variable at that instant, and let you step forward one line at a time. `printf` debugging asks you to guess in advance what you'll need to know. A debugger lets you ask questions *after* the program is already broken, while it's still broken, instead of rebuilding and re-guessing.

The standard C debugger on Linux is **gdb**; on macOS you'll more often reach for **lldb**, which has near-identical commands. Compile with `-g`, then:

```console
$ gcc -g -Wall -Wextra -o myapp main.c
$ gdb ./myapp
```

Inside gdb, the moves you'll use constantly:

| Command | What it does |
|---|---|
| `break main.c:12` | Set a breakpoint - pause execution right before line 12 runs |
| `run` | Start the program (stops at the first breakpoint it hits) |
| `next` | Run the current line, step *over* function calls |
| `step` | Run the current line, step *into* function calls |
| `print x` | Show the current value of variable `x` |
| `backtrace` | Show the call stack - which function called which, all the way up |
| `continue` | Resume running until the next breakpoint or exit |

Say you have a function that computes a bad value somewhere in a loop:

```c
int sum_squares(int *arr, int n) {
    int total = 0;
    for (int i = 0; i <= n; i++) {   // bug: should be i < n
        total += arr[i] * arr[i];
    }
    return total;
}
```

Instead of guessing where it goes wrong, you'd set a breakpoint inside the loop and watch `i` and `arr[i]` on each pass:

```console
(gdb) break 4
(gdb) run
(gdb) print i
$1 = 0
(gdb) print arr[i]
$2 = 3
(gdb) continue
...
(gdb) print i
$3 = 5
```

When `i` reaches `5` on a 5-element array (valid indices `0`-`4`), you're staring directly at the off-by-one instead of inferring it from a crash message. That's the whole value of a debugger: it turns "the program produced a wrong number somewhere" into "I watched exactly where the number went wrong."

One more habit worth building now: when a program crashes with a segfault, running it under gdb and typing `run` then `backtrace` after the crash shows you the exact line and call chain that caused it - far faster than adding print statements and recompiling until you corner it. You'll lean on this constantly once pointers and dynamic memory enter the picture in the next few phases.

## Recap

1. A **Makefile** describes targets, their dependencies, and the commands that build them - `make` only rebuilds what's stale, based on file timestamps.
2. Rule bodies must be indented with a real **tab**, not spaces.
3. `-Wall -Wextra` catches real bugs at compile time; `-g` embeds the debug info a debugger needs.
4. A **debugger** pauses your actual running binary so you can inspect variables and step through code, instead of guessing what to `printf` in advance.
5. `break`, `run`, `next`/`step`, `print`, and `backtrace` cover the vast majority of real debugging sessions.

## Quick check

Test yourself on the two ideas that matter most here - why `make` only rebuilds what's stale, and what a debugger gives you that `printf` can't:

```quiz
[
  {
    "q": "In the Makefile mental model, why does running `make` a second time (with no files changed) skip rebuilding `myapp`?",
    "choices": [
      "make compares each target's timestamp to its dependencies' timestamps, and skips a target that's already newer than everything it depends on",
      "make remembers the exact shell command it ran last time and refuses to run it twice",
      "make hashes the contents of every file and skips any whose hash is unchanged",
      "make always rebuilds only the first target listed in the Makefile"
    ],
    "answer": 0,
    "explain": "make's entire value proposition is timestamp comparison, not content hashing or command memoization: if a target is already newer than everything it depends on, there's nothing to do."
  },
  {
    "q": "A Makefile rule body must be indented with:",
    "choices": [
      "Any consistent whitespace - spaces or tabs, as long as it's the same throughout the file",
      "Two spaces, matching common style guides for other languages",
      "A literal tab character - spaces produce a `missing separator` error"
    ],
    "answer": 2,
    "explain": "make treats the indentation before a command as a syntax marker, not cosmetic whitespace, and only a real tab satisfies it."
  },
  {
    "q": "You're chasing a bug where a loop reads one element past the end of an array. What's the core advantage of stepping through it in gdb instead of adding more printf calls?",
    "choices": [
      "gdb lets you inspect any variable's value at the moment execution is paused, instead of only seeing values you thought to print in advance",
      "gdb automatically rewrites the loop to fix off-by-one errors",
      "gdb runs the compiled binary faster than running it directly"
    ],
    "answer": 0,
    "explain": "printf only shows what you decided to log ahead of time; a debugger pauses the actual running binary so you can ask questions after you already know something went wrong."
  }
]
```


---

# Dynamic Memory: malloc & free

Every array you've written so far has a size baked in at compile time: `int scores[10];` means "ten ints, forever, decided the moment this line was written." That's fine when you know the size up front. But most real programs don't. You don't know how many lines are in the file the user picked. You don't know how many results a search will return. You don't know how big the user's input is until they type it.

C's answer is **dynamic memory**: you ask for a block of memory *while the program is running*, sized exactly to what you need right now, and you get a pointer to it. This phase is about the four functions that make that possible - `malloc`, `calloc`, `realloc`, and `free` - and the single discipline that keeps them from turning into bugs.

## The mental model: asking a stranger for space

Picture your program's memory as two very different neighborhoods, which we'll properly tour in Phase 11 (The Stack vs the Heap). For now, you only need one idea: there's a region called the **heap**, and it's not managed by the compiler the way local variables are. Nothing about the heap knows your function names or your scopes. It's just a big pool of free bytes, and there's a librarian in front of it - the memory allocator - whose whole job is to hand out chunks of that pool on request and take them back when you're done.

`malloc` is you walking up to that librarian and saying "I need 40 bytes." The librarian finds 40 free bytes somewhere in the pool, marks them as yours, and hands you a pointer to the start. `free` is you walking back and saying "I'm done with those 40 bytes" - the librarian marks them free again so someone else can use them.

Nobody does this automatically for you. If you forget to give the memory back, the librarian has no way to know you're done - that block is gone for the rest of the program's life. That's a **memory leak**, and it's the first of several footguns we'll name properly in Phase 14. For now, just hold onto the shape of the deal: **every successful `malloc` (or `calloc`/`realloc`) owes exactly one `free`.**

## malloc: asking for raw bytes

`malloc` (memory allocate) takes a number of bytes and returns a pointer to that many free bytes, or `NULL` if it couldn't find that much memory.

```c
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *scores = malloc(5 * sizeof(int));   // room for 5 ints
    if (scores == NULL) {
        fprintf(stderr, "out of memory\n");
        return 1;
    }

    for (int i = 0; i < 5; i++) {
        scores[i] = i * 10;
    }

    for (int i = 0; i < 5; i++) {
        printf("%d\n", scores[i]);
    }

    free(scores);
    return 0;
}
```

A few things to notice, because each one is a real habit, not decoration:

**`sizeof(int)`, not the number 4.** `malloc` only knows bytes, it has no idea what you're storing. `sizeof(int)` asks the compiler for the true size of an `int` on *this* platform (usually 4, but not guaranteed), so `5 * sizeof(int)` is "enough bytes for 5 ints" on any machine your code runs on. Hardcoding `20` works today and breaks the day someone builds your code somewhere `int` is a different size.

**No cast on the return value.** `malloc` returns `void *` - a pointer with no type, meaning "a pointer to *some* bytes, you decide what." In C, `void *` converts to any other pointer type automatically, so `int *scores = malloc(...)` just works. You'll see older code write `(int *) malloc(...)`. That cast isn't wrong, but it's not needed in C, and historically it could hide a real bug: forget to `#include <stdlib.h>`, and older compilers assumed `malloc` returned an `int` - the cast silenced the warning you'd otherwise get from stuffing that `int` into a pointer. Modern C compilers flag the missing header on their own, so the trap is smaller today, but leaving the cast off is still the tidier habit.

**Always check for `NULL`.** `malloc` fails when the system is out of memory, and it signals that failure by returning `NULL` instead of a real pointer. If you skip the check and use the pointer anyway, you dereference `NULL` and crash - or worse, on some systems, corrupt something before you crash. Checking costs three lines. Skipping it costs a debugging session six months from now when the program finally runs on a machine tight on memory.

## free: giving it back

```c
free(scores);
```

That's the whole call - just the pointer, no size. The allocator already remembers how big your block was, because it tracked that bookkeeping when you called `malloc`. Your job is only to say *which* block you're done with.

Two rules that matter more than they look:

- **Free exactly once.** Calling `free` twice on the same pointer (a "double free") corrupts the allocator's own bookkeeping - undefined behavior, and a nasty one, covered in depth in Phase 14.
- **Don't use the pointer after freeing it.** Once you call `free(scores)`, `scores` still holds the old address, but that memory no longer belongs to you. Reading or writing through it is a **use-after-free** - the allocator may have already handed that same address to someone else. A cheap habit that catches a lot of bugs: set the pointer to `NULL` right after freeing it. Using a `NULL` pointer crashes immediately and loudly; using a dangling pointer corrupts something quietly, maybe far from where the real mistake was.

```c
free(scores);
scores = NULL;   // now any accidental reuse crashes fast, instead of corrupting quietly
```

## calloc: malloc, but zeroed

`malloc` hands you memory as-is - whatever bytes happened to be sitting there, which is often garbage left over from something else. `calloc` (a handy way to remember it: "clear allocate") gives you the same kind of block, but guarantees every byte starts at zero, and it takes the count and the element size as two separate arguments instead of one:

```c
int *scores = calloc(5, sizeof(int));   // 5 ints, all zero-initialized
```

Use `calloc` whenever "starts at zero" matters - a counter array, a buffer you'll read from before fully writing to. Use `malloc` when you're about to overwrite every byte anyway; skipping the zeroing is marginally faster, though it rarely matters in practice.

Splitting the count and size across two arguments buys you one more thing: `calloc` checks `count * size` for overflow and returns `NULL` if that multiplication would wrap around to a small number. With `malloc(count * size)` you compute that product yourself, and if it overflows you'd get a too-small block that looks like it succeeded - a classic source of buffer overflows.

## realloc: growing (or shrinking) a block

Sometimes you don't know the final size even when you start filling the block - you're reading lines from a file and have no idea how many there'll be. `realloc` resizes an existing allocation, copying the old contents into the new block if it has to move:

```c
int capacity = 4;
int count = 0;
int *nums = malloc(capacity * sizeof(int));
if (nums == NULL) { return 1; }

int value;
while (scanf("%d", &value) == 1) {
    if (count == capacity) {
        capacity *= 2;
        int *bigger = realloc(nums, capacity * sizeof(int));
        if (bigger == NULL) {
            free(nums);          // realloc failed - the old block is still valid, free it
            return 1;
        }
        nums = bigger;            // only overwrite nums once we know it worked
    }
    nums[count++] = value;
}
```

That `bigger` temporary is not paranoia, it's the whole point of the pattern. If `realloc` fails, it returns `NULL` and leaves the *original* block untouched. Writing `nums = realloc(nums, ...)` directly would overwrite `nums` with `NULL` on failure, and you'd have just leaked the original block - you no longer have any pointer to free it with. Always realloc into a fresh variable, check it, and only then assign it back.

## The four functions, side by side

| Function | What it does | Zeroed? | Common use |
|---|---|---|---|
| `malloc(n)` | allocate `n` bytes | no | fixed-size block, about to fill it yourself |
| `calloc(count, size)` | allocate `count * size` bytes | yes | array you need to start at zero |
| `realloc(ptr, n)` | resize `ptr`'s block to `n` bytes | new part only | growing a buffer as you go |
| `free(ptr)` | release the block back to the allocator | - | exactly once per successful allocation |

## Recap

1. **The heap** is a pool of memory the allocator hands out on request, not tied to any function's scope - that's what lets you size things at runtime instead of compile time.
2. **`malloc(n)`** returns `n` raw, uninitialized bytes as `void *`, or `NULL` on failure - always check for `NULL` before using the pointer.
3. **`calloc(count, size)`** is `malloc` plus zero-initialization; use it when the starting value matters.
4. **`realloc(ptr, n)`** resizes a block, possibly moving it - always assign the result to a temporary first so a failed realloc doesn't strand your only pointer to the original block.
5. **`free(ptr)`** returns memory to the allocator - once per allocation, never twice, and never touch the pointer afterward. Set it to `NULL` after freeing so accidental reuse fails loud instead of quiet.

The discipline in this phase - one `free` per allocation, check every return value, never touch freed memory - is exactly what Phase 14 (Undefined Behavior & Common Footguns) will name and dissect in full. Next, we go one level deeper: *why* the heap needs manual management at all, by putting it side by side with the stack, where cleanup happens automatically.

## Quick check

Test yourself on the discipline that keeps dynamic memory safe:

```quiz
[
  {
    "q": "You call `malloc(5 * sizeof(int))` and it succeeds. What must eventually happen to that pointer?",
    "choices": [
      "free() must be called on it exactly once",
      "Nothing - malloc'd memory frees itself when the program exits normally",
      "It should be set to NULL, which releases the memory automatically",
      "free() should be called each time you finish reading from the array"
    ],
    "answer": 0,
    "explain": "Every successful malloc (or calloc/realloc) owes exactly one free - the allocator has no way to know you're done unless you tell it, and setting a pointer to NULL doesn't release anything by itself."
  },
  {
    "q": "Why does the safe realloc pattern assign the result to a temporary (`int *bigger = realloc(nums, newSize);`) instead of writing `nums = realloc(nums, newSize);` directly?",
    "choices": [
      "If realloc fails it returns NULL, and overwriting nums directly would destroy the only pointer to the still-valid original block, leaking it",
      "Assigning directly to nums causes a compiler warning in C",
      "realloc runs slower when its result is assigned back to the same variable",
      "C doesn't allow reassigning a pointer that was returned by malloc"
    ],
    "answer": 0,
    "explain": "realloc leaves the original block untouched on failure, so the only way to lose it is to overwrite your one pointer to it - the temporary variable protects against exactly that."
  },
  {
    "q": "You need an array of ints that must start at zero. Why reach for calloc instead of malloc?",
    "choices": [
      "calloc guarantees every byte is zeroed; malloc hands back whatever bytes happened to already be there",
      "malloc already zero-initializes memory on modern systems, so calloc is just a slower alias",
      "calloc and malloc both guarantee zeroing - calloc is only preferred because it takes two arguments",
      "Neither guarantees zeroing; you must call memset() yourself either way"
    ],
    "answer": 0,
    "explain": "malloc gives you raw, uninitialized bytes - often leftover garbage from something else. calloc gives you the same kind of block but guarantees it starts at zero."
  }
]
```


---

# The Stack vs the Heap

You already know two ways to get memory in C. `int x = 5;` gets you a variable that just appears, and disappears when its block ends. `malloc(sizeof(int))` from [Phase 10: Dynamic Memory](10-dynamic-memory-malloc-and-free.md) gets you memory that sticks around until you `free` it. Those aren't two flavors of the same thing - they're memory coming from two entirely different regions of your program, managed by entirely different rules. Understanding *why* those regions exist, and what each one is actually good for, is what turns "my program crashed with a segfault" from a mystery into something you can predict before you even run the code.

## Two places a value can live

**What it actually is.** When your program runs, the memory it can use is split into regions. The two you'll touch constantly are the **stack** and the **heap**:

- The **stack** is where local variables and function-call bookkeeping live. It's a simple, fast, self-managing structure.
- The **heap** is a large, shared pool of memory that you request from and return to explicitly, using `malloc` and `free`.

**Why this exists.** These aren't arbitrary names - they describe *how* the memory is managed, and each name is literally the data structure it behaves like.

The stack behaves like a stack of plates: you can only add or remove from the top. Every time a function is called, a chunk of memory - a **stack frame** - is pushed on top, holding that function's local variables and its return address. When the function returns, its frame is popped off and that memory is instantly reclaimed. No bookkeeping, no searching for space - just move a pointer up or down. That's why stack memory is called **automatic**: the compiler manages its lifetime for you, tied exactly to scope.

The heap behaves like an open pool: you can ask for a chunk of any size, from anywhere, and give it back in any order. The allocator has to search for a free block, hand you its address, and remember it's yours until you say otherwise. That's why heap memory is called **dynamic** (or manual) - nothing frees it for you; *you* decide when its life ends.

## The stack: fast, automatic, and temporary

Every function call gets its own frame. Watch what that looks like:

```c
#include <stdio.h>

void third(void) {
    int c = 3;
    printf("third: c is at %p\n", (void *)&c);
}

void second(void) {
    int b = 2;
    printf("second: b is at %p\n", (void *)&b);
    third();
}

void first(void) {
    int a = 1;
    printf("first: a is at %p\n", (void *)&a);
    second();
}

int main(void) {
    first();
    return 0;
}
```
```console
$ ./frames
first: a is at 0x7ffd3a2c1b4c
second: b is at 0x7ffd3a2c1b2c
third: c is at 0x7ffd3a2c1b0c
```
*What just happened:* each address is a little *lower* than the last (on most systems the stack grows downward). Each function call pushed a new frame below the previous one, holding that function's own `a`, `b`, or `c`. When `third` returns, its frame is gone - `c`'s memory is immediately up for grabs again. You never called `free`; scope did the work.

💡 **Key point.** This is why a local variable's address is only meaningful while its function is still running. The moment the function returns, that stack space belongs to whatever gets called next, and the old value can be silently overwritten.

## Watching the stack overflow

The stack isn't infinite - a typical default is around 1-8 MB depending on your OS. Every frame eats into it, and if you push frames without ever popping any, you run out:

```c
#include <stdio.h>

void recurse(int depth) {
    char big[100000];      // eats ~100 KB of stack per call
    big[0] = depth;        // touch it so the compiler can't optimize it away
    printf("depth %d\n", depth);
    recurse(depth + 1);    // never returns, so frames never pop
}

int main(void) {
    recurse(0);
    return 0;
}
```
```console
$ ./blowup
depth 0
depth 1
...
depth 80
Segmentation fault (core dumped)
```
*What just happened:* `recurse` calls itself before it ever returns, so its frames pile up - each one 100 KB - until the stack region runs out of address space and the program crashes into memory it doesn't own. This is a **stack overflow**, and it's the direct, physical consequence of the stack's design: fast because it's a fixed region with no searching, but finite because that region has a hard edge.

⚠️ **Common footgun.** Deep or unbounded recursion, or a huge local array (`int buffer[10000000];`), are the two classic ways to blow the stack. If a value might be large or its size isn't known until runtime, that's a signal it belongs on the heap, not the stack.

## The classic bug: returning a pointer to a dead frame

This is the single most common pointer bug beginners write, and now you have the mental model to see exactly why it's wrong:

```c
#include <stdio.h>

int *make_number(void) {
    int n = 42;
    return &n;          // returning the address of a local variable
}

int main(void) {
    int *p = make_number();
    printf("%d\n", *p);  // undefined behavior
    return 0;
}
```
```console
$ gcc -Wall -o dangling dangling.c
dangling.c:5:12: warning: function returns address of local variable [-Wreturn-local-addr]
    5 |     return &n;
      |            ^
$ ./dangling
42          # ...or garbage, or a crash. It's undefined behavior.
```
*What just happened:* `n` lives in `make_number`'s stack frame. The function returns, its frame is popped, and `p` in `main` now holds the address of memory that no longer belongs to `n` - it's just an empty region waiting to be reused by the next function call. Reading through `p` is **undefined behavior** (more on this family of bugs in [Phase 14](14-undefined-behavior-and-common-footguns.md)): it might print `42` because nothing overwrote that spot yet, or it might print garbage, or crash. The compiler even warned you, in this case - always build with `-Wall`.

The fix is exactly the lesson of Phase 10: if a value needs to outlive the function that creates it, put it on the heap.

```c
int *make_number(void) {
    int *n = malloc(sizeof(int));
    *n = 42;
    return n;            // the heap block outlives this function - totally fine
}
```

## The heap: flexible, but you own the whole lifetime

The heap has no automatic scope. A `malloc`'d block lives exactly as long as you want it to - which is powerful, and also the entire source of the bugs from Phase 10 (leaks, double-frees, use-after-free). There's no free cleanup at the end of a block; *you* are the cleanup.

That flexibility buys you two things the stack structurally can't offer:

- **Size decided at runtime.** `malloc(n * sizeof(int))` works whether `n` is 10 or 10 million (well within available RAM); a stack array's size has to be known small and fixed, or the compiler-supported variable-length array trick, which still shares the stack's size limits.
- **Lifetime independent of any function.** A heap block can be created in one function, handed to another, stored in a data structure, and freed somewhere else entirely - useful for anything that needs to outlive the call that created it, like a linked list node or a buffer returned from a parser.

## Side by side

The same data, both ways, makes the tradeoff concrete:

```c
void stack_version(void) {
    int nums[100];              // allocated instantly, freed instantly
    // ... use nums ...
}                                // gone here, no code needed

void heap_version(void) {
    int *nums = malloc(100 * sizeof(int));   // one allocator call, can fail
    if (nums == NULL) return;                // must check
    // ... use nums ...
    free(nums);                              // must remember, or it leaks
}
```

`stack_version` is faster (allocation is a single instruction moving the stack pointer) and can never leak (the compiler guarantees the cleanup). `heap_version` costs more per call (the allocator does real bookkeeping) and puts the leak/double-free risk on you - but it's the only option once the size isn't known at compile time, the data needs to be huge, or the data needs to outlive the function.

## A mental map

| | Stack | Heap |
|---|---|---|
| Managed by | Compiler, automatically | You, manually (`malloc`/`free`) |
| Lifetime | Tied to scope (block/function) | Until you call `free` |
| Speed | Extremely fast (pointer move) | Slower (allocator does work) |
| Size | Small, fixed limit (MBs) | Large, limited by RAM |
| Size known at | Compile time | Can be decided at runtime |
| Failure mode | Stack overflow (crash) | `malloc` returns `NULL` (recoverable) |
| Common bug | Dangling pointer to a dead frame | Leak, double-free, use-after-free |

The rule of thumb that falls out of all this: **default to the stack** - it's free, fast, and impossible to leak. Reach for the heap only when you have a real reason: the data is large, its size isn't known until runtime, or it genuinely needs to outlive the function that creates it.

## Recap

1. The **stack** holds local variables and call bookkeeping, in **stack frames** pushed and popped automatically with every function call - fast, but finite, and its contents die the instant the function returns.
2. The **heap** is a pool you request from and return to explicitly with `malloc`/`free` - flexible size and lifetime, but you own the bookkeeping, and every bug from Phase 10 lives here.
3. Returning a pointer to a local (stack) variable is undefined behavior - the frame is gone before the caller ever reads it. If a value needs to outlive its function, allocate it on the heap instead.
4. Unbounded recursion or oversized local arrays blow the stack; oversized or dynamically-sized data belongs on the heap.
5. Default to the stack. Reach for the heap only when size, lifetime, or scale genuinely require it.

## Quick check

Test yourself on the idea that separates the two regions - who manages the lifetime, and what happens when each one runs out:

```quiz
[
  {
    "q": "You return `&n` from a function where `n` is a local `int`, and the caller prints `42` correctly anyway. What does that tell you?",
    "choices": [
      "The code is correct, since n clearly wasn't destroyed",
      "It's undefined behavior - the frame's memory just hasn't been overwritten yet, but nothing guarantees that",
      "The compiler silently moved n onto the heap because it was returned",
      "It only works because int is a small type"
    ],
    "answer": 1,
    "explain": "The frame is popped the moment the function returns, so that memory is free for reuse; reading through the dangling pointer is undefined behavior whether or not the old value happens to still be sitting there."
  },
  {
    "q": "Deep recursion crashes with a segfault, while a huge `malloc` request just returns `NULL`. Why the different failure modes?",
    "choices": [
      "The stack is a fixed-size region with no bounds check, so it runs straight into memory it doesn't own and crashes; the allocator can check the request against available memory first and fail gracefully",
      "malloc never fails, so this comparison doesn't apply",
      "Recursion is always a bug, so its failure mode doesn't matter",
      "Both are actually the same failure, just reported with different messages"
    ],
    "answer": 0,
    "explain": "The stack has no built-in size check - it just keeps pushing frames until it overruns its region. malloc, by contrast, can look at what's available and hand back NULL instead of crashing. (One wrinkle: Linux by default overcommits memory, so malloc may hand back a non-NULL pointer for more than really exists and let the OOM killer step in when you first touch it - but the return-NULL contract is still what you code against.)"
  },
  {
    "q": "You need an array sized by a number the user types in at runtime. What's the right call, and why?",
    "choices": [
      "Always use the stack - it's faster no matter what",
      "Use the heap - its size can be decided at runtime, while a stack array needs a size fixed (small) enough to stay well within the stack's limited space",
      "It doesn't matter, the two regions behave identically",
      "Use a global variable instead of either"
    ],
    "answer": 1,
    "explain": "A stack array's size has to be known and kept small at compile time (or risk a stack overflow), while the heap can be asked for exactly the size you need once you know it, up to available RAM."
  }
]
```


---

# Pointers II: Arithmetic, Double & Function Pointers

Back in Phase 5 you built the mental model for a pointer: a variable that holds an address. That model gets you through most C code. But three things still look like magic once you start reading real C: `ptr + 1` somehow "just works" no matter the type, `char **argv` shows up in every `main` signature you've ever seen, and libraries like `qsort` want you to hand them a *function* as an argument. This phase clears up all three, because they are really one idea applied three ways: **a pointer's type tells the compiler how to interpret what's at the address it holds** - whether that's "how many bytes to skip," "what kind of address is stored here," or "what kind of code lives here."

## Pointer arithmetic: the type does the math for you

**What it actually is.** When you add an integer to a pointer, C does not add that number of *bytes*. It adds that number of *elements*, where "element" means "however many bytes `sizeof` says this pointer's type takes up." `ptr + 1` means "the address of the next `T`," not "the next byte."

```c
#include <stdio.h>

int main(void) {
    int nums[4] = {10, 20, 30, 40};
    int *p = nums;              // decays to &nums[0]

    printf("%p -> %d\n", (void *)p, *p);
    printf("%p -> %d\n", (void *)(p + 1), *(p + 1));
    printf("%p -> %d\n", (void *)(p + 2), *(p + 2));

    return 0;
}
```
```console
$ gcc arith.c -o arith && ./arith
0x7ffee2a1c9a0 -> 10
0x7ffee2a1c9a4 -> 20
0x7ffee2a1c9a8 -> 30
```
Look at the addresses: they jump by 4, because `sizeof(int)` is 4 on this machine. If `p` were a `double *`, the same `p + 1` would jump by 8. The compiler already knows the type `p` points to, so it silently multiplies your `+ 1` by `sizeof(*p)`. You never write that multiplication yourself - that is the entire point of pointer arithmetic being type-aware instead of raw byte-counting.

**Why this exists.** It is what makes `nums[i]` and `*(nums + i)` the exact same operation. Array indexing in C is not a special feature - `nums[i]` is *defined* as `*(nums + i)`. When you write `nums[2]`, the compiler computes "start address, plus 2 elements' worth of bytes, then dereference." Understanding that array indexing is just pointer arithmetic in a nicer costume is the payoff of this whole section: once you see it, arrays and pointers stop being two topics and become one.

⚠️ **The trap: walking off the end.** Pointer arithmetic has no bounds checking. `p + 100` on a 4-element array computes a real address - some byte in memory you don't own - and the compiler will not stop you. Reading or writing through it is undefined behavior (more on exactly what that means in Phase 14). The array itself does not know its own length; *you* have to track it, usually by also carrying a count or a sentinel value like the `'\0'` that terminates a string.

You can also walk pointers with `++` and `--`, which is how idiomatic C often loops over an array without ever writing an index variable:

```c
void print_all(int *p, int count) {
    int *end = p + count;       // one-past-the-end pointer, valid to compute (not to deref)
    while (p != end) {
        printf("%d ", *p);
        p++;
    }
    printf("\n");
}
```

That `end` pointer is a deliberate C idiom: "one past the last valid element" is a legal address to *compute and compare against*, you just may never dereference it. It marks the stopping line without needing a separate index variable.

## Double pointers: a pointer to a pointer

**What it actually is.** A pointer holds the address of *something*. That something can itself be a pointer. `int **pp` is "the address of an `int *`." Nothing new is happening here beyond what you already know - it is the same "address of a box" idea, just with the box holding another box instead of an `int`.

The question that actually matters is: **why would you ever want that?** Two real answers.

**Reason 1: letting a function change a caller's pointer.** You already know that to let a function modify a caller's `int`, you pass `int *`. The same rule applies one level up: to let a function modify a caller's *pointer* (make it point somewhere new), you pass a pointer to that pointer.

```c
#include <stdlib.h>

void allocate(int **out, int value) {
    *out = malloc(sizeof(int));   // change what the CALLER's pointer points to
    **out = value;                // then set the value through it
}

int main(void) {
    int *p = NULL;
    allocate(&p, 42);             // pass the address of p itself
    printf("%d\n", *p);           // 42
    free(p);
    return 0;
}
```
```console
$ gcc dptr.c -o dptr && ./dptr
42
```
*What just happened:* if `allocate` took a plain `int *out`, it would only ever change its own local copy of the pointer - the caller's `p` would still be `NULL` when the function returned, same as the pass-by-value problem from Phase 4. Passing `&p` (an `int **`) gives `allocate` the address of the pointer variable itself, so `*out = ...` reaches back into `main` and rewrites `p`. This exact pattern is why functions that hand you back a freshly allocated pointer, like some parsing or "create" functions in real libraries, take a `T **` parameter.

**Reason 2: arrays of strings.** A `char *` is a string. An array of strings is naturally an array of `char *` - and an array, in a function signature, decays to a pointer to its first element. So "an array of strings" becomes `char **`. This is exactly `argv` in `int main(int argc, char **argv)`: `argv` points at the first element of an array of `char *`, and `argv[i]` is the i-th string.

```c
void print_args(int argc, char **argv) {
    for (int i = 0; i < argc; i++) {
        printf("arg %d: %s\n", i, argv[i]);   // argv[i] is a char*, %s prints the string it points to
    }
}
```

The layering is worth saying out loud once: `argv` is a pointer to a `char *`. `argv[i]` dereferences one level to get a `char *` (one string's address). `argv[i][j]` dereferences again to get a single `char`. Each `[ ]` peels off one layer of pointer.

## Function pointers: storing "code" in a variable

**What it actually is.** Everything in a running program lives at an address, including compiled functions. A function pointer is a variable that holds the address of a function, so you can pass a function around like any other value - store it in a variable, put it in a struct, hand it to another function to call later.

The syntax is the ugliest part of C, so read it slowly once and it stops being scary:

```c
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }

int main(void) {
    int (*op)(int, int);   // op: pointer to a function taking (int, int), returning int

    op = add;
    printf("%d\n", op(3, 4));   // 7

    op = sub;
    printf("%d\n", op(3, 4));   // -1

    return 0;
}
```
```console
$ gcc fptr.c -o fptr && ./fptr
7
-1
```
*Reading the declaration:* `int (*op)(int, int)` - the parentheses around `*op` are load-bearing. Without them, `int *op(int, int)` would mean "a function named `op` that returns `int *`," a completely different thing. With them, it means "`op` is a pointer, and what it points to is a function of type `(int, int) -> int`." Assigning `op = add` does not call `add` - a bare function name decays to its address, the same way an array name decays to a pointer to its first element.

**Why this matters: callbacks.** The standard library's sort function, `qsort`, has no idea how *your* data should be ordered - so it takes a function pointer and calls it whenever it needs to compare two elements:

```c
#include <stdlib.h>

int compare_ints(const void *a, const void *b) {
    int x = *(const int *)a;
    int y = *(const int *)b;
    return (x > y) - (x < y);  // negative, zero, positive - qsort's contract
}

int main(void) {
    int nums[] = {5, 3, 8, 1};
    qsort(nums, 4, sizeof(int), compare_ints);   // pass the function itself
    for (int i = 0; i < 4; i++) printf("%d ", nums[i]);
    printf("\n");
    return 0;
}
```
```console
$ gcc sort.c -o sort && ./sort
1 3 5 8
```
`qsort` is generic over any data because it never looks inside your elements itself - it just calls whatever comparison function you handed it, over and over, and trusts the return value. This is the same idea behind every callback API you'll meet later: signal handlers, GUI event handlers, thread start routines. They all boil down to "here is an address of code, call it when X happens."

A table of function pointers (a "dispatch table") is the C way of doing what other languages call polymorphism - an array where `table[opcode]` picks which function runs, instead of a chain of `if`/`else if`:

```c
int (*ops[])(int, int) = {add, sub};   // ops[0] == add, ops[1] == sub
printf("%d\n", ops[1](10, 4));         // calls sub(10, 4) -> 6
```

## Recap

- Pointer arithmetic is scaled by the pointee's `sizeof` automatically - `p + 1` means "next element," not "next byte." This is *why* `arr[i]` and `*(arr + i)` are the same expression.
- Pointer arithmetic has no bounds checking; tracking valid range is your job, not the compiler's.
- A double pointer (`T **`) is a pointer to a pointer. Use one when a function needs to modify the caller's *pointer variable*, or when representing an array of pointers (like `char **argv`, an array of strings).
- A function pointer stores a function's address so it can be assigned, stored, and called through a variable. The declaration syntax `T (*name)(args)` needs those parentheses to mean "pointer to function" rather than "function returning pointer."
- Callbacks (`qsort`) and dispatch tables are the two big real-world uses: let a generic function call code it doesn't know about yet.

### Check yourself

```quiz
[
  {
    "q": "Given `double *p`, what does `p + 1` compute?",
    "choices": [
      "The address one byte after `p`",
      "The address `sizeof(double)` (8, typically) bytes after `p`",
      "The value at `p`, incremented by 1",
      "A compile error, since only `int *` supports arithmetic"
    ],
    "answer": 1,
    "explain": "Pointer arithmetic is scaled by the pointee's sizeof, so `p + 1` always means 'the next element,' not 'the next byte' - that's true for any pointer type, not just int."
  },
  {
    "q": "Why does `allocate(int **out, int value)` take an `int **` instead of an `int *`?",
    "choices": [
      "`int **` is required whenever a function allocates memory",
      "So the function can reach back into the caller and change what the caller's own pointer variable points to",
      "It lets the function store two separate integers instead of one",
      "It makes the pointer arithmetic inside the function scale by 8 instead of 4"
    ],
    "answer": 1,
    "explain": "An `int *` argument only lets a function change its own local copy of the pointer; to change which address the caller's pointer variable holds, the function needs the address of that variable itself, which is an `int **`."
  },
  {
    "q": "Why do the parentheses matter in `int (*op)(int, int)`?",
    "choices": [
      "They are just a style convention and can be dropped safely",
      "Without them, `int *op(int, int)` declares a function named `op` returning `int *`, a completely different type",
      "They tell the compiler to allocate `op` on the heap instead of the stack",
      "They mark `op` as a pointer to an array of two ints"
    ],
    "answer": 1,
    "explain": "`*` binds to `op` first only because of the parentheses, making `op` a pointer to a function; drop them and `*` binds to `int` instead, declaring a function that returns a pointer."
  }
]
```


---

# The Standard Library Essentials

## The mental model: C gives you almost nothing, on purpose

If you came from Python or JavaScript, this phase might feel thin. Those languages ship with string
formatting, regex, JSON parsing, and HTTP clients built in. C ships with none of that. What C gives you
is a small set of header files - `<string.h>`, `<stdlib.h>`, `<ctype.h>`, `<math.h>`, and a few others -
that wrap the handful of operations that are either impossible to write correctly yourself (safe memory
copying with overlap handling) or so common that everyone would otherwise reinvent them slightly
differently (checking if a character is a digit).

This is not an oversight. C was designed in the 1970s to be the language you use to build everything
else - operating systems, other languages' runtimes, embedded firmware. A big stdlib means big
assumptions about what your program needs (a heap, a filesystem, threads), and those assumptions don't
hold on a microcontroller with 2KB of RAM. So the standard library stays small, and the header files you
already know the shape of - `#include`, declare-in-header-define-elsewhere, from [Phase
8](08-header-files-and-the-preprocessor.md) - are exactly how it's delivered: they're just headers
declaring functions that live in a library called **libc**, which your linker attaches automatically.

You already met part of the standard library without calling it that: `printf` and `scanf` from
`<stdio.h>` in [Phase 1](01-install-compiling-and-your-first-program.md), and `malloc`/`free` from `<stdlib.h>` in
[Phase 10](10-dynamic-memory-malloc-and-free.md). This phase fills in the rest of what you'll reach for
in nearly every real program: working with strings, classifying characters, converting between text and
numbers, and basic math.

## `<string.h>`: because arrays don't know their own length

Recall from [Phase 6](06-arrays-and-strings.md) that a C string is just a `char` array ending in `'\0'`,
and the array itself carries no length information. Every operation you'd want to do on a string -
measure it, copy it, compare it, search it - has to walk the bytes looking for that terminator. `<string.h>`
is a set of functions that do that walking correctly, so you don't write a subtly-wrong version yourself
every time.

```c
#include <stdio.h>
#include <string.h>

int main(void) {
    char src[] = "hello";
    char dst[20];

    printf("length: %zu\n", strlen(src));       // 5 (not counting '\0')

    strcpy(dst, src);                            // copies including '\0'
    printf("copied: %s\n", dst);

    strcat(dst, ", world");                      // appends onto dst
    printf("joined: %s\n", dst);

    printf("equal: %d\n", strcmp(dst, src) == 0); // 0 -> not equal (they differ)

    char *found = strstr(dst, "world");
    printf("found at: %s\n", found ? found : "(not found)");

    return 0;
}
```
```console
$ ./a.out
length: 5
copied: hello
joined: hello, world
equal: 0
found at: world
```

`strlen` returns `size_t` (an unsigned type sized to hold any array length on your platform), which is
why the format specifier is `%zu`, not `%d`. `strcmp` returns `0` for equal strings and a nonzero value
otherwise, which trips people up constantly, since it *reads* like a boolean but works backwards from one.

**The danger you need to know about.** `strcpy` and `strcat` do not check whether `dst` is big enough.
If `src` is longer than the destination buffer, they'll write past the end of it, which is undefined
behavior ([Phase 14](14-undefined-behavior-and-common-footguns.md) covers exactly why that's catastrophic,
not just wrong). The fix is the `n`-suffixed versions that take an explicit size limit:

```c
char dst[8];
strncpy(dst, "this string is way too long", sizeof(dst) - 1);
dst[sizeof(dst) - 1] = '\0';   // strncpy doesn't guarantee a terminator - add it yourself
```

That last line matters: unlike `strcpy`, `strncpy` will *not* null-terminate `dst` if the source doesn't
fit, so you always add the terminator by hand after calling it. This is the kind of sharp edge that makes
people write their own string-handling helpers on top of `<string.h>` rather than calling it raw every
time - a completely reasonable thing to do once you understand what's underneath.

Three more you'll use constantly for raw memory rather than text: `memcpy(dst, src, n)` copies `n` bytes
(faster than `strcpy` when you already know the length and the data isn't necessarily text), `memset(ptr,
value, n)` fills `n` bytes with a byte value (handy for zeroing a buffer: `memset(buf, 0, sizeof(buf))`),
and `memmove(dst, src, n)` is `memcpy`'s safe sibling when the source and destination might overlap.

## `<ctype.h>`: classifying and converting characters

Every "is this character a letter" check you'd otherwise hand-roll with comparisons is here, and it's
worth using these instead of writing your own, because the raw comparisons are easy to get subtly wrong
across locales and character sets:

```c
#include <ctype.h>
#include <stdio.h>

int main(void) {
    char c = 'A';
    printf("isalpha: %d\n", isalpha(c));   // nonzero (true)
    printf("isdigit: %d\n", isdigit(c));   // 0 (false)
    printf("lower: %c\n", tolower(c));     // a
    return 0;
}
```

The common ones: `isalpha`, `isdigit`, `isalnum`, `isspace`, `isupper`/`islower`, and the converters
`toupper`/`tolower`. They all take an `int` and return nonzero for true, `0` for false, matching C's
"no real boolean" convention from [Phase 2](02-syntax-variables-and-types.md). There's a subtle rule
about *how* you feed them a character: on platforms where `char` is signed, a byte with the high bit set
(anything past plain ASCII) becomes a negative `int`, and passing a negative value that isn't `EOF` is
undefined behavior. The safe habit is to cast through `unsigned char` first, e.g.
`isalpha((unsigned char)c)`. Plain ASCII like `'A'` is always fine, so the demo above doesn't need it.

## `<stdlib.h>`: conversions, sorting, and exiting

You've already used `malloc`/`free` from here. Three more essentials:

**Text-to-number conversion.** `atoi("42")` gives you `42` as an `int`, fast and simple - but it has no
way to tell you the string wasn't a valid number; garbage input silently becomes `0`. `strtol` (string to
long) is the version you should reach for when the input might be wrong, because it reports failure:

```c
#include <stdlib.h>
#include <stdio.h>

int main(void) {
    char *end;
    long n = strtol("42abc", &end, 10);   // base 10
    printf("parsed: %ld, stopped at: \"%s\"\n", n, end);
    return 0;
}
```
```console
$ ./a.out
parsed: 42, stopped at: "abc"
```

`end` points at the first character `strtol` couldn't parse. If `end` points at the same place `str`
does, nothing was parsed at all - that's your "this wasn't a number" signal that `atoi` can't give you.

**Sorting anything.** `qsort` sorts an array of *any* type by taking a comparison function you write,
since C has no generics to write one sort that works for every type:

```c
int compare_ints(const void *a, const void *b) {
    int x = *(const int *)a;
    int y = *(const int *)b;
    return (x > y) - (x < y);   // negative, zero, or positive
}

int nums[] = {5, 2, 8, 1};
qsort(nums, 4, sizeof(int), compare_ints);
// nums is now {1, 2, 5, 8}
```

The `void *` parameters are why [Phase 5](05-pointers-i-the-mental-model.md)'s pointer mental model
matters here: `qsort` doesn't know or care what type it's sorting, so it hands your comparator raw
addresses, and you cast them back to the real type before comparing. (You'll often see the shorter
`return x - y;` in textbooks. It works for small values but can overflow for large or mixed-sign
`int`s, which is undefined behavior - `(x > y) - (x < y)` is the same idea without the trap.)

**Leaving the program early.** `exit(0)` terminates the program immediately from anywhere in the call
tree, running any registered cleanup and flushing open buffered files first. A plain `return` only hands
control back to the calling function - it ends the whole program only when you're already in `main`, where
`return n` behaves just like `exit(n)` (same cleanup, same flushing).

## `<math.h>`: the floating-point functions

`sqrt`, `pow`, `floor`, `ceil`, `fabs`, and the trig functions (`sin`, `cos`, `tan`) all live here and
operate on `double`. One platform gotcha worth knowing: on Linux/GCC, math functions live in a separate
library, so you compile with `-lm` (`gcc prog.c -lm -o prog`) or the linker won't find them - a good
early exposure to the idea that "in the standard" and "linked by default" aren't always the same thing.

## Why this phase matters more than it looks

The real skill here isn't memorizing function signatures - it's the reflex to check `<string.h>` or
`<stdlib.h>` *before* writing your own string-length loop or your own number parser. C's standard library
is small enough to actually learn, and every function in it exists because someone already hit the edge
cases you'd hit writing it yourself. Reach for it first.

## Quick check

Test yourself on the sharp edges that trip people up most:

```quiz
[
  {
    "q": "You use `strcpy(dst, src)` where `src` is longer than `dst`'s buffer. What happens?",
    "choices": [
      "strcpy detects the overflow and truncates src to fit",
      "It writes past the end of dst, which is undefined behavior",
      "The compiler rejects the code at build time",
      "src is silently split across two calls"
    ],
    "answer": 1,
    "explain": "strcpy never checks the destination's size, so overflow just writes past the buffer's end; use strncpy plus a manual terminator when the input might not fit."
  },
  {
    "q": "`strcmp(a, b)` returns `0`. What does that tell you?",
    "choices": [
      "a and b are different strings",
      "a and b are equal",
      "The comparison failed",
      "a is empty"
    ],
    "answer": 1,
    "explain": "strcmp returns 0 for equal strings and nonzero otherwise, which reads like a boolean but works backwards from one."
  },
  {
    "q": "Why is `strtol` a better choice than `atoi` when the input might not be a valid number?",
    "choices": [
      "atoi is slower, so it skips validation to save time",
      "atoi has no way to report where parsing stopped or failed; strtol writes that position into the `end` pointer you pass it",
      "atoi only works on negative numbers",
      "strtol re-checks the string twice for accuracy"
    ],
    "answer": 1,
    "explain": "atoi just returns 0 on anything unparseable, indistinguishable from an actual 0; strtol's end pointer tells you exactly where it stopped, so you can tell 'partially parsed' from 'totally invalid' from 'clean number'."
  }
]
```


---

# Undefined Behavior & Common Footguns

Here's a sentence that should worry you a little: a C program can compile cleanly, run correctly a thousand times in a row, and still be completely broken. Not "broken in an edge case you haven't tested" - broken in a way the language itself refuses to define the meaning of. This phase is about that gap, because it's the single biggest thing separating someone who writes C from someone who understands it.

## What "undefined behavior" actually is

**What it actually is.** The C standard describes what correct programs must do. For a large set of situations - reading an uninitialized variable, indexing past the end of an array, signed integer overflow, dereferencing a null or freed pointer - the standard says nothing at all about what happens. Not "it crashes." Not "it returns garbage." It says: *the compiler may assume this never happens, and if it does happen anyway, literally anything is permitted as the result.* That's what "undefined behavior" (UB) means: not "undefined" like "unspecified detail," but "outside the contract entirely."

**Why this exists.** C was designed to be fast, and fast means the compiler doesn't insert runtime checks you didn't ask for. Instead of the language guaranteeing "out-of-bounds access throws an exception" (which costs a check on every access), C says "don't do that - and because you promised not to, I won't spend any instructions checking for it." The compiler takes your promise at face value and optimizes as if UB is impossible. That's the trade C makes for speed: zero-cost as long as you hold up your end.

**Why this bites people.** "Undefined" doesn't mean "predictably bad." It can mean the code works fine today, works fine on your laptop, and then a compiler upgrade or a new optimization level makes it print nonsense or delete a security check you were relying on. UB isn't a runtime event you can catch - it's a broken promise the compiler was never watching for, because you told it (by writing valid-looking C) that you wouldn't break it.

## The classic list, with real examples

### 1. Reading an uninitialized variable

```c
#include <stdio.h>

int main(void) {
    int x;              // no initializer - x holds garbage
    if (x > 0) {         // reading x here is UB
        printf("positive\n");
    } else {
        printf("not positive\n");
    }
    return 0;
}
```
Locals aren't zeroed for you (Phase 2 covered this - unlike globals, which *are* zeroed). `x` is whatever bit pattern happened to be sitting on the stack. Reading it before you write to it is UB, not "reads zero" or "reads garbage" - the compiler is allowed to assume it never happens and reason accordingly, which can produce output that looks impossible to explain from the source alone.

### 2. Out-of-bounds array access

```c
int scores[5] = {90, 85, 77, 60, 95};
printf("%d\n", scores[5]);   // valid indices are 0..4 - this is UB
```
There's no bounds check in C (Phase 6). `scores[5]` reads whatever memory happens to sit right after the array - maybe another variable, maybe unmapped memory that segfaults, maybe nothing visibly wrong at all. All three are "correct" outcomes of UB.

### 3. Signed integer overflow

```c
#include <limits.h>
#include <stdio.h>

int main(void) {
    int x = INT_MAX;
    int y = x + 1;        // signed overflow - UB, NOT wraparound
    printf("%d\n", y);
    return 0;
}
```
This one surprises people the most. Unsigned overflow *is* defined (it wraps, Phase 2) - but signed overflow is UB. Compilers exploit this aggressively. A real, well-documented case: a security check like `if (x + 1 < x)` (meant to detect overflow) can be silently deleted by the optimizer, because the compiler is allowed to assume `x + 1 < x` is *never* true for a signed `int` - overflow "can't happen," so the whole branch is dead code from its point of view. The check you wrote to catch overflow gets erased *because* it relied on overflow occurring.

### 4. Use-after-free and dangling pointers

```c
#include <stdlib.h>
#include <stdio.h>

int main(void) {
    int *p = malloc(sizeof(int));
    *p = 42;
    free(p);
    printf("%d\n", *p);   // p is dangling - UB
    return 0;
}
```
`free(p)` (Phase 10) doesn't erase memory or nullify `p` - it just tells the allocator "this block is available again." `*p` after that reads memory that might already belong to something else, or might still look right *this run* and wrong the next. This is the same category of bug as the stack-vs-heap dangling pointer from Phase 11, just reached via `free` instead of a returned local's address.

### 5. Buffer overflows (writing, not just reading)

```c
#include <string.h>

char name[8];
strcpy(name, "This string is way too long");  // overflows name - UB
```
`strcpy` (Phase 13) has no idea how big `name` is - it copies until it hits `'\0'`, writing straight past the buffer's end. This overwrites whatever memory comes next: other variables, a saved return address, anything. This exact mistake is the root cause of a large fraction of historical security vulnerabilities. The fix is `snprintf` (or `strncpy`) with an explicit size, or better, tracking buffer capacity everywhere you write. One catch: `strncpy` does not guarantee a terminating `'\0'` - if the source is as long as the size you pass, it fills the buffer and leaves it unterminated, which just turns a write overflow into a later out-of-bounds read. That's why `snprintf`, which always terminates, is the safer default.

### 6. Double free and freeing unowned memory

```c
int *p = malloc(sizeof(int));
free(p);
free(p);        // double free - UB, often corrupts the allocator itself
```
The allocator keeps its own bookkeeping in memory near your blocks. Freeing the same pointer twice can corrupt that bookkeeping, making a *later, unrelated* `malloc` crash - which makes this bug notoriously hard to trace back to its real cause.

## Why "it worked when I ran it" proves nothing

UB is not "the program crashes." Crashing would be easy - you'd see it immediately. The dangerous cases are the ones that *look* fine: the garbage value in an uninitialized variable happens to be zero this run, the freed memory hasn't been reused yet, the out-of-bounds read lands on padding nobody cares about. Change the compiler, the optimization level, the surrounding code, or the platform, and the same source can start doing something different - not because anything "changed," but because you were never guaranteed the old behavior in the first place. Treat "it ran correctly" as *no evidence at all* that a program without UB is UB-free.

## Catching UB before it catches you

You can't spot most of this by reading code carefully forever - use tools built for exactly this:

- **Compile with warnings on:** `gcc -Wall -Wextra -Wpedantic` catches a surprising amount (uninitialized reads, type mismatches, format string errors) at compile time, for free.
- **AddressSanitizer:** `gcc -fsanitize=address -g prog.c -o prog` instruments the binary to catch buffer overflows, use-after-free, and double-free *the instant they happen*, with a stack trace pointing at the exact line - instead of silently corrupting memory and crashing somewhere unrelated later.
- **UndefinedBehaviorSanitizer:** `gcc -fsanitize=undefined -g prog.c -o prog` catches signed overflow, null dereferences, and misaligned access at the moment they occur.
- **Valgrind** (Phase 9): catches uninitialized reads and memory leaks by running your program under a full memory-tracking emulator - slower, but thorough.

Run your test suite under `-fsanitize=address,undefined` regularly. It turns "undefined behavior" from a ghost that appears months later into a normal, debuggable crash with a line number.

## Recap

1. **Undefined behavior means "outside the language's contract."** The compiler is allowed to assume it never happens and optimize on that assumption - it does not mean "predictably bad" or "crashes."
2. C skips runtime checks (bounds, initialization, overflow) for speed, on the promise that *you* keep code within the rules. UB is what happens when that promise breaks.
3. The classics: uninitialized reads, out-of-bounds access, signed integer overflow, use-after-free, buffer overflows, double free. Every one of them can "work" by accident and fail unpredictably later.
4. "It ran fine" is not proof of correctness. Compile with `-Wall -Wextra` always, and run tests under `-fsanitize=address,undefined` (or Valgrind) to catch UB the moment it happens instead of months later.

## Quick check

Test yourself on the idea that makes UB dangerous - that it's not "predictably bad," it's outside the language's contract entirely:

```quiz
[
  {
    "q": "A program with undefined behavior runs correctly every time you test it. What does that prove?",
    "choices": [
      "Nothing - UB can still misbehave under a different compiler, flag, or platform",
      "The UB is harmless and won't cause problems later",
      "The compiler already checked for UB and found none",
      "The code no longer contains UB, since it ran correctly"
    ],
    "answer": 0,
    "explain": "The standard makes no guarantee for UB, so a clean run today is not evidence of anything - the same source can behave differently the moment the compiler, optimization level, or surrounding code changes."
  },
  {
    "q": "Why can a compiler legally delete a security check like `if (x + 1 < x)` that's meant to catch signed integer overflow?",
    "choices": [
      "Signed overflow is UB, so the compiler is allowed to assume `x + 1 < x` is never true and treat the branch as dead code",
      "The compiler detects the bug and 'fixes' the code for you",
      "`if` statements comparing the same variable are always removed as an optimization",
      "It only happens with `-O0`, so raising optimization level would prevent it"
    ],
    "answer": 0,
    "explain": "Unsigned overflow wraps and is defined, but signed overflow is UB - so the compiler can assume it never happens, which means a check that only triggers via overflow can be optimized away entirely."
  },
  {
    "q": "After `free(p)`, what has actually happened to `p` and the memory it pointed to?",
    "choices": [
      "The memory is marked available for reuse, but `p` still holds the old address and the memory itself isn't erased or zeroed",
      "The memory is immediately zeroed out and `p` is set to NULL",
      "`p` becomes a compile error the next time it's used",
      "The memory is safe to read from but not write to"
    ],
    "answer": 0,
    "explain": "`free` only tells the allocator the block is available again - it doesn't nullify the pointer or clear the memory, which is exactly why a dangling `*p` can look fine one run and read garbage the next."
  }
]
```


---

# Where to Go Next

Look at what you actually did. You started at "install a compiler" and ended at understanding *why* a dangling pointer is dangerous, *why* the stack is fast and the heap isn't free, and *why* the compiler is allowed to assume your program never triggers undefined behavior. That's not a beginner's slice of C. Most people who "know C" stop well before [Phase 11](11-the-stack-vs-the-heap.md) and [Phase 14](14-undefined-behavior-and-common-footguns.md) - you didn't.

This phase is short on purpose. You don't need another concept dumped on you. You need to know where this knowledge actually gets used, which tools turn "I think this is a memory bug" into "here's the exact line," and what to build so none of it fades.

## Where C actually shines

C didn't get old and irrelevant. It became the floor everything else stands on.

**Operating systems and kernels.** Linux, Windows' core, macOS's XNU kernel, every embedded RTOS - written in C, because an OS needs to talk to hardware directly with no runtime, no garbage collector, and no surprises about what a line of code actually does. The mental model from [Phase 11](11-the-stack-vs-the-heap.md) - you own memory, you know exactly where it lives - is the whole reason C can do this and a garbage-collected language can't.

**Embedded systems and firmware.** Microcontrollers running a thermostat, a car's engine controller, a pacemaker - these have kilobytes of RAM, no operating system underneath them, and zero tolerance for a garbage collector pausing mid-task. C's "no hidden costs" promise from [Phase 1](01-install-compiling-and-your-first-program.md) is exactly the deal embedded work needs.

**Language runtimes and interpreters.** CPython, the reference Lua interpreter, and large parts of Node's V8 engine are C or C++. When you build a language, you need to manage memory by hand and reason precisely about the machine underneath - which is the entire second half of this guide.

**Databases and performance-critical libraries.** SQLite, Redis, and the guts of PostgreSQL are C. When milliseconds and bytes matter at scale, C's directness (no hidden allocations, no hidden indirection) wins.

```mermaid
flowchart LR
  C(C) --> OS[Operating systems<br/>Linux, kernels, RTOS]
  C --> EMB[Embedded & firmware<br/>microcontrollers]
  C --> LANG[Language runtimes<br/>CPython, Lua]
  C --> DB[Databases & perf libs<br/>SQLite, Redis]
```

You are very unlikely to write a web app's UI in C in 2026. You're quite likely to depend, right now, on ten programs written in it without knowing it.

## The tools that turn theory into instinct

[Phase 9](09-build-tooling-makefiles-and-debugging.md) gave you a debugger. Two more tools are worth learning next, because they catch exactly the bugs [Phase 14](14-undefined-behavior-and-common-footguns.md) warned you about, automatically, every time you run your program.

**AddressSanitizer and UndefinedBehaviorSanitizer.** Compile with `-fsanitize=address,undefined` and your program gets a runtime bodyguard. A use-after-free or a buffer overrun aborts on the spot with a precise report - file, line, and what went wrong - instead of silently corrupting memory three functions later. A signed-integer overflow gets the same precise report, though by default UBSan prints it and keeps running (add `-fno-sanitize-recover=all` if you want those to abort too).

```c
// bug.c - one element past the end of the array
#include <stdio.h>
int main(void) {
    int arr[5] = {1, 2, 3, 4, 5};
    printf("%d\n", arr[5]);   // out of bounds - UB
    return 0;
}
```

```console
$ gcc -fsanitize=address -g bug.c -o bug
$ ./bug
==12345==ERROR: AddressSanitizer: stack-buffer-overflow on address ...
    #0 in main bug.c:5
```

Without the sanitizer this might print garbage, print `5`, or crash somewhere unrelated. With it, the bug names itself. Turn these flags on for every project you write from here forward - the cost is a slower binary, the payoff is bugs that used to take an afternoon now take a second.

**Valgrind.** Where sanitizers watch your program as it runs, Valgrind's `memcheck` tool runs your compiled binary in a simulator and catches leaked `malloc`s ([Phase 10](10-dynamic-memory-malloc-and-free.md)), reads of uninitialized memory, and invalid frees, with a full report of exactly which allocation leaked and where it was made.

## What to build next

Pick one and finish it. A small project you complete teaches you more than an ambitious one you abandon halfway through fighting the linker.

- **A dynamic array (a "vector") from scratch.** Implement `push`, `get`, and automatic growth via `realloc`. This forces you to actually use everything from [Phase 10](10-dynamic-memory-malloc-and-free.md) and [Phase 12](12-pointers-ii-arithmetic-double-and-function-point.md) in one place, and it's the exact data structure most higher-level languages hide from you.
- **A simple key-value store.** A hash table backed by linked lists for collisions, built on structs ([Phase 7](07-structs-and-typedef.md)) and pointers. It's small enough to finish in a weekend and touches nearly every idea in this guide.
- **A text adventure or a tiny shell.** Something that reads input, branches on it, and manages some state - good practice for [Phase 6](06-arrays-and-strings.md)'s string handling and [Phase 3](03-control-flow.md)'s control flow, without a memory-management deep end.

For each one: build it with a `Makefile` ([Phase 9](09-build-tooling-makefiles-and-debugging.md)), compile with `-Wall -Wextra -fsanitize=address,undefined`, and don't consider it done until it runs clean.

## Where C leads from here

If you want more safety without giving up C's directness, **C++** adds classes, RAII (automatic cleanup tied to scope, a distant cousin of what you saw in [Phase 11](11-the-stack-vs-the-heap.md)), and a much bigger standard library, while staying close enough to C that everything you just learned still applies. If you want a language that takes the memory rules you learned to respect in [Phase 14](14-undefined-behavior-and-common-footguns.md) and enforces them *at compile time* instead of trusting you to remember them, that's [Rust From Zero](/guides/rust-from-zero) - its whole "ownership" system exists to catch the exact class of bug this guide just taught you to fear.

For deeper reading, two resources are worth owning: **"The C Programming Language"** by Kernighan and Ritchie (the language's own creators, and still the clearest short book on it), and **Beej's Guide to C**, a free, precise, no-fluff reference that reads like a friendlier version of the manual pages.

## Recap

1. **Where C lives** - operating systems and kernels, embedded firmware, language runtimes, and the databases and libraries everything else is built on.
2. **Sanitizers** (`-fsanitize=address,undefined`) turn silent memory corruption into an immediate, precise crash report - turn them on for everything you write from here.
3. **Valgrind's memcheck** catches leaks and invalid memory access by running your binary in a simulator.
4. **Build one real thing** - a dynamic array, a key-value store, or a small interpreter/shell - and finish it with warnings and sanitizers on.
5. **Next languages** - C++ for more structure without leaving C behind, or [Rust](/guides/rust-from-zero) if you want the compiler to enforce the memory rules you now understand by hand.

You came in not knowing what a pointer was. You're leaving able to explain why `free`ing something twice corrupts memory, why the stack is fast, and why undefined behavior is the compiler's silent permission to assume you didn't make a mistake. That's the real skill. Go build the small thing.

## Quick check

Test yourself on the ideas that matter most for what comes after this guide:

```quiz
[
  {
    "q": "You compile with `-fsanitize=address,undefined`. What does that actually get you?",
    "choices": [
      "A runtime check that catches a memory error or undefined behavior the moment it happens and reports it with a precise file and line",
      "A static analysis pass that rewrites your code to remove undefined behavior automatically",
      "A compile-time guarantee that the program can never crash",
      "A checker that only catches memory leaks, not out-of-bounds access"
    ],
    "answer": 0,
    "explain": "Sanitizers instrument your binary so the bug is caught loudly and precisely at the moment it happens, instead of quietly corrupting memory somewhere else. A memory error aborts on the spot; UBSan reports the exact line and, by default, keeps going (use `-fno-sanitize-recover` to abort on those too). Either way they don't fix anything or guarantee correctness up front."
  },
  {
    "q": "How does Valgrind's memcheck differ from AddressSanitizer?",
    "choices": [
      "Valgrind runs your already-compiled binary inside a simulator, with no special compile flags needed, while sanitizers require compiling with `-fsanitize` to build the checks into the binary itself",
      "Valgrind only runs on Windows, sanitizers only run on Linux",
      "They do the exact same thing, so you only ever need to learn one",
      "Sanitizers work on C++ only, never on plain C"
    ],
    "answer": 0,
    "explain": "The two catch overlapping bugs but work differently: Valgrind simulates your existing binary after the fact, sanitizers bake the checks in at compile time - which is also why sanitized binaries run faster than a Valgrind session."
  },
  {
    "q": "Why does an operating system kernel get written in C instead of a garbage-collected language?",
    "choices": [
      "Because C source files compile faster than other languages",
      "Because a kernel needs to talk to hardware directly, with no runtime, no garbage collector pauses, and no hidden costs it didn't ask for",
      "Because garbage-collected languages can't be compiled to machine code",
      "Because only C has pointers"
    ],
    "answer": 1,
    "explain": "Garbage-collected languages compile to machine code just fine - the real issue is a GC's unpredictable pauses and hidden runtime, which a kernel talking directly to hardware can't tolerate. C's whole appeal here is no surprises about what a line of code costs."
  }
]
```

---

[Guide overview](_guide.md)
