This is part one of two. Part two covers the C memory model and stack frames.
-Wall
If I use the -Wall flag, the compiler emits warnings. Simple rule: always use it.
0 means more than zero
The value 0 is equivalent to false in C boolean expressions. It also shows up in shell short-circuit evaluation. In an expression like ./a.out && ls, ls only executes if ./a.out returns 0. That’s because 0 is the success exit code, and && short-circuits on failure. So ./a.out returning 0 makes ls run. The 0 in the exit code sense is “success / true” from the shell’s perspective, opposite of the C boolean convention, which trips people up.
printf is variadic
printf is a variadic function: it accepts a variable number of arguments. The format string drives how the remaining arguments are interpreted. No runtime type checking; you lie in the format string, you get undefined behavior.
printf also doesn’t write to the terminal immediately. Data goes into a buffer first. The buffer is flushed when:
- a
\nis encountered fflush(stdout)is called explicitly
This is why output can seem to disappear when a program crashes mid-execution: the buffer never got flushed. If I need output to appear immediately regardless of newlines, fflush(stdout) after the printf is the fix. The buffering is intentional: it makes I/O significantly faster by batching writes instead of issuing a syscall on every character.
Symbolic constants: #define
To avoid magic numbers, C gives you symbolic constants:
#define NAME replacement_text
Convention: names are always uppercase. This signals to every reader that the identifier is a compile-time constant, not a variable.
#define is also the right tool when I need conditional compilation (#if, #ifdef) or when I’m writing macros. It’s also what I reach for when writing header guards or shared header files (#define FILE_H).
What #define actually is
#define belongs to the preprocessor, which runs before the compiler. It rewrites your source: textual substitution, nothing more. It has no type, no memory address, no scope in the C sense.
The compilation pipeline in C goes:
- Preprocessor: macro expansion,
#includeresolution, conditional compilation. Stop here withgcc -E main.cto see what the compiler actually receives. - Compiler: source to assembly
- Assembler: assembly to object code
- Linker: object files → executable
The preprocessor doesn’t know C. It doesn’t know about types, variables, or functions. It does text substitution and moves on. So #define MAX 100 means: wherever the token MAX appears, replace it with 100 before compilation begins. The compiler never sees MAX.
Same for function-like macros:
#define SQUARE(x) ((x) * (x))
Parentheses around x and the whole expression are not optional. Without them:
#define SQUARE(x) x * x
// SQUARE(1 + 1) → 1 + 1 * 1 + 1 = 3, not 4
The preprocessor is doing instruction rewriting, not function calls. It runs before the CPU gets involved, has no type information, and no scope.
Up next
Part two: what happens at the machine level when you declare a variable — registers, RAM, EBP, ESP, and why locals don’t survive past return.