The Preprocessor
#include <stdio.h> has appeared in nearly every example since Chapter 1. This chapter finally explains what it — and every other line starting with # — actually does.
The Preprocessor Runs Before Compilation
The preprocessor is a genuinely separate pass: pure text substitution over the source file, completed entirely before parsing or type-checking ever begins. Rust has no preprocessor at all — its macro system (rust3-4) operates on the actual parsed syntax tree, with real awareness of types and structure, not raw text. This is a fundamental difference in kind, not just syntax.
#define — Object-like Macros
Every occurrence of MAX_SIZE in the file is replaced, textually, with 100 — before the compiler ever sees it as a number. No type checking happens at this stage at all.
#define — Function-like Macros
Looks like a function; is pure text substitution. Without full parenthesization, this breaks badly:
Even with correct parentheses, macros carry a trap no real function has: arguments can be evaluated more than once.
#include
Now the honest explanation: #include pastes the entire contents of the named file, textually, in place of the #include line itself — nothing more sophisticated than that. It's not an "import" in Rust's sense at all; there's no module system underneath, just literal text insertion before compilation.
Header Guards
Including the same header twice — common once a project has several source files each including several headers — pastes its contents twice, causing duplicate definitions and a real compile error. The classic fix:
The modern, widely-supported alternative is a single line:
Rust's module system has no equivalent problem at all — use never textually pastes anything, so there's nothing to accidentally duplicate.
Conditional Compilation
#ifdef/#ifndef/#if/#else/#endif compile different code depending on which macros are defined — commonly used for platform-specific code, or separating debug and release builds.
| Concept | Rust | C |
|---|---|---|
| Preprocessor | none — macros operate on the syntax tree | a genuine separate text-substitution pass |
| Module inclusion | use — a real module reference, no text pasting | #include — literal text pasting |
| Double-inclusion risk | not possible — no text pasting involved | real — requires header guards or #pragma once |
#ifndef/#define/#endif pattern, and supported by every major compiler — though technically a compiler extension, not part of the C standard itself.
i++) can be silently evaluated multiple times, a class of bug that simply cannot occur with a real function call.
Coding Challenges
Define a macro SQUARE(x) without any parentheses around x or the whole expression. Call it as SQUARE(2 + 3) and show what it actually expands to and evaluates as, compared to the mathematically correct answer (25).
📄 View solutionWrite a header file with a struct definition, protected by a header guard using #ifndef/#define/#endif. Explain what compile error would occur if the guard were removed and the header were included twice from the same source file.
📄 View solutionExplain why SQUARE(i++), even with SQUARE fully and correctly parenthesized as ((x) * (x)), is still a bug — and why the equivalent call to a real function square(i++) would never have this problem.
📄 View solutionChapter 4 Quick Reference
- The preprocessor is a text-substitution pass, complete before compilation begins — unlike Rust, which has none
#define NAME value— object-like macro, pure text substitution- Function-like macros need full parenthesization — and can still evaluate arguments multiple times
#includeliterally pastes a file's contents — not a real module import- Header guards (
#ifndef/#define/#endifor#pragma once) prevent duplicate-inclusion errors #ifdef/#if/#else/#endif— conditional compilation for platform/debug-vs-release code- Next chapter: Multi-File Projects & Makefiles — separate compilation and linking, for real