The preprocessor, multi-file programs and make · about 50 min

The preprocessor: include, define and macros

Why this matters

Every C file you have written starts with #include, and real C code is full of #define: constants, small helper macros, debug logging, platform switches. The preprocessor is powerful precisely because it is dumb: it rewrites text before the compiler sees it, with no knowledge of types or scope. Used carefully it removes repetition. Used carelessly it produces bugs that are invisible in the source, because the code the compiler actually compiles is not the code you wrote. This lesson shows exactly what the preprocessor does so you can always see through it.

A text processor in front of the compiler

You met the pipeline in the first module: preprocess, compile, assemble, link. The preprocessor (stage 1) handles directives, lines whose first non-blank character is #. It works on tokens (identifiers, numbers, string literals, punctuation), not on C semantics. It does not know what a type or a function is.

You can always see its output with gcc -E. Adding -P drops the line-marker noise:

$ gcc -E -P main.c | tail -20

When a macro confuses you, look at the expansion. That habit resolves most macro mysteries in seconds.

#include: pasting files

#include replaces the directive with the entire contents of the named file, recursively. There are two spellings:

So use <> for the standard library and system headers, "" for your own project's headers. You can add directories to the search with -I, as in gcc -Iinclude main.c. To see the search list your compiler uses:

$ echo | gcc -xc -E -v - 2>&1 | sed -n '/search starts/,/End of/p'
#include "..." search starts here:
#include <...> search starts here:
 /usr/lib/gcc/x86_64-linux-gnu/14/include
 /usr/local/include
 /usr/include/x86_64-linux-gnu
 /usr/include
End of search list.

#include is not an import in the Python sense. It is textual pasting, which is why the same header pasted twice can cause "redefinition" errors (the fix, include guards, is in the next two lessons) and why a header full of code makes every file that includes it bigger.

Object-like macros: named constants

#define BUFFER_SIZE 4096
#define PROGRAM_NAME "wc-lite"
#define PI 3.14159265358979

char buf[BUFFER_SIZE];

After #define NAME replacement, every later occurrence of the token NAME is replaced by the replacement tokens. Not inside string literals ("BUFFER_SIZE" stays as it is), and not parts of other identifiers (BUFFER_SIZE_MAX is a different token). By convention macro names are in UPPER_CASE, so readers know text substitution is going on. #undef NAME removes a definition.

Defining the same name twice with different text gives a warning:

redef.c:2:9: warning: "X" redefined
    2 | #define X 2
      |         ^
redef.c:1:9: note: this is the location of the previous definition

Alternatives for constants are const variables and enum constants, which have types and obey scope. Macros are still universal for array sizes in older code and for values that must also work in #if (next lesson).

Function-like macros

