The Preprocessor: Macros, Include Guards, Conditional Compilation
What This Concept Is
The C preprocessor is a text-substitution pass that runs before the compiler sees your source. It understands:
#include "file.h"or#include <file.h>- paste the file here#define NAME replacement- every later occurrence ofNAMEbecomesreplacement#define MACRO(x) (x)*(x)- function-like macros (parameters substituted textually)#undef NAME- remove a prior definition#if,#ifdef,#ifndef,#elif,#else,#endif- conditional inclusion of source#error,#pragma,#line- compiler directives- Predefined macros:
__FILE__,__LINE__,__DATE__,__STDC__,__STDC_VERSION__
The preprocessor does not know C grammar. It is pure text. That is its power and its biggest hazard.
Why It Matters Here
You meet the preprocessor in three daily tasks:
- guarding headers against multiple inclusion
- defining constants and small inline operations
- selecting code for different platforms or build configurations
Every one of those has a canonical idiom and a classic bug. Learn the idioms up front.
Concrete Example
Include guards - every header you write uses them:
/* math_util.h */
#ifndef MATH_UTIL_H
#define MATH_UTIL_H
int add(int a, int b);
#endif
Without the guard, a header included twice (transitively) would declare things twice and some compilers would complain or accept partially.
Function-like macro, parenthesized correctly:
#define SQUARE(x) ((x) * (x))
int y = SQUARE(1 + 2); /* expands to ((1 + 2) * (1 + 2)) = 9, correct */
Without the inner parentheses, SQUARE(1 + 2) would expand to 1 + 2 * 1 + 2 = 5. Always parenthesize both the whole body and every parameter use.
Conditional compilation for platform selection:
#if defined(__linux__)
#include <unistd.h>
#elif defined(_WIN32)
#include <windows.h>
#else
#error "Unsupported platform"
#endif
Compile-time debug switch:
#ifdef DEBUG
#define LOG(fmt, ...) fprintf(stderr, "[%s:%d] " fmt "\n", __FILE__, __LINE__, __VA_ARGS__)
#else
#define LOG(fmt, ...) ((void)0)
#endif
Toggle with gcc -DDEBUG ....
Common Confusion / Misconception
"Macros are like functions." They are text substitution. #define MAX(a, b) ((a) > (b) ? (a) : (b)) called as MAX(i++, j++) evaluates one argument twice, incrementing it twice. Prefer static inline functions for anything non-trivial; reserve macros for literal constants, type-independent operations, and compile-time switches.
"The header is included, so its declarations only exist once." The textual content is pasted every time. The #ifndef guard makes the second paste empty so the names are not redeclared.
How To Use It
- Every header file: unique
#ifndef X_H / #define X_H / #endifguard based on the path/name. - Prefer
constandenumto object-like#definefor constants with types. - Parenthesize macro bodies and every argument inside them.
- Do not put semicolons at the end of
#defines unless the macro is meant to terminate a statement. - Use
#pragma onceonly if you already ship on a compiler family that supports it;#ifndefguards are portable.
Check Yourself
- Why does
#define DOUBLE(x) x+x; int y = DOUBLE(3) * 2;not produce12? - What does
#ifdef NDEBUGtypically guard? - Why do header guards need a unique identifier per header?
Mini Drill or Application
- Write a header guard for
stringutil.hcorrectly, and also wrong (missing#endif); observe the error. - Define
SQUARE(x)two ways, one with full parentheses and one without. Call each withSQUARE(1 + 2)and explain both results. - Add a
-DVERBOSEcompile flag and gate aprintfbehind#ifdef VERBOSE. - Write a macro that prints
file:line: messageusing__FILE__and__LINE__.