The preprocessor in C — exercises with lift capacity, a floor limiter and a simulator with traces (and the DEBUG 0 that switched nothing off)
After that zero for submitting an assignment full of traces, I became very disciplined: at the very top I put #define DEBUG 1 and every trace inside an #ifdef DEBUG. For the submission, I changed the 1 to a 0, convinced that this switched them off. I ran the program the night before the deadline and… there they all were, one after another. #ifdef does not ask whether it is 1 or 0: it asks whether it exists. And it did.
These exercises are so that it does not happen to you. You will write your own macros, wrestle with the preprocessor’s traps and build a small simulator with headers and traces that switch on with -DDEBUG. If you get stuck, the theory and the practice have everything you need.
Table of Contents
How to use these exercises
- Read the statement and the expected output, and write your solution without looking at the solutions.
- Always compile with
gcc -Wall -std=c11: in this topic the compiler warnings will give away more than one macro bug. - If it does not work, open the 🔵 hint and, if it still does not work, look at your code with
gcc -E -Pto see what the preprocessor has done. - When it works, compare it with the solution: what matters is understanding why it is written that way, especially the parentheses.
Level 1: The capacity calculator
Write a program with these constants: MAX_LOAD 1500, ADULT_WEIGHT 75, CHILD_WEIGHT 35 and NUM_GROUPS 4. Define two macros:
GROUP_WEIGHT(adults, children): the weight of the group.FITS(kg): true if the lift can carry those kilos (exactly 1500 kg still fits).
The groups have {4, 15, 20, 16} adults and {2, 6, 0, 10} children. For each group, print its weight, whether it fits and, as an acid test for your parentheses, the weight and result if one more adult got in, calculated with GROUP_WEIGHT(adults[i] + 1, children[i]).
Expected output:
Group 1 ( 4+ 2): 370 kg fits | with one more adult: 445 kg fits Group 2 (15+ 6): 1335 kg fits | with one more adult: 1410 kg fits Group 3 (20+ 0): 1500 kg fits | with one more adult: 1575 kg too big Group 4 (16+10): 1550 kg too big | with one more adult: 1625 kg too big
If the “with one more adult” column gives strange figures (for example, 149 instead of 445 for the first group), the + 1 is being added without being multiplied. Check the parentheses around the parameters. To line up “fits” and “too big” use %-9s, which pads with spaces on the right up to 9 characters.
Level 2: The floor limiter
The building goes from floor −2 to floor 8, but the panel buttons sometimes send impossible requests. Define the macros MIN(a, b), MAX(a, b), CLAMP(x, lo, hi) (which keeps x inside the range [lo, hi]) and ABS(x). The cabin starts on floor 0 and receives the requests {5, 12, -4, 0, 8, -2, 9}. For each one, the cabin goes to the clamped floor; mark with “(corrected)” the ones that had to be changed. At the end, count the corrected requests and the total floors travelled.
Expected output:
Request 5 -> goes to 5 Request 12 -> goes to 8 (corrected) Request -4 -> goes to -2 (corrected) Request 0 -> goes to 0 Request 8 -> goes to 8 Request -2 -> goes to -2 Request 9 -> goes to 8 (corrected) Corrected: 3 | Floors travelled: 48
CLAMP can be written with the other two macros: first raise the value to the minimum with MAX and then lower it to the maximum with MIN. And do not pass requests[i++] to any macro: copy the value into a variable first.
Level 3: The day simulator
Now with headers. Create a lift.h, with a guard, containing MAX_LOAD 1500, AVG_WEIGHT 75, MAX_PEOPLE (calculated), the macros ABS, MIN and DIV_UP(a, b) (integer division rounding up) and a struct Group with origin, destination and people. Also create a logger.h, with a guard, that includes lift.h and defines LOG(msg, value): with -DDEBUG it prints [file:line] msg = value; without it, it does nothing.
The program simulator.c includes both .h files and simulates these five groups: {0, 6, 8}, {0, 3, 25}, {4, -1, 12}, {7, 0, 41}, {-2, 5, 3}. If a group does not fit in one go, the lift makes several trips, and between trips it goes back empty for more people. For each group, work out the trips and the floors travelled, with a LOG for each value; at the end, the totals and the fullest trip of the day.
Expected output without -DDEBUG:
Group 1: 8 ppl. 0 -> 6 | 1 trip(s), 6 floors Group 2: 25 ppl. 0 -> 3 | 2 trip(s), 9 floors Group 3: 12 ppl. 4 -> -1 | 1 trip(s), 5 floors Group 4: 41 ppl. 7 -> 0 | 3 trip(s), 35 floors Group 5: 3 ppl. -2 -> 5 | 1 trip(s), 7 floors Total: 8 trips, 62 floors, max 20 people (1500 kg) at once
And with gcc -Wall -std=c11 -DDEBUG simulator.c -o simulator:
[simulator.c:20] trips = 1 [simulator.c:21] floors = 6 Group 1: 8 ppl. 0 -> 6 | 1 trip(s), 6 floors [simulator.c:20] trips = 2 [simulator.c:21] floors = 9 Group 2: 25 ppl. 0 -> 3 | 2 trip(s), 9 floors [simulator.c:20] trips = 1 [simulator.c:21] floors = 5 Group 3: 12 ppl. 4 -> -1 | 1 trip(s), 5 floors [simulator.c:20] trips = 3 [simulator.c:21] floors = 35 Group 4: 41 ppl. 7 -> 0 | 3 trip(s), 35 floors [simulator.c:20] trips = 1 [simulator.c:21] floors = 7 Group 5: 3 ppl. -2 -> 5 | 1 trip(s), 7 floors Total: 8 trips, 62 floors, max 20 people (1500 kg) at once
DIV_UP(41, 20) has to give 3. The classic trick is to add b - 1 before dividing: (41 + 19) / 20. And if you make n trips, the lift covers the leg 2n − 1 times: n trips with people and n − 1 empty returns.
What’s wrong here?
Three snippets that compile (some with warnings) and do something different from what they seem to. Before reading the explanation, predict what they print.
Snippet 1: the ! that does not negate
#include <stdio.h>
#define MAX_LOAD 1500
#define TOO_BIG(v) v >= MAX_LOAD
int main(void) {
int loads[] = {300, 1650, 980};
for (int i = 0; i < 3; i++) {
if (!TOO_BIG(loads[i]))
printf("%d kg: can go up\n", loads[i]);
else
printf("%d kg: too much weight\n", loads[i]);
}
return 0;
}
What do you think it prints for 300, 1650 and 980 kg?
This is what really comes out:
300 kg: too much weight 1650 kg: too much weight 980 kg: too much weight
All three loads are “too much weight”, even the 300 kg one. And that is despite gcc -Wall warning us at compile time (I show the warnings without the column number):
snippet.c: In function 'main': snippet.c:4: warning: comparison of constant '1500' with boolean expression is always false [-Wbool-compare] snippet.c:9: note: in expansion of macro 'TOO_BIG' snippet.c:4: warning: logical not is only applied to the left hand side of comparison [-Wlogical-not-parentheses] snippet.c:9: note: in expansion of macro 'TOO_BIG' snippet.c:9: note: add parentheses around left hand side expression to silence this warning
The preprocessor turns !TOO_BIG(loads[i]) into !loads[i] >= MAX_LOAD, and ! has higher precedence than >=. So C calculates (!loads[i]) >= 1500: !300 is 0 (any non-zero number negated gives 0), and 0 >= 1500 is false. The if is never true and everything goes to the else. The drawing shows both paths:
Trusting that the macro “is a condition”. Without outer parentheses, any operator you put in front (a !, a -, a *) sticks only to the first part. The -Wlogical-not-parentheses and -Wbool-compare warnings are telling you exactly that.
The fix is to put the whole macro, and its parameter, in parentheses:
#include <stdio.h>
#define MAX_LOAD 1500
#define TOO_BIG(v) ((v) >= MAX_LOAD)
int main(void) {
int loads[] = {300, 1650, 980};
for (int i = 0; i < 3; i++) {
if (!TOO_BIG(loads[i]))
printf("%d kg: can go up\n", loads[i]);
else
printf("%d kg: too much weight\n", loads[i]);
}
return 0;
}
And now it works:
300 kg: can go up 1650 kg: too much weight 980 kg: can go up
When a warning says “in expansion of macro”, the bug is almost never on the if line but in the #define. Go to the line given by the first warning (line 4 here).
Snippet 2: the semicolon that sneaks in
#include <stdio.h>
#define MAX_LOAD 1500;
int main(void) {
int load = 640;
int spare = MAX_LOAD - load;
printf("Free space: %d kg\n", spare);
return 0;
}
If there are 640 kg inside, how many are free?
What comes out:
Free space: 1500 kg
1500 kg free with 640 kg inside. The compiler warning gives the clue:
snippet.c: In function 'main': snippet.c:7: warning: statement with no effect [-Wunused-value]
The ; in the #define is part of the replacement text, so line 7 becomes int spare = 1500; - load;. That is two statements: int spare = 1500; and - load;, which calculates −640 and throws it away (“statement with no effect”). spare stays at 1500.
Ending a #define with a semicolon out of habit. Sometimes it gives a syntax error and you find out; other times, like here, it compiles and the result is wrong.
The fix is to remove the ;:
#include <stdio.h>
#define MAX_LOAD 1500
int main(void) {
int load = 640;
int spare = MAX_LOAD - load;
printf("Free space: %d kg\n", spare);
return 0;
}
Now int spare = 1500 - load; does the whole sum:
Free space: 860 kg
1500 − 640 = 860.
-Wunused-value warnings (“statement with no effect”) on a line that uses a #define are almost always an extra ; in the definition.
Snippet 3: the DEBUG that is 0
#include <stdio.h>
#define DEBUG 0 /* 0 = debugging off... are you sure? */
int main(void) {
int load = 640;
#ifdef DEBUG
printf("[DEBUG] load = %d\n", load);
#endif
printf("Load: %d kg\n", load);
return 0;
}
With DEBUG set to 0, does the trace appear?
It does:
[DEBUG] load = 640 Load: 640 kg
This is my anecdote. #define DEBUG 0 defines the symbol DEBUG, even though its value is 0. And #ifdef DEBUG only asks whether it exists, so the trace goes through to the compiler. There is not even a warning here: as far as the preprocessor is concerned, everything is correct.
Using #ifdef with a symbol whose value is 0 or 1. Either you define it and remove it (#ifdef + -DDEBUG), or you give it a value and compare it (#if DEBUG). Mixing the two ideas does not work.
The fix, if you want to keep the 0 and 1, is to ask about the value with #if:
#include <stdio.h>
#define DEBUG 0 /* 0 = off, 1 = on */
int main(void) {
int load = 640;
#if DEBUG
printf("[DEBUG] load = %d\n", load);
#endif
printf("Load: %d kg\n", load);
return 0;
}
And the trace disappears:
Load: 640 kg
With #if DEBUG, if one day you delete the #define DEBUG, the preprocessor treats it as 0 and the traces switch off by themselves. It is the safer of the two versions.
Solutions
Level 1 solution
#include <stdio.h>
#define MAX_LOAD 1500
#define ADULT_WEIGHT 75
#define CHILD_WEIGHT 35
#define NUM_GROUPS 4
/* Parentheses around each parameter: so GROUP_WEIGHT(a + 1, n) still works */
#define GROUP_WEIGHT(adults, children) ((adults) * ADULT_WEIGHT + (children) * CHILD_WEIGHT)
#define FITS(kg) ((kg) <= MAX_LOAD) /* <= : exactly 1500 kg still fits */
int main(void) {
int adults[NUM_GROUPS] = {4, 15, 20, 16};
int children[NUM_GROUPS] = {2, 6, 0, 10};
for (int i = 0; i < NUM_GROUPS; i++) {
int kg = GROUP_WEIGHT(adults[i], children[i]);
int one_more = GROUP_WEIGHT(adults[i] + 1, children[i]); /* acid test */
printf("Group %d (%2d+%2d): %4d kg %-9s | with one more adult: %4d kg %s\n",
i + 1, adults[i], children[i], kg, FITS(kg) ? "fits" : "too big",
one_more, FITS(one_more) ? "fits" : "too big");
}
return 0;
}
Why it is written this way:
GROUP_WEIGHThas parentheses around(adults)and(children). WithGROUP_WEIGHT(adults[i] + 1, children[i]), without them it would becomeadults[i] + 1 * 75 + children[i] * 35: only the extra adult would be multiplied by 75 and the first group would weigh 4 + 75 + 70 = 149 kg instead of 445. The right-hand column is what checks it.- The whole expression is in parentheses in case someone writes
2 * GROUP_WEIGHT(...). FITSuses<=because exactly 1500 kg fits (group 3 checks it: 20 · 75 = 1500 → “fits”).FITS(kg) ? "fits" : "too big"chooses the text with the ternary operator, and%-9slines it up.
The sums, checked:
| Group | Weight | Fits? | With one more adult | Fits? |
|---|---|---|---|---|
| 1 | 4 · 75 + 2 · 35 = 300 + 70 = 370 | Yes | 370 + 75 = 445 | Yes |
| 2 | 15 · 75 + 6 · 35 = 1125 + 210 = 1335 | Yes | 1410 | Yes |
| 3 | 20 · 75 + 0 = 1500 | Yes (just) | 1575 | No |
| 4 | 16 · 75 + 10 · 35 = 1200 + 350 = 1550 | No | 1625 | No |
Level 2 solution
#include <stdio.h>
#define MIN_FLOOR (-2)
#define MAX_FLOOR 8
#define NUM_REQUESTS 7
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define CLAMP(x, lo, hi) MIN(MAX((x), (lo)), (hi)) /* first the floor limit, then the ceiling */
#define ABS(x) ((x) < 0 ? -(x) : (x))
int main(void) {
int requests[NUM_REQUESTS] = {5, 12, -4, 0, 8, -2, 9};
int current = 0, corrected = 0, travelled = 0;
for (int i = 0; i < NUM_REQUESTS; i++) {
int request = requests[i]; /* copy: no requests[i++] inside the macro */
int floor = CLAMP(request, MIN_FLOOR, MAX_FLOOR);
if (floor != request)
corrected++;
travelled += ABS(floor - current); /* floors the cabin moves */
printf("Request %3d -> goes to %2d%s\n", request, floor,
floor != request ? " (corrected)" : "");
current = floor;
}
printf("Corrected: %d | Floors travelled: %d\n", corrected, travelled);
return 0;
}
Why it is written this way:
CLAMP(x, lo, hi)isMIN(MAX((x), (lo)), (hi)):MAXraises any request below −2 up to −2 andMINlowers any above 8 down to 8. Macros can be nested: the preprocessor rescans the text that comes out of a replacement.MIN_FLOORis(-2)in parentheses by the usual rule: it is an expression, not a plain number.int request = requests[i];is the key decision.CLAMPends up repeatingxseveral times when expanded (there is aMAXinside aMIN, and each repeats its parameters), so withCLAMP(requests[i++], …)the index would advance several times in a single loop.travelled += ABS(floor - current)adds the distance in floors, without sign: going down from 8 to −2 is 10 floors, the same as going up.
The trace loop by loop:
| Loop | request | floor | Corrected? | ABS(floor - current) | travelled |
|---|---|---|---|---|---|
| 0 | 5 | 5 | No | |5 − 0| = 5 | 5 |
| 1 | 12 | 8 | Yes | |8 − 5| = 3 | 8 |
| 2 | −4 | −2 | Yes | |−2 − 8| = 10 | 18 |
| 3 | 0 | 0 | No | |0 − (−2)| = 2 | 20 |
| 4 | 8 | 8 | No | |8 − 0| = 8 | 28 |
| 5 | −2 | −2 | No | |−2 − 8| = 10 | 38 |
| 6 | 9 | 8 | Yes | |8 − (−2)| = 10 | 48 |
Three corrected and 48 floors, as in the output.
Level 3 solution
First, lift.h:
/* lift.h: constants, macros and the simulator struct */
#ifndef LIFT_H
#define LIFT_H
#define MAX_LOAD 1500
#define AVG_WEIGHT 75
#define MAX_PEOPLE (MAX_LOAD / AVG_WEIGHT) /* 20 */
#define ABS(x) ((x) < 0 ? -(x) : (x))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define DIV_UP(a, b) (((a) + (b) - 1) / (b)) /* division rounding up */
struct Group {
int origin;
int destination;
int people;
};
#endif /* LIFT_H */
Then, logger.h:
/* logger.h: traces that only exist when compiling with -DDEBUG */
#ifndef LOGGER_H
#define LOGGER_H
#include "lift.h" /* included again from the .c: the guard prevents it */
#ifdef DEBUG
#define LOG(msg, value) printf(" [%s:%d] %s = %d\n", __FILE__, __LINE__, msg, value)
#else
#define LOG(msg, value) ((void)0)
#endif
#endif /* LOGGER_H */
And the program:
#include <stdio.h>
#include "lift.h"
#include "logger.h"
#define NUM_GROUPS 5
int main(void) {
struct Group day[NUM_GROUPS] = {
{0, 6, 8}, {0, 3, 25}, {4, -1, 12}, {7, 0, 41}, {-2, 5, 3}
};
int total_trips = 0, total_floors = 0, fullest = 0;
for (int i = 0; i < NUM_GROUPS; i++) {
struct Group g = day[i];
int trips = DIV_UP(g.people, MAX_PEOPLE);
int leg = ABS(g.destination - g.origin);
/* go, empty return for more people, go...: 2 * trips - 1 legs */
int floors = leg * (2 * trips - 1);
int in_cabin = MIN(g.people, MAX_PEOPLE); /* the fullest trip of the group */
LOG("trips", trips);
LOG("floors", floors);
printf("Group %d: %2d ppl. %2d -> %2d | %d trip(s), %2d floors\n",
i + 1, g.people, g.origin, g.destination, trips, floors);
total_trips += trips;
total_floors += floors;
if (in_cabin > fullest)
fullest = in_cabin;
}
printf("Total: %d trips, %d floors, max %d people (%d kg) at once\n",
total_trips, total_floors, fullest, fullest * AVG_WEIGHT);
return 0;
}
Why it is written this way:
- Both guards (
LIFT_HandLOGGER_H) are essential:simulator.cincludeslift.hdirectly and again throughlogger.h. Without the guard inlift.h,struct Groupwould be defined twice and it would not compile. - **
DIV_UP(a, b)** is(((a) + (b) - 1) / (b)). With normal integer division, 41 / 20 = 2 and one person would be left on floor 7; adding 19 first, (41 + 19) / 20 = 60 / 20 = 3. If it was already exact, nothing changes: (40 + 19) / 20 = 59 / 20 = 2. - **
floors = leg * (2 * trips - 1)**: with one trip, the leg once; with three, go, return, go, return, go = five times. - **
MIN(g.people, MAX_PEOPLE)** is the number of people in the fullest trip of that group: if there are 8, all 8 go; if there are 41, at most 20. - **
LOG** comes fromlogger.h, so the.cneither knows nor cares whether traces are on.__LINE__says 20 and 21, the lines ofsimulator.cwhere it is used, even though the macro is written in another file.
The sums for each group:
| Group | People | trips | leg | floors | In the fullest trip |
|---|---|---|---|---|---|
| 1 | 8 | (8 + 19) / 20 = 1 | |6 − 0| = 6 | 6 · 1 = 6 | 8 |
| 2 | 25 | (25 + 19) / 20 = 2 | 3 | 3 · 3 = 9 | 20 |
| 3 | 12 | 1 | |−1 − 4| = 5 | 5 | 12 |
| 4 | 41 | 3 | 7 | 7 · 5 = 35 | 20 |
| 5 | 3 | 1 | |5 − (−2)| = 7 | 7 | 3 |
Total: 1 + 2 + 1 + 3 + 1 = 8 trips and 6 + 9 + 5 + 35 + 7 = 62 floors; the fullest trip carries 20 people, 20 · 75 = 1500 kg. Group 4 is the most interesting one, and the drawing shows it in full:
Watch it step by step in Python Tutor
Python Tutor runs the code line by line and draws the variables. Choose C and paste this version of snippet 1, which stores each step in a variable so you can watch the bug live:
#include <stdio.h>
#define MAX_LOAD 1500
#define TOO_BIG(v) v >= MAX_LOAD /* no parentheses */
int main(void) {
int load = 300;
int negated = !load; /* what the ! really calculates */
int result = !TOO_BIG(load); /* !load >= 1500 */
int good = !(load >= MAX_LOAD); /* what we wanted */
printf("negated = %d, result = %d, good = %d\n", negated, result, good);
return 0;
}
Its output:
negated = 0, result = 0, good = 1
What to watch step by step:
- **
loadis 300** and it is the only thing that exists at the start; neitherMAX_LOADnor the macro appear as variables. - **When
negatedis created**, it is 0:!300is 0, because any non-zero number counts as “true” and negating it gives “false”. - **When
resultis created**, it is also 0. That is the macro without parentheses:!load >= 1500isnegated >= 1500, that is,0 >= 1500. - **When
goodis created**, it is 1: with the parentheses in the right place,300 >= 1500is 0 and negating it gives 1. That is the difference between “too much weight” and “can go up”.
Cheat sheet: the preprocessor in C
The essentials in 30 seconds: the preprocessor works before the compiler and only with text. #include pastes files, #define replaces symbols (and macros with parameters), #ifdef/#if keep or remove code, and gcc -D defines symbols from outside. In macros, parentheses around each parameter and around everything; never a ; at the end nor arguments with side effects.
Directives:
| I want… | Code | Result |
|---|---|---|
| A system library | #include <stdio.h> | stdio.h is pasted in |
| My own header | #include "lift.h" | Searched for in my folder |
| A constant | #define MAX_LOAD 1500 | MAX_LOAD → 1500 |
| A symbol with no value | #define WITH_SCREEN | It just exists |
| A macro | #define WEIGHT(n) ((n) * 75) | WEIGHT(a + b) → ((a + b) * 75) |
| To remove a symbol | #undef WITH_SCREEN | It stops existing |
| A multi-line macro | \ at the end of each line | A single definition |
Conditional compilation:
| I want… | Code | Result |
|---|---|---|
Traces only with -DDEBUG | #ifdef DEBUG … #endif | Without -DDEBUG, they disappear |
| A default value you can change | #ifndef N / #define N 10 / #endif | -DN=20 changes it |
| To pick a version by number | #if MODEL == 1 … #elif … #else … #endif | Only one branch is left |
| To switch off a block | #if 0 … #endif | It never reaches the compiler |
| To block a compilation | #error "message" | gcc stops with that message |
| To define from the terminal | gcc -DDEBUG / gcc -DMODEL=2 | Like a #define on line 1 |
Templates from the exercises:
| Template | Code | Result |
|---|---|---|
| Weight of a group | #define GROUP_WEIGHT(a, n) ((a) * 75 + (n) * 35) | GROUP_WEIGHT(4, 2) → 370 |
| Fits or not | #define FITS(kg) ((kg) <= MAX_LOAD) | FITS(1500) → 1 |
| Minimum and maximum | #define MIN(a, b) ((a) < (b) ? (a) : (b)) | MIN(12, 8) → 8 |
| Clamp to a range | #define CLAMP(x, lo, hi) MIN(MAX((x), (lo)), (hi)) | CLAMP(-4, -2, 8) → −2 |
| Absolute value | #define ABS(x) ((x) < 0 ? -(x) : (x)) | ABS(-10) → 10 |
| Division rounding up | #define DIV_UP(a, b) (((a) + (b) - 1) / (b)) | DIV_UP(41, 20) → 3 |
| Trace with location | printf("[%s:%d] ...", __FILE__, __LINE__, ...) | [simulator.c:20] ... |
.h guard | #ifndef LIFT_H / #define LIFT_H / … / #endif | Included only once |
Symptom / likely cause:
| Symptom | Likely cause |
|---|---|
| A calculation with a macro gives a strange number | Missing parentheses around a parameter or around everything |
A !MACRO(x) is never (or always) true | The macro is missing its outer parentheses |
statement with no effect on a line with a #define | ; at the end of the #define |
| A variable advances twice | You passed i++ to a macro that repeats the parameter |
Traces appear even though DEBUG is 0 | You use #ifdef DEBUG instead of #if DEBUG |
redefinition of 'struct …' | A .h without a guard included twice |
Lots of undeclared in a row | Look at the first error: an #error or an #include that fails |
Before you submit, ask yourself:
- Do all my macros have parentheses around each parameter and around the expression?
- Does any
#defineend in;? - Do my traces disappear when I compile without
-DDEBUG(with no side effects inside them)? - Do all my
.hfiles have their#ifndef … #define … #endifguard?
Did this article help you? Here is how you can support SergioLearns →
