C

Preprocessor & Macros

Introduction to C mentioned the preprocessor as the first stage of compilation — a pure text-substitution pass with no understanding of C syntax at all. This chapter is everything that pass actually does: #include, #define, macros, and conditional compilation. This is also the final chapter of this domain — everything from here builds on everything before it.

Quick reference
What it is
Directives (lines starting with #) processed by pure text substitution before real compilation begins.
Mental model
An aggressive find-and-replace pass over your source text — it has no idea what a variable or a function is, it just swaps text for other text, exactly as instructed.
Key numbers
Every directive starts with # and has no trailing semicolon — a stray semicolon on a #define line becomes part of the substituted text, a classic bug.
Reach for it when
Including other files, defining compile-time constants, writing small inlined helpers, or compiling different code for different targets/configurations.

#include

#include is replaced, before compilation, by the entire contents of the named file — it's literally a copy-paste operation performed by the preprocessor:

#include <stdio.h>    // angle brackets: search the compiler's standard include paths
#include "myheader.h"  // quotes: search relative to this file first

#define for constants

#define MAX_RETRIES 3
#define PI 3.14159

int attempts = 0;
while (attempts < MAX_RETRIES) {   // becomes: while (attempts < 3)
    attempts++;
}

Every occurrence of MAX_RETRIES in the rest of the file is textually replaced with 3 before the compiler ever sees it — the compiler never knows MAX_RETRIES existed as a name at all. This is different from declaring const int MAX_RETRIES = 3;, which creates a real, typed variable the compiler is aware of; a macro constant is pure text substitution with zero type information.

Function-like macros

A macro can also take arguments, making it look like a function call — but it's still pure text substitution, not an actual function call:

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

int result = SQUARE(5);   // becomes: ((5) * (5)) → 25

Because it's textual, every parenthesis in a macro definition matters far more than it looks — see Gotchas for exactly what goes wrong without them.

Conditional compilation

#ifdef/#ifndef/#endif let you include or exclude entire blocks of code at compile time, based on whether a name is defined — the standard way to compile different code for different targets or build configurations:

#define DEBUG_MODE

#ifdef DEBUG_MODE
    printf("Debug: entering main loop\n");
#endif

#ifndef DEBUG_MODE
    // this block is entirely skipped — removed before compilation even sees it
    fast_path_only();
#endif

This is exactly how embedded firmware commonly compiles different code for different target boards from one shared codebase, or strips out debug logging entirely from a release build with zero runtime cost — the excluded code never even reaches the compiler.

Header guards

A header file included by two different files (directly or indirectly) would otherwise get pasted in twice, causing "redefinition" errors. A header guard uses conditional compilation to make a header safe to include more than once:

// myheader.h
#ifndef MYHEADER_H
#define MYHEADER_H

// ... declarations ...

#endif   // MYHEADER_H

The first time this file is included, MYHEADER_H isn't defined yet, so its contents are included and MYHEADER_H gets defined. Every subsequent #include of the same file sees MYHEADER_H already defined and skips the contents entirely — every .h file you write should have one of these.

Real-world applications

Gotchas

Field notes