The Preprocessor

Course 2 · Ch 4
The Preprocessor
A text-substitution pass that runs before the compiler even sees real C

#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

#define MAX_SIZE 100

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

#define SQUARE(x) ((x) * (x))

Looks like a function; is pure text substitution. Without full parenthesization, this breaks badly:

// #define SQUARE(x) x * x (missing parens) SQUARE(a + b) // expands to: a + b * a + b — wrong, due to operator precedence

Even with correct parentheses, macros carry a trap no real function has: arguments can be evaluated more than once.

int i = 5; SQUARE(i++); // expands to ((i++) * (i++)) — i is incremented TWICE, not 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:

#ifndef MYHEADER_H #define MYHEADER_H // header contents #endif

The modern, widely-supported alternative is a single line:

#pragma once

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.

#ifdef DEBUG printf("Debug: value = %d\n", value); #endif
ConceptRustC
Preprocessornone — macros operate on the syntax treea genuine separate text-substitution pass
Module inclusionuse — a real module reference, no text pasting#include — literal text pasting
Double-inclusion risknot possible — no text pasting involvedreal — requires header guards or #pragma once
Prefer #pragma once in new code
Simpler than the #ifndef/#define/#endif pattern, and supported by every major compiler — though technically a compiler extension, not part of the C standard itself.
Macro arguments can be evaluated more than once
Always fully parenthesize both the parameters and the entire macro body — but even then, remember that a function-like macro is not a real function: an argument with a side effect (like i++) can be silently evaluated multiple times, a class of bug that simply cannot occur with a real function call.

Coding Challenges

Challenge 1

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 solution
Challenge 2

Write 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 solution
Challenge 3

Explain 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 solution

Chapter 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
  • #include literally pastes a file's contents — not a real module import
  • Header guards (#ifndef/#define/#endif or #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