Setup & First Program
#include, main, printf format specifiers, escape characters, comments — the compilation pipeline end to end.
The five pieces of this chapter
This chapter broke into five pieces for me: the Hello World skeleton, the compilation pipeline hiding behind one gcc command, printf's format specifiers, escape characters, and the two comment styles. My first real writing exercise was printing a name, an age, and a float in a single printf call — %s, %d, and %f together instead of one at a time.
Hello, World
#include <stdio.h>
int main(void) {
printf("Hello, World!\n");
return 0;
}#include <stdio.h>— I'm not importing a library, I'm pulling in a header: declarations forprintfand the rest of<stdio.h>that the compiler needs to see before it'll let me call them. The actual implementation gets linked in later, at the last pipeline stage.int main(void)— the entry point.intis the return type; the shell reads it back as my process's exit code.(void)means explicitly no arguments — bare()in old-style C means "unspecified arguments," not "none," which caught me off guard the first time.printf("Hello, World!\n")— a call into the C standard library, format string first, arguments after.return 0;— exit code 0 conventionally means success to the shell (echo $?right after running it shows the number).
The compilation pipeline
gcc hello.c -o hello looks like one step. It's actually four, chained together silently:
| Stage | Tool | Input → output | What happens |
|---|---|---|---|
| Preprocessing | cpp | hello.c → hello.i | #include files are pasted in literally, #define macros are textually substituted, comments are stripped. |
| Compilation | cc1 | hello.i → hello.s | Preprocessed C is translated into assembly for the target architecture. |
| Assembly | as | hello.s → hello.o | Assembly becomes machine code, packaged as a relocatable object file. |
| Linking | ld | hello.o → hello | The object file is combined with the compiled C library — where printf actually lives — into one runnable executable. |
I ran each stage on its own, just to stop treating the compiler as a black box:
gcc -E hello.c -o hello.i # stop after preprocessing — read the expanded source
gcc -S hello.i -o hello.s # stop after compilation — read the generated assembly
gcc -c hello.s -o hello.o # stop after assembly — an object file, not yet runnable
gcc hello.o -o hello # link — now it's runnableBefore any of this I had to actually install the toolchain — sudo apt update && sudo apt install build-essential gdb valgrind git on Ubuntu/Debian/WSL, verified with gcc --version. build-essential is what actually provides gcc, as, and ld in the first place.
printf format specifiers
Eight specifiers, each telling printf how to read the next argument off the stack — get the specifier wrong and I get the wrong type back, silently:
| Specifier | Reads as | Example |
|---|---|---|
| %d | int, decimal | printf("%d", 42) → 42 |
| %c | char | printf("%c", 'A') → A |
| %f | float / double | printf("%f", 3.14) → 3.140000 |
| %s | char*, string | printf("%s", "hi") → hi |
| %x | unsigned int, hex | printf("%x", 255) → ff |
| %p | pointer, address | printf("%p", &x) → 0x7ffee... |
| %u | unsigned int, decimal | printf("%u", 42) → 42 |
| %ld | long, decimal | printf("%ld", 100000L) → 100000 |
%x prints hexadecimal, not decimal — printf("%x", 255) is ff, not 255. I got this one wrong more than once before it actually stuck — the kind of mistake worth actually drilling instead of just reading past it again.
Escape characters
| Escape | Meaning |
|---|---|
\n | newline |
\t | tab |
\\ | a literal backslash |
\" | a literal double-quote inside a string |
\0 | the null character — value 0, not the character '0' |
\0 matters more to me than the other four combined: it's the terminator every C string relies on, the sentinel strlen scans for, and the one every hand-rolled string function I wrote later (1.3.F) had to respect explicitly. Here it was just a one-line mention; two topics later it became load-bearing.
Comments
// line comment — runs to end of line
/* block comment —
can span
multiple lines */Both get stripped at the preprocessing stage above, before the compiler proper ever sees them — comments cost nothing at runtime, by construction.
Notes from readers
Comments — via GitHub