Exercises — Preprocessor directives — #define, #ifdef, #ifndef, #include guards
Remember the one law that governs everything here: the preprocessor does blind text substitution and text deletion before the compiler runs. It does not know C. It only knows tokens and # lines.
L1 — Recognition
Exercise 1.1
State in one sentence what each line does:
#define MAX 100
#ifdef DEBUG
#ifndef CONFIG_H
#endifRecall Solution
#define MAX 100→ everywhere the tokenMAXappears later, paste the text100.#ifdef DEBUG→ keep the following lines only ifDEBUGis currently defined; otherwise delete them.#ifndef CONFIG_H→ keep the following lines only ifCONFIG_His not defined.#endif→ close the most recent conditional block.
Exercise 1.2
Which of these are preprocessor directives, and which are ordinary C?
(a) #include <stdio.h> (b) int x = 5; (c) #define N 8 (d) return 0;
Recall Solution
Directives (start with #, handled before compilation): (a) and (c).
Ordinary C statements (handled by the compiler): (b) and (d).
The distinguishing mark is the leading #.
L2 — Application
Exercise 2.1
After preprocessing, what exact text replaces y?
#define DOUBLE(x) (x) + (x)
int y = DOUBLE(5) * 3;Give the substituted text and evaluate the number.
Recall Solution
Substitute DOUBLE(5) → (5) + (5):
Now the compiler applies precedence: * before +, so (5) * 3 = 15, then (5) + 15 = 20.
Answer: 20 — not 30. The body wasn't wrapped in outer parentheses, so the * 3 grabbed only the second (x).
Exercise 2.2
Evaluate by hand:
#define HALF(x) x / 2
int z = HALF(6 + 4);Recall Solution
Blind substitution → int z = 6 + 4 / 2;
Precedence: / before +, so 4 / 2 = 2, then 6 + 2 = 8.
Answer: 8 (not 5). The fix is #define HALF(x) ((x) / 2).
Exercise 2.3
What number ends up in w?
#define SQUARE(x) ((x) * (x))
int w = SQUARE(3 + 1);Recall Solution
Substitute → ((3 + 1) * (3 + 1)) → (4 * 4) = 16. The full parenthesisation makes this behave like a real squaring.
Exercise 2.4
Which lines survive to the compiler?
#define VERBOSE
#ifdef VERBOSE
printf("A\n");
#endif
#ifdef QUIET
printf("B\n");
#endifRecall Solution
VERBOSE is defined → the printf("A\n"); survives.
QUIET is never defined → the #ifdef QUIET block is deleted as text; printf("B\n"); never reaches the compiler.
Surviving code: just printf("A\n");. This is the mechanism behind Conditional compilation for cross-platform code.
L3 — Analysis
Exercise 3.1
This compiles with a confusing error. Explain the text the compiler actually sees.
#define SIZE 32;
int arr[SIZE];Recall Solution
The replacement text for SIZE is 32; (semicolon included). So int arr[SIZE]; becomes:
That is not valid C — the ; inside the brackets breaks it. A #define is not a C statement, so it takes no trailing semicolon. Fix: #define SIZE 32.
Exercise 3.2
Two headers, both using the same guard tag:
/* a.h */ /* b.h */
#ifndef HEADER_H #ifndef HEADER_H
#define HEADER_H #define HEADER_H
struct A { int p; }; struct B { int q; };
#endif #endifmain.c does #include "a.h" then #include "b.h". Is struct B compiled? Explain.
Recall Solution
No — struct B is silently dropped.
a.hpasted first:HEADER_Hundefined → guard is true → defineHEADER_H, keepstruct A.b.hpasted next:HEADER_His now defined →#ifndef HEADER_His false → the whole body ofb.h(includingstruct B) is deleted. The tag must be unique (e.g.A_H,B_H) — see the include-order picture below.

Exercise 3.3
How many times does struct Point appear after preprocessing main.c?
/* 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 */Recall Solution
Exactly once.
- 1st include:
POINT_Hundefined → guard true → definePOINT_H, keep the struct. - 2nd include:
POINT_Hnow defined → guard false → body deleted. The guard prevents the "redefinition ofstruct Point" error you'd get without it. See Header files and translation units.
L4 — Synthesis
Exercise 4.1
Write a correct include guard for a header named matrix_utils.h that defines struct Matrix { int rows, cols; };.
Recall Solution
#ifndef MATRIX_UTILS_H
#define MATRIX_UTILS_H
struct Matrix { int rows, cols; };
#endif /* MATRIX_UTILS_H */The tag is derived from the filename (uppercase, . → _) so it is unique across the project.
Exercise 4.2
Write a macro MIN(a, b) that returns the smaller of two values and is safe for expressions like MIN(x+1, y-2).
Recall Solution
#define MIN(a, b) (((a) < (b)) ? (a) : (b))Every argument is wrapped, and the whole conditional expression is wrapped. Check with MIN(3+1, 2*5) → (((3+1) < (2*5)) ? (3+1) : (2*5)) → (4 < 10) ? 4 : 10 → 4.
(Caveat worth knowing: because a and b are pasted twice, MIN(i++, j) evaluates i++ twice — a reason inline functions are often safer.)
Exercise 4.3
Write a "default-with-override" configuration block: use LOG_LEVEL set to 2 unless the build supplies its own value via the command line.
Recall Solution
#ifndef LOG_LEVEL
#define LOG_LEVEL 2
#endifIf the build passes -DLOG_LEVEL=4 (see Build flags -D and the command line), then LOG_LEVEL is already defined → the #ifndef is false → the default is skipped and 4 wins. Otherwise it defaults to 2.
L5 — Mastery
Exercise 5.1
Trace this fully. What is the exact surviving C code, and what value does total hold?
#define SCALE 3
#ifndef LIMIT
#define LIMIT 10
#endif
#define AREA(w, h) ((w) * (h))
#ifdef SCALE
int total = AREA(2 + 1, LIMIT) * SCALE;
#else
int total = 0;
#endifAssume LIMIT was not supplied externally.
Recall Solution
Step 1 — LIMIT undefined → guard true → #define LIMIT 10.
Step 2 — SCALE is defined → keep the #ifdef SCALE branch, delete the #else.
Step 3 — expand macros in int total = AREA(2 + 1, LIMIT) * SCALE;:
AREA(2 + 1, LIMIT)→((2 + 1) * (LIMIT))→((2 + 1) * (10))SCALE→3Surviving code: Step 4 — the compiler evaluates:(3 * 10) = 30, then30 * 3 = 90. Answer: 90.
Exercise 5.2
Now suppose the build is invoked as gcc -DLIMIT=4 prog.c. Redo Exercise 5.1's total.
Recall Solution
-DLIMIT=4 defines LIMIT as 4 before the file's text is seen.
#ifndef LIMITis now false → the default#define LIMIT 10is skipped →LIMITstays4.SCALEstill defined → same branch survives. Substituted:int total = ((2 + 1) * (4)) * 3;→(3 * 4) = 12, then12 * 3 = 36. Answer: 36. This shows how a single command-line flag rewires the compiled result — the whole point of Build flags -D and the command line.
Exercise 5.3
Across the build pipeline, at which stage is the following bug caught, and why?
#define GREET printf("hi")
int main(void) { GREET } /* note: no semicolon after GREET */Recall Solution
The preprocessor happily produces:
No # error occurs — text substitution is legal. The bug is a missing semicolon, which is a syntax rule of C. So it is caught later, at the compiler stage (parsing), not by the preprocessor. Lesson: the preprocessor never validates C grammar; it only pastes and deletes. Fix: #define GREET printf("hi"); or write the ; at the call site.
Recall Feynman self-check
Cover the solutions and ask yourself three questions per exercise: What text does the preprocessor paste or delete? What does the compiler then evaluate? What number/answer results? If you can narrate those three cleanly, you own this topic.
Flashcards
In DOUBLE(5) * 3 with #define DOUBLE(x) (x) + (x), what is the value?
* 3 bind to only one term.#define HALF(x) x / 2, what does HALF(6 + 4) give?
4 / 2 happens before the +.Why does #define SIZE 32; break int arr[SIZE];?
; is substituted too → int arr[32;];, invalid.If two headers share #ifndef HEADER_H, what happens to the second?
With guards, how many times does a struct appear after two #includes of its header?
Correct expression-safe MIN macro?
#define MIN(a, b) (((a) < (b)) ? (a) : (b)).Default-with-override pattern for LOG_LEVEL?
#ifndef LOG_LEVEL / #define LOG_LEVEL 2 / #endif.