The gap nobody can skip
In the last guide you watched the CPU do exactly one thing, over and over: fetch a number from memory, decode it as an instruction, run it, repeat — the fetch–execute cycle. The crucial detail is what it fetches. The processor understands only machine code: raw bytes like 0x48 0x89 0xC3 that happen to mean "copy register rax into rbx" on this exact chip. It has no idea what the word `printf` is. It cannot read text.
But you write `int x = 41 + 1;` — readable text, designed for humans. That is the whole tension of source code versus machine code: the form that is pleasant to write is exactly the form the machine cannot run, and the form the machine runs is unbearable to write by hand. Some piece of software has to stand between the two and do the translation. The two big strategies for doing that — translate it all first, or translate as you go — are what people mean by compiled versus interpreted.
Keep one thing straight from the start: "compiled" and "interpreted" describe how a language is usually run, not a fixed property of the language itself. The same source could, in principle, be compiled by one tool and interpreted by another. The label is about the road you take, not the destination — and most of this guide is about why C, the language at the bottom of the low-level versus high-level stack, almost always takes the compiled road.
Road one: compile everything first
A compiler reads your entire source file once, ahead of time, and translates it into machine code for one specific kind of CPU. The output is a finished file of instructions — an executable — that the operating system can load and the processor can run directly, with no translator anywhere in sight at run time. This is the road C takes. When you run `gcc -O2 -Wall main.c`, you are asking a compiler to turn `main.c` into an a.out full of ready-to-fetch bytes.
Because all the translating happens before the program runs, the compiler can take its time. It can rearrange your loops, fold `41 + 1` into the constant `42` at compile time, keep a hot variable in a register instead of memory, and delete code it can prove is dead. That is why compiled C tends to be fast: the cleverness is paid for once, up front, and the running program is pure machine code with no overhead of being re-translated. This is a big part of what "running close to the metal" buys you.
The price is paid in two places. First, you must compile before you can run, every time you change the code — the slower, more deliberate rhythm we will meet below. Second, the bytes are baked for one target. An executable compiled for an x86-64 laptop is meaningless gibberish to an ARM phone; to run there you must compile again for that chip and operating system. That bound-to-a-target reality is the heart of portability and the platform, and it is why a C program is portable as source but never as a single finished binary.
Road two: interpret as you go
An interpreter takes a different road. Instead of producing a standalone machine-code file ahead of time, it keeps your source (or a lightly digested form of it) and walks through it while the program runs, performing each operation as it reaches it. Python, Ruby, and a classic shell work roughly this way. There is no `a.out` to hand someone; you ship the source plus the interpreter, and the interpreter does the work live, every time.
Here is the honest trade. The interpreter never sees the CPU's raw instruction stream as your code — your loop runs inside the interpreter's own machine code, which decides at run time what each line means. That layer of indirection costs speed: re-deciding "what does this line do" on every pass through a loop is real work a compiler would have done once. In exchange you get a very short feedback loop (edit, run, immediately see the result) and easy portability: the same script runs anywhere the interpreter has been built, because the interpreter is the part that is platform-specific, not your code.
Why C took the compiled road
Systems programming — the kind you met in guide 1 — sits at the bottom of the low-level versus high-level divide. You are writing kernels, drivers, allocators, the very interpreters other languages depend on. That work demands predictable speed and direct contact with memory and hardware, with no interpreter standing between you and the chip. A finished machine-code executable is exactly that: no run-time translator, nothing to ship alongside, just instructions the CPU fetches.
There is a second, quieter reason. Because C compiles down to plain machine code and a tiny, well-defined calling convention, almost everything can call into it and it can call almost anything. That is why C became the lingua franca that operating-system interfaces are written in: the Python interpreter, your graphics driver, and the kernel all meet on C's terms. Choosing the compiled road was not only about speed — it was about being the common ground everything else stands on.
The edit–compile–run loop in practice
Choosing the compiled road shapes your daily rhythm. With an interpreter you mostly edit and run. With C you live inside the edit–compile–run loop: change the text, ask the compiler to rebuild, and only then run the result. The extra compile step is also a gift — the compiler reads your whole program before it ever runs and catches a whole class of mistakes (a misspelled name, a type that does not fit) at compile time, where they are cheap, instead of at run time, where they are not.
Here is the tiniest complete C program — the traditional hello world — and the three commands that take it from text to a running process. Notice that `main` returns `0`: that becomes the program's exit status, the one number it hands back to whoever started it, where 0 conventionally means "success". Guide 5 builds and dissects this line by line; for now, just see the shape of the loop.
/* hello.c */
#include <stdio.h>
int main(void) {
printf("hello, world\n");
return 0; /* 0 = success, becomes the exit status */
}
# the edit-compile-run loop, in three commands:
$ gcc -Wall hello.c -o hello # compile source -> executable named 'hello'
$ ./hello # run it; the OS loads and the CPU fetches
hello, world
$ echo $? # the shell shows the exit status
0That is the whole arc of the compiled road in miniature. You will spend the rest of this rung mapping out the layers underneath `./hello` — what the operating system does when it loads that file, what "the kernel" even is — before guide 5 returns to compile and run a C program of your own, slowly and on purpose.