5.1.27 · D4C Programming

Exercises — Preprocessor directives — #define, #ifdef, #ifndef, #include guards

2,199 words10 min readBack to topic

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
#endif
Recall Solution
  • #define MAX 100 → everywhere the token MAX appears later, paste the text 100.
  • #ifdef DEBUG → keep the following lines only if DEBUG is currently defined; otherwise delete them.
  • #ifndef CONFIG_H → keep the following lines only if CONFIG_H is 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: 20not 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");
#endif
Recall 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                    #endif

main.c does #include "a.h" then #include "b.h". Is struct B compiled? Explain.

Recall Solution

No — struct B is silently dropped.

  1. a.h pasted first: HEADER_H undefined → guard is true → define HEADER_H, keep struct A.
  2. b.h pasted next: HEADER_H is now defined → #ifndef HEADER_H is false → the whole body of b.h (including struct B) is deleted. The tag must be unique (e.g. A_H, B_H) — see the include-order picture below.
Figure — Preprocessor directives — #define, #ifdef, #ifndef, #include guards

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_H undefined → guard true → define POINT_H, keep the struct.
  • 2nd include: POINT_H now defined → guard false → body deleted. The guard prevents the "redefinition of struct 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 : 104. (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
#endif

If 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;
#endif

Assume 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))
  • SCALE3 Surviving code: Step 4 — the compiler evaluates: (3 * 10) = 30, then 30 * 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 LIMIT is now false → the default #define LIMIT 10 is skipped → LIMIT stays 4.
  • SCALE still defined → same branch survives. Substituted: int total = ((2 + 1) * (4)) * 3;(3 * 4) = 12, then 12 * 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?
20 — missing outer parens let * 3 bind to only one term.
#define HALF(x) x / 2, what does HALF(6 + 4) give?
8, because 4 / 2 happens before the +.
Why does #define SIZE 32; break int arr[SIZE];?
The ; is substituted too → int arr[32;];, invalid.
If two headers share #ifndef HEADER_H, what happens to the second?
Its body is deleted — the tag is already defined by the first.
With guards, how many times does a struct appear after two #includes of its header?
Exactly once.
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.
AREA(2+1, 10) * 3 with #define AREA(w,h) ((w)*(h)) and SCALE=3?
90.
Same as above but -DLIMIT=4 so h=4?
36.
Which stage catches a missing semicolon after a macro expansion?
The compiler, not the preprocessor.