If the macro name is followed immediately by (, with no space, it takes parameters:

#define SQUARE(x) ((x) * (x))

SQUARE(n + 1) is replaced by ((n + 1) * (n + 1)). The argument's tokens are substituted for each parameter, and that is all. There is no evaluation, no type check, no function call. With a space before the parenthesis, #define SQUARE (x) ((x)*(x)) defines an object-like macro whose text begins with (x), and you get confusing errors such as 'x' undeclared.

Why all the parentheses

The parentheses look excessive until you see what happens without them:

#include <stdio.h>

#define BAD_SQUARE(x) x * x
#define SQUARE(x) ((x) * (x))
#define BAD_MAX(a, b) a > b ? a : b
#define MAX(a, b) ((a) > (b) ? (a) : (b))

int main(void)
{
    printf("BAD_SQUARE(1 + 2) = %d\n", BAD_SQUARE(1 + 2));
    printf("SQUARE(1 + 2)     = %d\n", SQUARE(1 + 2));
    printf("100 / BAD_SQUARE(5) = %d\n", 100 / BAD_SQUARE(5));
    printf("100 / SQUARE(5)     = %d\n", 100 / SQUARE(5));
    printf("2 * BAD_MAX(3, 4) = %d\n", 2 * BAD_MAX(3, 4));
    printf("2 * MAX(3, 4)     = %d\n", 2 * MAX(3, 4));
    int i = 5, j = 3;
    int m = MAX(i++, j);
    printf("m = %d, i = %d\n", m, i);
    return 0;
}
BAD_SQUARE(1 + 2) = 5
SQUARE(1 + 2)     = 9
100 / BAD_SQUARE(5) = 100
100 / SQUARE(5)     = 4
2 * BAD_MAX(3, 4) = 3
2 * MAX(3, 4)     = 8
m = 6, i = 7

gcc -E -P shows why:

    printf("BAD_SQUARE(1 + 2) = %d\n", 1 + 2 * 1 + 2);
    printf("100 / BAD_SQUARE(5) = %d\n", 100 / 5 * 5);
    printf("2 * BAD_MAX(3, 4) = %d\n", 2 * 3 > 4 ? 3 : 4);
    int m = ((i++) > (j) ? (i++) : (j));

Two rules follow:

  1. Parenthesize every parameter in the body, so an argument like 1 + 2 stays one unit.
  2. Parenthesize the whole body, so the surrounding expression (100 / ..., 2 * ...) cannot reach inside.

Double evaluation

The last line shows a problem parentheses cannot fix: MAX mentions a twice, so i++ runs twice. Any argument with a side effect (i++, a function call that prints or allocates, getchar()) is dangerous in a macro that uses its parameter more than once. A real function evaluates each argument exactly once. Modern C code therefore prefers static inline functions for anything non-trivial:

static inline int max_int(int a, int b) { return a > b ? a : b; }

The compiler inlines it just as efficiently, and you get type checking and normal evaluation.

Macros that are statements: do { } while (0)

A macro that expands to several statements breaks under an if without braces:

#define LOG_BAD(msg) printf("log: "); printf("%s\n", msg)

if (!quiet)
    LOG_BAD("starting");   /* only the first printf is inside the if! */

With quiet set to 1, this prints starting: the second printf is not guarded. gcc notices:

dw.c:3:22: warning: macro expands to multiple statements [-Wmultistatement-macros]
dw.c:9:5: note: some parts of macro expansion are not guarded by this 'if' clause

Wrapping the body in braces is not enough, because the caller's semicolon after } ends the if, and a following else becomes an error: 'else' without a previous 'if'. The standard idiom is a loop that runs exactly once:

#define LOG(msg) do { printf("log: "); printf("%s\n", msg); } while (0)

do { ... } while (0) is a single statement that requires a semicolon after it, so LOG(x); behaves exactly like a function call everywhere.

A multi-line macro continues with a backslash as the very last character on each line:

#define LOG(msg)                 \
    do {                         \
        printf("log: ");         \
        printf("%s\n", msg);     \
    } while (0)

The # and ## operators

Inside a function-like macro, two operators work on tokens:

#include <stdio.h>

#define SHOW_INT(expr) printf("%s = %d\n", #expr, (expr))
#define WHERE() printf("%s:%d in %s\n", __FILE__, __LINE__, __func__)
#define MAKE_GETTER(field) int get_##field(void) { return field; }

int width = 640, height = 480;
MAKE_GETTER(width)
MAKE_GETTER(height)

int main(void)
{
    SHOW_INT(2 + 3 * 4);
    SHOW_INT(get_width() * get_height());
    WHERE();
    return 0;
}
2 + 3 * 4 = 14
get_width() * get_height() = 307200
strs.c:15 in main

MAKE_GETTER(width) generated a whole function named get_width. __FILE__ and __LINE__ are predefined macros that expand to the current file name and line number, which is how assert reports where it failed. (__func__ looks similar but is not a macro: it is a variable the compiler provides inside every function.) Other predefined macros include __DATE__, __TIME__ and __STDC_VERSION__.

Common mistakes

Recap

Going further: read the GNU cpp manual's "Macro Pitfalls" section (info cpp on a machine with the documentation installed, or online). Then run gcc -E -P on a file that includes <stdio.h> and search the output for getc to see how real headers use macros.

Exercises

Exercise 1 · quiz

Predict the expansion

#define SQUARE(x) x * x

printf("%d\n", SQUARE(1 + 2));

What does this print?

Answer
  • 5

The preprocessor pastes the argument text in place of x without adding anything: SQUARE(1 + 2) becomes 1 + 2 * 1 + 2, and multiplication binds tighter, so the result is 1 + 2 + 2 = 5. The fix is to parenthesize every use of a parameter and the whole body: #define SQUARE(x) ((x) * (x)).

Exercise 2 · quiz

Arguments evaluated twice

#define MAX(a, b) ((a) > (b) ? (a) : (b))

int i = 5, j = 3;
int m = MAX(i++, j);
printf("m = %d, i = %d\n", m, i);

What does this print?

Answer
  • m = 6, i = 7

The expansion is ((i++) > (j) ? (i++) : (j)). The condition compares 5 > 3 and increments i to 6; the chosen branch evaluates i++ again, yielding 6 and leaving i at 7. A function max(i++, j) would have evaluated i++ exactly once. Never pass expressions with side effects to a macro that may use its argument more than once; this is a main reason to prefer functions (or static inline functions) over function-like macros.

Exercise 3 · write code

Write the macros properly

Define three function-like macros in main.c, written so they work correctly with any expression as an argument and inside any larger expression:

Keep the given main, which must print exactly:

9 4 2 5

The checker also includes your file into a test program that uses the macros in tricky ways, such as 100 / SQUARE(2 + 3) and -MIN(3, 4).

File: main.c

Starting point

#include <stdio.h>

#define SQUARE(x) x * x
#define MIN(a, b) a < b ? a : b
#define ARRAY_LEN(arr) sizeof arr / sizeof arr[0]

int main(void)
{
    int data[] = {3, 1, 4, 1, 5};
    printf("%d %d %d %zu\n", SQUARE(3), 100 / SQUARE(5), MIN(7, 2) * 1, ARRAY_LEN(data));
    return 0;
}

Example

expected output

9 4 2 5
Hint 1

Put parentheses around each use of a parameter, and around the entire macro body.

Hint 2

#define SQUARE(x) ((x) * (x)) is the model; do the same for MIN with the conditional operator.

Hint 3

#define ARRAY_LEN(arr) (sizeof (arr) / sizeof (arr)[0])

Reference solution
#include <stdio.h>

#define SQUARE(x) ((x) * (x))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define ARRAY_LEN(arr) (sizeof (arr) / sizeof (arr)[0])

int main(void)
{
    int data[] = {3, 1, 4, 1, 5};
    printf("%d %d %d %zu\n", SQUARE(3), 100 / SQUARE(5), MIN(7, 2) * 1, ARRAY_LEN(data));
    return 0;
}

Exercise 4 · write code

A macro that shows its own code

When debugging it is handy to print an expression together with its value. Define a macro SHOW(expr) so that

SHOW(2 + 3 * 4);
SHOW(limit - used);

prints exactly

2 + 3 * 4 = 14
limit - used = 37

for int limit = 40, used = 3;. The expression is an int; use the stringizing operator # to turn the argument into a string. The given main must print exactly:

2 + 3 * 4 = 14
limit - used = 37
limit * 2 = 80

The checker also uses your macro in a separate test, including inside an if with an else, so make it safe to use as a single statement.

File: main.c

Starting point

#include <stdio.h>

#define SHOW(expr) printf("%d\n", expr)

int main(void)
{
    int limit = 40, used = 3;
    SHOW(2 + 3 * 4);
    SHOW(limit - used);
    SHOW(limit * 2);
    return 0;
}

Example

expected output

2 + 3 * 4 = 14
limit - used = 37
limit * 2 = 80
Hint 1

#expr inside the macro body becomes the argument's text as a string literal: SHOW(a + b) gives "a + b".

Hint 2

Use a format with two conversions: "%s = %d\n", passing #expr and (expr).

Hint 3

A single printf call is already one expression statement, so it is safe in if/else without do { } while (0).

Reference solution
#include <stdio.h>

#define SHOW(expr) printf("%s = %d\n", #expr, (expr))

int main(void)
{
    int limit = 40, used = 3;
    SHOW(2 + 3 * 4);
    SHOW(limit - used);
    SHOW(limit * 2);
    return 0;
}