Intuition What this page is
The parent note taught you the rules of #define, #ifdef, #ifndef, and include guards. Here we stress-test those rules against every kind of case the topic can throw at you — every macro-expansion trap, every conditional branch, the degenerate and limiting inputs, a real-world word problem, and an exam-style twist. For each, you forecast the output first (guess before you peek), then we expand the text by hand exactly like the preprocessor does, then we verify .
Remember the golden rule from the parent topic : the preprocessor does blind text substitution and text deletion . It never computes, never checks types. When we compute a number here, that is the compiler doing arithmetic on the already-substituted text.
Every problem this topic can pose falls into one of these case classes . The worked examples below are each tagged with the cell they cover, and together they hit every cell.
#
Case class
What can go wrong / what to watch
Example that covers it
A
Object-like macro, simple constant
text swap, no arithmetic by preprocessor
Ex 1
B
Function-like macro, precedence danger
missing parens change grouping
Ex 2
C
Function-like macro, side-effect / double-eval
argument evaluated twice
Ex 3
D
#ifdef / #ifndef branch selection (defined)
which text survives
Ex 4
E
#ifndef default value override
external -D wins vs built-in default
Ex 5
F
Include guard — paste-twice degenerate case
second paste deleted entirely
Ex 6
G
Degenerate / zero input to a macro
empty arg, 0, macro-defined-as-nothing
Ex 7
H
Real-world word problem (cross-platform)
choosing the right branch on a real target
Ex 8
I
Exam twist — trailing semicolon + nesting
the classic arr[MAX;] and stacked conditions
Ex 9
J
Limiting case — macro that expands to another macro (chained)
rescanning, recursion limits
Ex 10
Worked example Ex 1 — Case A: object-like constant
#define RATE 5
int cost = RATE * 8 + 2 ;
What integer does cost hold?
Forecast: guess the number before reading on. ______
Substitute the token RATE. The preprocessor scans, finds RATE, pastes 5.
→ int cost = 5 * 8 + 2;
Why this step? This is all the preprocessor does — no math yet. It hands this plain-C line to the compiler.
Now the compiler computes. 5 * 8 = 40, then 40 + 2 = 42.
Why this step? Multiplication binds tighter than addition, so * happens first.
Verify: 5 × 8 + 2 = 42 . The macro contributed no arithmetic; it only supplied text. ✅
Worked example Ex 2 — Case B: precedence trap in a function-like macro
#define AREA ( w , h ) w * h
int a = AREA ( 2 + 3 , 4 );
What is a?
Forecast: the name suggests ( 2 + 3 ) × 4 = 20 . Is it?
Substitute blindly. Replace w with the text 2 + 3 and h with 4:
→ int a = 2 + 3 * 4;
Why this step? Substitution is pure paste — no invisible parentheses are added around your arguments.
Compiler evaluates with real precedence. 3 * 4 = 12, then 2 + 12 = 14.
Why this step? * outranks +, so the intended "add first" never happens.
Verify: we got 14, not 20 . The fix is #define AREA(w, h) ((w) * (h)), which expands to ((2 + 3) * (4)) = 20. ✅ (Both values checked below.)
Worked example Ex 3 — Case C: double-evaluation / side effects
#define MAX2 ( a , b ) ((a) > (b) ? (a) : (b))
int i = 5 ;
int m = MAX2 (i ++ , 3 );
After this runs, what are m and i?
Forecast: you might expect m = 5, i = 6. Watch the i++.
Expand the macro. a is i++, b is 3:
→ int m = ((i++) > (3) ? (i++) : (3));
Why this step? Notice a appears twice in the body, so i++ was pasted twice .
Trace execution. First (i++) compares 5 > 3 → true, and increments i to 6. Because the condition is true, the middle branch (i++) runs: it yields 6, then increments i to 7.
Why this step? The ? : picks the true-branch, which re-evaluates the argument — a real function would evaluate i++ exactly once.
Verify: m = 6, i = 7. This is the danger Macros vs inline functions warns about: an inline function evaluates each argument once ; a macro pastes it as many times as it appears. ✅
Worked example Ex 4 — Case D:
#ifdef / #ifndef branch selection
#define LOG_LEVEL
#ifdef LOG_LEVEL
int level = 2 ;
#else
int level = 0 ;
#endif
#ifndef LOG_LEVEL
int hidden = 99 ;
#endif
Which lines survive to the compiler, and what is level?
Forecast: is LOG_LEVEL defined? What does that make each block do?
First block: #ifdef LOG_LEVEL. LOG_LEVEL is defined, so the #ifdef is true . Keep int level = 2; and delete the #else line.
Why this step? #ifdef keeps its body when the name is defined.
Second block: #ifndef LOG_LEVEL. The name is defined, so #ifndef is false . The line int hidden = 99; is deleted as text — the variable hidden never exists.
Why this step? #ifndef is the exact mirror of #ifdef.
Verify: the compiler sees only int level = 2;. So level == 2, and hidden is not even declared. ✅
Worked example Ex 5 — Case E:
#ifndef default with external override
/* compiled with: gcc -DBUFFER_SIZE=1024 prog.c */
#ifndef BUFFER_SIZE
#define BUFFER_SIZE 256
#endif
char buf [BUFFER_SIZE];
How big is buf?
Forecast: the file says 256, the command line says 1024. Who wins?
The -D flag defines it first. -DBUFFER_SIZE=1024 is exactly like putting #define BUFFER_SIZE 1024 at the very top, before your code — see Build flags -D and the command line .
Why this step? Command-line defines are injected ahead of the source.
#ifndef BUFFER_SIZE is now false. It's already defined (=1024), so the inner #define BUFFER_SIZE 256 is skipped .
Why this step? The whole point of #ifndef here is "only set a default if nobody set it."
Substitute. char buf[BUFFER_SIZE]; → char buf[1024];.
Verify: buf has 1024 bytes. Remove the -D flag and the same file compiles buf[256]. External configuration overrides without editing source. ✅
Worked example Ex 6 — Case F: include guard defeats the double paste
Files:
/* point.h */
#ifndef POINT_H
#define POINT_H
struct Point { int x, y; };
#endif
/* main.c */
#include "point.h"
#include "point.h" /* included twice on purpose */
How many times does struct Point { ... }; appear in the translation unit?
Forecast: without a guard it appears twice → "redefinition" error. With the guard?
First #include pastes the file. POINT_H is undefined → #ifndef true → define POINT_H, keep the struct.
→ translation unit now contains struct Point { int x, y; }; once.
Second #include pastes the file again. But POINT_H is now defined → #ifndef false → the preprocessor deletes everything up to #endif.
Why this step? The tag we defined the first time acts as a "already glued" flag.
Verify: the struct appears exactly once — no redefinition error. See Header files and translation units for why one struct-definition-per-unit is the hard limit. ✅
Worked example Ex 7 — Case G: degenerate / empty inputs
#define EMPTY
#define STICK ( a , b ) a b
#define ZERO 0
int n = STICK ( 1 , ZERO);
int m = 4 EMPTY + 1 ;
What are n and m?
Forecast: a macro defined as nothing — what does EMPTY paste?
EMPTY expands to the empty string. 4 EMPTY + 1 → 4 + 1 (just whitespace where EMPTY was).
Why this step? #define EMPTY with no replacement text means "delete this token." A degenerate but legal macro.
STICK(1, ZERO). Paste a=1, b=ZERO: → 1 ZERO. Then ZERO is itself rescanned and becomes 0: → 1 0.
Why this step? But 1 0 is two integer tokens with a space — that is not a valid expression, so this line would be a compile error unless we meant something joinable.
Repair intent. To glue into 10 you'd need the ## paste operator: #define STICK(a,b) a##b → 10. As written, n does not compile.
Verify: m = 4 + 1 = 5 (the empty macro vanished cleanly). STICK(1, ZERO) without ## is a degenerate failure — worth knowing that "expands to two tokens" ≠ "expands to a number." ✅ (We check m and the ## version below.)
Worked example Ex 8 — Case H: real-world cross-platform word problem
Problem. You ship a logging function. On Windows the newline should be "\r\n"; on Linux/macOS it's "\n". Your build sets _WIN32 on Windows automatically. Write the macro and predict what a Linux build emits for NEWLINE.
#ifdef _WIN32
#define NEWLINE " \r\n "
#else
#define NEWLINE " \n "
#endif
Forecast: on a Linux build, is _WIN32 defined?
Ask: is _WIN32 defined here? On Linux the compiler does not predefine _WIN32, so #ifdef _WIN32 is false .
Why this step? Cross-platform code branches on predefined target macros — see Conditional compilation for cross-platform code .
The #else branch survives. The preprocessor keeps #define NEWLINE "\n" and deletes the Windows line entirely.
Verify: on Linux, NEWLINE expands to "\n" (a 1-character string). On Windows it would be "\r\n" (2 characters). We check both string lengths below. ✅
Worked example Ex 9 — Case I: exam twist (trailing
; + nested conditions)
#define SIZE 10 ;
#define FLAG
int arr [SIZE]
#ifdef FLAG
;
int extra = SIZE + 1 ;
#endif
Spot the two bugs and give the fixed extra.
Forecast: what does arr[SIZE] become when SIZE carries a semicolon?
Substitute SIZE → 10;. int arr[SIZE] → int arr[10;].
Why this step? The ; was part of the replacement text , so it lands inside the brackets → syntax error. Bug 1: never end a #define value with ;.
The second use. int extra = SIZE + 1; → int extra = 10; + 1; — again broken (10; closes the statement, leaving a stray + 1;).
Why this step? Same trailing-semicolon poison, now in an expression.
Fix. #define SIZE 10 (no semicolon). Then arr[10] is valid and extra = 10 + 1.
Verify: with the fix, extra = 11 and arr has 10 elements. The #ifdef FLAG block is kept (FLAG is defined) but the semicolon bug is independent of the conditional. ✅
Worked example Ex 10 — Case J: limiting case — chained macros & rescanning
#define A B
#define B C
#define C 7
#define TWICE ( x ) ((x) + (x))
int r = TWICE (A);
What is r?
Forecast: how far does the chain A → B → C → 7 unwind?
Expand TWICE(A). → ((A) + (A)).
Why this step? Argument A is pasted twice (Case C flavour), but here it has no side effect.
Rescan the result. The preprocessor re-examines the output: each A → B, then B → C, then C → 7.
→ ((7) + (7)).
Why this step? After every substitution the preprocessor rescans for more macros — this is the "limiting behaviour" of chained defines. It stops when no macro names remain (and would refuse to expand a macro into itself to avoid infinite recursion).
Compiler computes. 7 + 7 = 14.
Verify: r = 14. The full chain resolved in one preprocessing pass because rescanning follows the links A→B→C→7. ✅
Recall Rapid self-test (cover the answers)
Ex1 RATE*8+2 ::: 42
Ex2 AREA(2+3,4) with w*h body ::: 14 (fix gives 20)
Ex3 MAX2(i++,3) from i=5 gives m,i ::: m=6, i=7 (double evaluation)
Ex5 with -DBUFFER_SIZE=1024 ::: buf has 1024 bytes
Ex6 struct pasted with guard, included twice ::: appears once
Ex8 NEWLINE on Linux ::: "\n"
Ex9 fixed extra ::: 11
Ex10 TWICE(A) through chain ::: 14
Mnemonic The whole matrix in one breath
"Paste, don't compute; parenthesise, don't assume; guard, don't double; and the semicolon is a trap." Every cell above is one of those four sins avoided.