The preprocessor in C — practice with a lift panel, a debug logger and three models in one file (and the submission full of traces)
The first time I submitted a C assignment to the automatic marker I got a zero. The program did the sums correctly, but I had scattered printf("got here, total = %d\n", total); all over the place to track down a bug, and I forgot to remove them. The marker compared my output with the expected one, line by line, and mine had thirty extra lines. The lesson cost me a mark: debug traces must be able to switch on and off without touching the code. And in C, that is done with the preprocessor.
In this practice you will put to work what you saw in the preprocessor theory with three programs for the same lift, all of medium level: a configuration panel with its constants and macros in a config.h, a debug logger that only exists when you compile with -DDEBUG (the one that would have saved me from that zero) and three lift models coming out of a single file. The data looks like real data: trips to floors that do not exist, groups that are 50 kg over the limit and a house move that does not fit.
Table of Contents
Before you start
Make sure you are clear on:
- That
#definereplaces text: it does not create variables or do sums. - That macros with parameters need parentheses around each parameter and around everything.
- The difference between
#ifdef(does it exist?) and#if(what is it worth?), and thatgcc -DNAMEdefines a symbol from outside. - How an include guard (
#ifndef FILE_H) works in a.h.
If any of these sounds shaky, go back over the theory before carrying on.
Program 1 has two files, config.h and panel.c. Save them in the same folder and compile only the .c: gcc -Wall -std=c11 panel.c -o panel. The #include "config.h" with quotes makes the preprocessor look for it there. Programs 2 and 3 are compiled several times with different options; I give you the command with each output.
Program 1: The configuration panel
The lift in a building with two basements and eight floors receives a list of six trips: which floor each one goes to, how many people get in and how many extra kilos they carry (suitcases, shopping trolleys, a pallet…). The program has to decide whether each trip is valid. What makes it tricky is that **the rules are not in the .c**: all the constants and checks live in a config.h, so moving the lift to another building means editing a single file.
The idea
First we write config.h with three kinds of things: constants (maximum load, average weight, lowest and highest floor, number of trips), a calculated constant (MAX_PEOPLE, which comes from dividing the load by the average weight) and macros that ask (OVERLOADED, VALID_FLOOR) or calculate (PEOPLE_WEIGHT). Then panel.c goes through the trips and, for each one, works out the load and checks the floor first and then the weight: if the floor does not exist, the weight does not matter.
Before coding, think about how each macro will be used. VALID_FLOOR is going to sit behind a !, and MIN_FLOOR is a negative number. Both force you to add parentheses where they seem unnecessary.
The code
First, the configuration:
/* config.h: the lift configuration in one place */ #ifndef CONFIG_H #define CONFIG_H #define MAX_LOAD 1500 /* kg */ #define AVG_WEIGHT 75 /* kg per person */ #define MIN_FLOOR (-2) /* in parentheses: it is a negative number */ #define MAX_FLOOR 8 #define NUM_TRIPS 6 /* Calculated constant: the preprocessor copies the sum, the compiler does it */ #define MAX_PEOPLE (MAX_LOAD / AVG_WEIGHT) /* Macros with parameters: parentheses around each parameter and around everything */ #define PEOPLE_WEIGHT(n) ((n) * AVG_WEIGHT) #define OVERLOADED(kg) ((kg) > MAX_LOAD) #define VALID_FLOOR(p) ((p) >= MIN_FLOOR && (p) <= MAX_FLOOR) #endif /* CONFIG_H */
And the program that uses it:
#include <stdio.h>
#include "config.h" /* all the constants and macros come from here */
int main(void) {
/* Each trip: destination floor, people and extra kg (suitcases, trolleys...) */
int destination[NUM_TRIPS] = {3, 9, -1, 5, -3, 0};
int people[NUM_TRIPS] = {4, 2, 19, 6, 1, 12};
int extra[NUM_TRIPS] = {0, 40, 0, 120, 0, 650};
int accepted = 0;
printf("Capacity: %d kg (%d people)\n", MAX_LOAD, MAX_PEOPLE);
printf("Floors from %d to %d\n\n", MIN_FLOOR, MAX_FLOOR);
for (int i = 0; i < NUM_TRIPS; i++) {
int load = PEOPLE_WEIGHT(people[i]) + extra[i];
printf("Trip %d -> floor %2d, %2d ppl., %4d kg: ",
i + 1, destination[i], people[i], load);
/* The ! works because VALID_FLOOR has outer parentheses */
if (!VALID_FLOOR(destination[i])) {
printf("REJECTED (floor out of range)\n");
} else if (OVERLOADED(load)) {
printf("REJECTED (over by %d kg)\n", load - MAX_LOAD);
} else {
printf("OK (spare: %d kg)\n", MAX_LOAD - load);
accepted++;
}
}
printf("\nAccepted: %d of %d\n", accepted, NUM_TRIPS);
return 0;
}
Output
With gcc -Wall -std=c11 panel.c -o panel and ./panel:
Capacity: 1500 kg (20 people) Floors from -2 to 8 Trip 1 -> floor 3, 4 ppl., 300 kg: OK (spare: 1200 kg) Trip 2 -> floor 9, 2 ppl., 190 kg: REJECTED (floor out of range) Trip 3 -> floor -1, 19 ppl., 1425 kg: OK (spare: 75 kg) Trip 4 -> floor 5, 6 ppl., 570 kg: OK (spare: 930 kg) Trip 5 -> floor -3, 1 ppl., 75 kg: REJECTED (floor out of range) Trip 6 -> floor 0, 12 ppl., 1550 kg: REJECTED (over by 50 kg) Accepted: 3 of 6
Let us check the sums by hand. MAX_PEOPLE is 1500 / 75 = 20 people. Trip by trip:
| Trip | Floor | Load | Valid floor? | Over 1500? | Result |
|---|---|---|---|---|---|
| 1 | 3 | 4 · 75 + 0 = 300 | Yes | No | OK, 1500 − 300 = 1200 spare |
| 2 | 9 | 2 · 75 + 40 = 190 | No (9 > 8) | Not checked | REJECTED (floor) |
| 3 | −1 | 19 · 75 + 0 = 1425 | Yes | No | OK, 75 spare |
| 4 | 5 | 6 · 75 + 120 = 570 | Yes | No | OK, 930 spare |
| 5 | −3 | 1 · 75 + 0 = 75 | No (−3 < −2) | Not checked | REJECTED (floor) |
| 6 | 0 | 12 · 75 + 650 = 1550 | Yes | Yes | REJECTED, over by 50 |
Three accepted out of six. Trip 3 is the tightest: 19 people fit with 75 kg to spare, exactly the weight of one more. The drawing puts the six loads against the 1500 kg line:
Trip 2 is the one that proves the order of the ifs matters: it weighs very little, but it is rejected because of the floor before the program even looks at the weight.
Now, a zoom into the line that hides the most work, the floor check:
/* The ! works because VALID_FLOOR has outer parentheses */
if (!VALID_FLOOR(destination[i])) {
printf("REJECTED (floor out of range)\n");
After the preprocessor, !VALID_FLOOR(destination[i]) becomes:
if (!((destination[i]) >= (-2) && (destination[i]) <= 8)) {
The macro’s outer parentheses make the ! negate the whole condition. Without them, the ! would stick only to destination[i] (you will see it blow up in the exercises). And (-2) is in parentheses because -2 is also an expression (the minus operator applied to 2): the habit of putting every #define that is not a plain number in parentheses saves you from thinking about precedence every time you use it.
Questions you might ask
**Why does MAX_PEOPLE have parentheses if it has no parameters?** Because it is a calculation. MAX_LOAD / AVG_WEIGHT works on its own, but if someone writes 100 % MAX_PEOPLE without parentheses it would become 100 % 1500 / 75, which C calculates as (100 % 1500) / 75 = 1, instead of 100 % 20 = 0. Any #define that is an expression goes in parentheses.
Who does the division 1500 / 75, the preprocessor or the program? Neither, in the sense you might imagine: the preprocessor just copies (1500 / 75) into the code, and the compiler, seeing two constants, works it out at compile time. The program already has the 20 written in.
**Why are the arrays declared with NUM_TRIPS and the loop too?** So that the size lives in one place. If tomorrow there are eight trips, you change NUM_TRIPS in config.h and the array and the loop grow together. You cannot do that with a const int in an array like this in C.
**What happens if I include config.h twice?** Nothing: the #ifndef CONFIG_H guard makes the second inclusion skip everything. Try it by adding another #include "config.h" line and you will see it still compiles.
**Why does OVERLOADED use > and not >=?** Because exactly 1500 kg is the maximum allowed load, not an excess. With >=, a group of 20 people (exactly 1500) would be rejected.
Writing #define VALID_FLOOR(p) p >= MIN_FLOOR && p <= MAX_FLOOR, without the parentheses. With !VALID_FLOOR(destination[i]) the ! only applies to the first destination[i], and the trip to floor 9 would pass as valid.
If you want to see the if as the compiler gets it, gcc -E -P panel.c | grep "if (!" shows it without the 800 lines of stdio.h.
Try it yourself
- Add to
config.ha macroSPARE_PEOPLE(kg)that says how many more people fit with that load, and use it in the OK line (“1200 kg spare, 16 more people fit”). - Change the building: no basements (
MIN_FLOOR 0) and 12 floors. Which trips change result? Do it touching onlyconfig.h.
Program 2: The debug logger
Now the lift has to check two loads: a group of neighbours and a house move with pallets. The program adds up the weights and says whether they can go up. What makes it tricky is that we want debug traces to see the sum step by step, but we want them to disappear on their own in the version we submit. Exactly what I was missing.
The idea
We define a macro LOG(msg, value) with two versions inside an #ifdef DEBUG: if you compile with -DDEBUG, LOG is a printf that shows the file, line, function and value; if not, LOG becomes ((void)0), an expression that does nothing. The rest of the code uses LOG normally and never changes: what changes is the compile command.
Before coding, think about what LOG(...) should become when it is off. If you leave it empty (#define LOG(msg, value)), the line ends up as a lone ;, which compiles, but ((void)0) is safer: it is still an expression and works anywhere a printf would.
The code
#include <stdio.h>
#define MAX_LOAD 1500
#ifdef DEBUG
/* With -DDEBUG: each trace shows file, line, function and the value */
#define LOG(msg, value) \
printf(" [%s:%d %s] %s = %d\n", __FILE__, __LINE__, __func__, msg, value)
#else
/* Without DEBUG: LOG becomes an expression that does nothing */
#define LOG(msg, value) ((void)0)
#endif
int total_load(const int weights[], int n) {
int total = 0;
for (int i = 0; i < n; i++) {
total += weights[i];
LOG("running total", total); /* one trace per person or pallet */
}
return total;
}
void check(const char *group, const int weights[], int n) {
LOG("items", n);
int total = total_load(weights, n);
printf("%s: %d kg -> %s\n", group, total,
total > MAX_LOAD ? "OVERLOAD" : "can go up");
}
int main(void) {
int neighbours[] = {82, 64, 91, 70};
int moving[] = {650, 420, 480};
/* sizeof the array divided by sizeof one element = number of elements */
check("Neighbours", neighbours, sizeof neighbours / sizeof neighbours[0]);
check("Moving", moving, sizeof moving / sizeof moving[0]);
return 0;
}
Output
Compiled normally, gcc -Wall -std=c11 logger.c -o logger:
Neighbours: 307 kg -> can go up Moving: 1550 kg -> OVERLOAD
Compiled with traces, gcc -Wall -std=c11 -DDEBUG logger.c -o logger:
[logger.c:24 check] items = 4 [logger.c:18 total_load] running total = 82 [logger.c:18 total_load] running total = 146 [logger.c:18 total_load] running total = 237 [logger.c:18 total_load] running total = 307 Neighbours: 307 kg -> can go up [logger.c:24 check] items = 3 [logger.c:18 total_load] running total = 650 [logger.c:18 total_load] running total = 1070 [logger.c:18 total_load] running total = 1550 Moving: 1550 kg -> OVERLOAD
Same file, two programs. The normal version is the one you would submit: two lines, not one more. The -DDEBUG version shows every step of the sum. Let us check: 82 + 64 = 146, + 91 = 237, + 70 = 307 kg, which is under 1500. The house move: 650 + 420 = 1070, + 480 = 1550 kg, which is 50 over. The drawing sums up what -DDEBUG does to the code:
The trace of total_load loop by loop for the neighbours:
Loop (i) | weights[i] | total after | Trace with -DDEBUG |
|---|---|---|---|
| 0 | 82 | 82 | running total = 82 |
| 1 | 64 | 146 | running total = 146 |
| 2 | 91 | 237 | running total = 237 |
| 3 | 70 | 307 | running total = 307 |
A zoom into the part that does the magic, the macro with its two versions:
#ifdef DEBUG
/* With -DDEBUG: each trace shows file, line, function and the value */
#define LOG(msg, value) \
printf(" [%s:%d %s] %s = %d\n", __FILE__, __LINE__, __func__, msg, value)
#else
/* Without DEBUG: LOG becomes an expression that does nothing */
#define LOG(msg, value) ((void)0)
#endif
Notice three details. The \ at the end of line 7 tells the preprocessor that the definition continues on the next line (a directive takes a single logical line). __LINE__ comes out as 18 and 24 in the traces, which are the lines where LOG is used, not line 8 where it is defined. And __func__ says which function we are in, so the trace shows that items comes from check and the totals come from total_load.
Questions you might ask
**Why not use a normal if (debug) with a variable?** Because the traces would still be inside the program: they take up space, waste time checking the if and changing the variable is enough to make them appear. With #ifdef, in the normal version the trace printfs do not even exist: the compiler never sees them.
**What is the (void) in ((void)0) for?** To make it clear that the expression returns nothing useful and so that no “unused value” warning appears. It is the standard way to write “nothing here” where C expects an expression.
**Why is the number of items worked out with sizeof?** sizeof neighbours is the size of the array in bytes (4 integers · 4 bytes = 16) and sizeof neighbours[0] is the size of one element (4): 16 / 4 = 4. That way, if you add a neighbour to the array, the number updates itself. Careful: it only works with the original array, not with a pointer inside a function.
Can I have several trace levels? Yes: instead of #ifdef DEBUG, use #if LEVEL >= 2 and compile with -DLEVEL=2. That is what program 3 does with the models.
Putting a side effect inside LOG, for example LOG("next", i++). In the normal version, LOG disappears and the i++ with it: the program behaves differently with and without -DDEBUG. Inside a trace, only things that can be read without changing anything.
Many real projects use the opposite convention: NDEBUG (“no debug”) to switch checks off. The assert macro from <assert.h>, for example, disappears if you compile with -DNDEBUG.
Try it yourself
- Add a trace in
mainthat shows the total of both loads together and check that it only appears with-DDEBUG. - Change the
#ifdef DEBUGto#if LEVEL >= 1and make-DLEVEL=2also print a trace for eachcheck, with the group name.
Program 3: Three models in one file
The lift company makes three models: a basic one with four floors, a residential one with ten floors and a screen, and a hospital one with twelve floors, a screen and a stretcher mode that keeps the doors open longer. The program shows the model’s spec sheet. What makes it tricky is that we do not want three almost identical files: **a single panels.c has to give the three programs**, and if someone asks for a model that does not exist, it must not compile.
The idea
The model number comes from outside with -DMODEL=2. An #ifndef MODEL sets 1 as the default. Then a chain #if MODEL == 1 … #elif MODEL == 2 … #elif MODEL == 3 defines each model’s constants and, only in the models that have them, the valueless symbols WITH_SCREEN and WITH_STRETCHER. An #else with #error stops the compilation if the number is anything else. In main, #ifdef WITH_SCREEN and #ifdef WITH_STRETCHER decide which lines exist.
Before coding, think about which symbols need a value (FLOORS, MAX_LOAD: they go into sums) and which just need to exist (WITH_SCREEN: they are only tested with #ifdef).
The code
#include <stdio.h>
#ifndef MODEL
#define MODEL 1 /* without -DMODEL=..., the basic one is built */
#endif
#if MODEL == 1
#define MODEL_NAME "Basic"
#define FLOORS 4
#define MAX_LOAD 630
#elif MODEL == 2
#define MODEL_NAME "Residential"
#define FLOORS 10
#define MAX_LOAD 1000
#define WITH_SCREEN
#elif MODEL == 3
#define MODEL_NAME "Hospital"
#define FLOORS 12
#define MAX_LOAD 2500
#define WITH_SCREEN
#define WITH_STRETCHER
#else
#error "MODEL must be 1, 2 or 3"
#endif
#define MAX_PEOPLE (MAX_LOAD / 75) /* integer division: rounds down */
int main(void) {
printf("Model %d (%s)\n", MODEL, MODEL_NAME);
printf("Floors: 0-%d | Load: %d kg | People: %d\n",
FLOORS - 1, MAX_LOAD, MAX_PEOPLE);
#ifdef WITH_SCREEN
printf("Screen: shows the floor and the load\n");
#else
printf("No screen: buttons only\n");
#endif
#ifdef WITH_STRETCHER
printf("Stretcher mode: doors open 20 s\n");
#endif
return 0;
}
Output
Without -D, gcc -Wall -std=c11 panels.c -o panels (model 1 by default):
Model 1 (Basic) Floors: 0-3 | Load: 630 kg | People: 8 No screen: buttons only
With gcc -Wall -std=c11 -DMODEL=2 panels.c -o panels:
Model 2 (Residential) Floors: 0-9 | Load: 1000 kg | People: 13 Screen: shows the floor and the load
With -DMODEL=3:
Model 3 (Hospital) Floors: 0-11 | Load: 2500 kg | People: 33 Screen: shows the floor and the load Stretcher mode: doors open 20 s
And with -DMODEL=4, which does not exist, the program is not even built. This is what gcc says (without the column number):
panels.c:23: error: #error "MODEL must be 1, 2 or 3" panels.c: In function 'main': panels.c:29: error: 'MODEL_NAME' undeclared (first use in this function) panels.c:29: note: each undeclared identifier is reported only once for each function it appears in panels.c:31: error: 'FLOORS' undeclared (first use in this function) panels.c:31: error: 'MAX_LOAD' undeclared (first use in this function)
Let us check the spec sheets: the basic one has 4 floors, numbered 0 to 3 (hence FLOORS - 1), and 630 / 75 = 8.4 → 8 people, because integer division drops the decimals. The residential one: 1000 / 75 = 13.3 → 13. The hospital one: 2500 / 75 = 33.3 → 33. The drawing shows the three programs that come out of the same file:
In the model 4 error, the first line is our #error (line 23) and the rest are consequences: since no branch defined MODEL_NAME, FLOORS or MAX_LOAD, the compiler finds those names undefined in main. When you see a cascade of errors like this, always read the first one.
A zoom into the model chain, which is the new part:
#if MODEL == 1 #define MODEL_NAME "Basic" #define FLOORS 4 #define MAX_LOAD 630 #elif MODEL == 2 #define MODEL_NAME "Residential" #define FLOORS 10 #define MAX_LOAD 1000 #define WITH_SCREEN #elif MODEL == 3 #define MODEL_NAME "Hospital" #define FLOORS 12 #define MAX_LOAD 2500 #define WITH_SCREEN #define WITH_STRETCHER #else #error "MODEL must be 1, 2 or 3" #endif
Each branch defines its own set of constants, and only one reaches the compiler. #elif is the preprocessor’s else if: it is only evaluated if the previous branches were false. And notice that WITH_SCREEN is defined in two branches: that is fine, because only one of them exists in each compilation.
Questions you might ask
**Why #if MODEL == 2 and not #ifdef MODEL?** Because MODEL always exists (if you do not pass it, the #ifndef sets it to 1). What tells one model from another is its value, and only #if looks at that.
**What exactly does -DMODEL=2 do?** The same as writing #define MODEL 2 on the first line of the file. Without the =2, -DMODEL would define it with the value 1.
**Why is a run-time if (model == 2) not enough?** It would work, but the basic model’s program would carry the hospital code inside without ever using it. With the preprocessor, each model carries only its own code, and #error prevents building one that does not exist: the mistake shows up at compile time, not when a customer presses a button.
**Why does the #ifndef MODEL go before the chain?** Because the chain compares MODEL with numbers. If it were not defined, the preprocessor would treat it as 0 in the #if and we would end up in the #error without knowing why.
Writing #if MODEL = 2 with a single =. In #if, just like in C, comparison is ==; with one = the preprocessor gives an error.
gcc -dM -E panels.c | grep WITH_ lists the WITH_… symbols that end up defined for a model. With -DMODEL=3 you will see both.
Try it yourself
- Add a model 4, “Freight”, with 6 floors and 4000 kg, with a symbol
GOODS_ONLYthat replaces the people line with “Not for passengers”. - Add an
#if MAX_LOAD > 2000inmainthat prints “Needs a reinforced annual inspection”. Which models print it?
Common mistakes in this practice
- **Macros without outer parentheses behind a
!.**!VALID_FLOOR(p)only works if the whole macro is in parentheses. - **Forgetting the guard in
config.h.** It does not fail today because it is included once; the day another.hincludes it, it will. - **Side effects inside
LOG.** Whatever goes inside a trace disappears in the normal version. - **Confusing
#ifdefand#if.**#ifdef MODELis true for any model; to pick one you need#if MODEL == n. - **A
\followed by spaces** in a multi-line macro. The\has to be the very last thing on the line, or the definition gets cut. - Reading the last error in a cascade. With
#error(or any failure), the one that matters is the first.
Watch it step by step in Python Tutor
Python Tutor runs the program line by line and draws the variables. Choose C in the drop-down. Since you cannot compile with -DDEBUG there, the program defines it on its first line:
#include <stdio.h>
#define DEBUG /* Python Tutor has no -D: we define it here */
#define MAX_LOAD 1500
#define PEOPLE_WEIGHT(n) ((n) * 75)
#define OVERLOADED(kg) ((kg) > MAX_LOAD)
int main(void) {
int people[3] = {4, 19, 12};
int extra[3] = {0, 0, 650};
for (int i = 0; i < 3; i++) {
int load = PEOPLE_WEIGHT(people[i]) + extra[i];
#ifdef DEBUG
printf("trip %d: %d kg\n", i + 1, load);
#endif
if (OVERLOADED(load))
printf(" rejected\n");
}
return 0;
}
Its output:
trip 1: 300 kg trip 2: 1425 kg trip 3: 1550 kg rejected
What to watch step by step:
- **The
peopleandextraarrays** appear with their three values before the loop starts; the macros do not appear anywhere. - **In each loop, when
loadis created**, check the sum: 4 · 75 = 300, 19 · 75 = 1425 and 12 · 75 + 650 = 1550. - **The debug
printfline** runs in all three loops becauseDEBUGis defined. Delete the#define DEBUG, visualise again and you will see that line is no longer a step: the preprocessor has removed it. - In the third loop,
OVERLOADED(load)is true (1550 > 1500) and the “rejected” line runs; in the other two, the program jumps straight to the next loop.
Summary
- A
config.hwith a guard gathers all the program’s constants and macros: changing building means changing one file. - Macros that are negated or combined need outer parentheses, and negative numbers go in parentheses.
- A
LOGmacro with two versions inside#ifdef DEBUGgives **traces that switch on with-DDEBUG** without touching the code. __FILE__,__LINE__and__func__say where each trace was written, and they are replaced where the macro is used.- With
#if MODEL == n,#elifandgcc -DMODEL=n, one file gives several programs, and#errorprevents compiling an impossible one. gcc -E(andgcc -dM -E) are the way to check what the preprocessor has done.
Checklist: have you practised it?
- Have you run the three programs and checked the outputs?
- Have you compiled
logger.cwith and without-DDEBUGand seen the difference? - Can you explain why
!VALID_FLOOR(9)works and what would happen without the parentheses? - Have you tried
panels.cwith a model that does not exist and read the first error? - Have you looked at a program with
gcc -E -P? - Have you tried the “Try it yourself” tasks without looking at the theory?
If you answered yes to almost everything, you are ready for the preprocessor exercises, where I no longer give you the finished code. See you there!
Next step: exercisesThe preprocessor in C — exercises with lift capacity, a floor limiter and a simulator with traces (and the DEBUG 0 that switched nothing off)Did this article help you? Here is how you can support SergioLearns →
