C Preprocessor Directives, Object-Like Macros & Function-Like Macro Pitfalls Masterclass
Welcome to Phase 15 (Chapter 39): C Preprocessor Directives & Macro Pitfalls Masterclass! The C Preprocessor (CPP) is a text-transformation engine that runs before the C compiler ever sees your source file. Every line starting with # is a preprocessor directive. In this guide you master how CPP transforms source code, how to write safe macros, and how to avoid notorious pitfalls.
Run gcc -E hello.c -o hello.i to see the preprocessed output directly. You will see thousands of lines from expanded system headers!
Object-like macros define symbolic constants that CPP textually replaces before compilation. They are NOT variables โ they have NO type and NO memory address.
Macro vs const Variable:
โข #define MAX_SIZE 100 โ Pure text substitution. No memory. No type safety. Cannot take address.
โข const int MAX_SIZE = 100; โ Typed, memory allocated on stack. Can be debugged. Preferred in C99+.
Function-like macros accept arguments and perform textual substitution:
โ ๏ธ 4 Classic Macro Pitfalls:
1. Missing parentheses: #define SQ(x) x*x โ SQ(2+3) expands to 2+3*2+3 = 11 instead of 25!
2. Side-effect arguments: #define MAX(a,b) ((a)>(b)?(a):(b)) with MAX(i++, j) increments i TWICE!
3. Multi-statement macro without do-while block causes if-else attachment bugs.
4. No type checking โ Macros bypass C type system entirely.
#include <stdio.h>
/* UNSAFE โ Missing argument parentheses */
#define SQ_BAD(x) x * x
/* SAFE โ Full parentheses wrap */
#define SQ_GOOD(x) ((x) * (x))
/* SAFE multi-statement macro using do-while(0) idiom */
#define SWAP(a, b, type) do { \
type _tmp = (a); \
(a) = (b); \
(b) = _tmp; \
} while (0)
/* Preferred: inline function โ has type safety + debugging */
static inline int sq_inline(int x) { return x * x; }
int main(void) {
printf("SQ_BAD(2+3) = %d (BUG: expected 25)\n", SQ_BAD(2+3));
printf("SQ_GOOD(2+3) = %d (OK: 25)\n", SQ_GOOD(2+3));
printf("sq_inline(5) = %d (OK: 25)\n", sq_inline(5));
int x = 10, y = 20;
SWAP(x, y, int);
printf("After SWAP: x=%d, y=%d\n", x, y); // x=20, y=10
return 0;
}| Macro | Type | Expands To |
|---|---|---|
__FILE__ | String literal | Source filename at compile time |
__LINE__ | Integer | Current line number in source file |
__DATE__ | String literal | Compilation date (e.g. "Aug 18 2026") |
__TIME__ | String literal | Compilation time (e.g. "12:34:56") |
__STDC_VERSION__ | Long integer | C standard version (201710L = C17) |
Q1: When should I prefer inline functions over macros?
Always prefer static inline functions in C99+. They provide type safety, debugger visibility, and single-evaluation semantics that macros cannot guarantee.
Q2: Can macros call other macros?
Yes. Macro expansion is recursive โ CPP will keep expanding until no more macro names remain in the text.
Q3: How do I view the fully preprocessed output of a file?
Run gcc -E source.c -o source.i. The .i file contains all #include expansions and macro substitutions resolved.
Q4: What is the do-while(0) macro idiom?
Wrapping multi-statement macros in do { ... } while(0) makes the macro behave as a single statement expression so it works correctly in if-else branches.
Q5: How do I undefine a previously defined macro?
Use #undef MACRO_NAME. This removes the macro definition from the preprocessor symbol table for all subsequent lines.