C from the ground up

The preprocessor and define

Before C sees your file, the preprocessor edits it: pulling in headers and replacing names. define gives a value or a tiny rule a name.

Part 11 of 12

Advanced13 min readBack to Courses

A pass before the compiler

The preprocessor runs before the compiler proper. It does not understand C grammar; it works on lines that start with a hash and rewrites the file before the real compilation begins.

You have already used it. #include <stdio.h> is the preprocessor copying the contents of a header file into yours, which is why the compiler then knows what printf is.

Naming a value

#define PI 3.14159 makes the preprocessor replace every later PI with those digits. It is not a variable: nothing is stored, and the name is swapped in as plain text before compiling.

That means no type and no address, but also no cost. A named constant beats a scattered magic number, because changing the one define changes every use.

Tiny rules and their pitfall

A define can take arguments, so it can act like a small rule: a square define takes a value and returns it multiplied by itself. It is still text substitution, not a function call.

That is why the parentheses matter. Without them, a define can bind to the wrong part of an expression and give a surprising answer. Wrap the whole body and every use of an argument in brackets.

Expansion is textual

#define does not create a variable or a function. The preprocessor replaces the name with the body before the compiler parses anything, so the compiler never sees SQUARE — it sees the expanded expression. That is why a macro has no type and no address, and why it must be defined above the first line that uses it.

Because the body is pasted in, precedence comes from the surrounding expression. #define HALF(x) x / 2 called as HALF(4 + 2) expands to 4 + 2 / 2, which is 5, not 3. Parentheses around the body and around every use of an argument keep the expansion self-contained.

Conditional compilation and header guards

#if, #ifdef and #ifndef keep parts of the file out of the build. A header uses that to define itself only once: #ifndef POINT_H, then #define POINT_H, the declarations, and #endif. The second include then expands to nothing.

That guard matters because a header included twice would declare the same struct twice, which the compiler rejects. The macro name is a convention, not a reserved word, so tie it to the file name to keep two headers from colliding.

Arguments are pasted, and may run twice

A macro argument is pasted wherever the parameter appears, and if it appears twice it is evaluated twice. SQUARE(i++) expands to ((i++) * (i++)), which increments i twice and multiplies two values read at different moments — undefined behaviour, and never what you meant. A function call would evaluate its argument once, before the call, so the difference is invisible until the argument has a side effect.

The rule that follows is simple: pass a plain value or a variable, never an expression that changes something. When a macro must guard against it, it can assign the argument to a local temporary first, but at that point a small function is usually the clearer tool. A function also gives the compiler types to check.

A define or a const?

#define PI 3.14159 has no type, so the compiler cannot warn when it lands in an int. static const double PI = 3.14159; is a real object with a type and an address, visible in a debugger and usable in expressions like any other value. In modern C it is the better default for a named constant.

The preprocessor still owns the jobs a constant cannot do: including a header, keeping a file out of the build with #if, and naming a value that has to be a compile-time constant, such as the size of an array at file scope. Use define for those, and leave ordinary values to the language.

Three macro mistakes are easy to make: leaving out the parentheses around the body or an argument, adding a semicolon to a define such as #define LIMIT 10;, and passing an argument with a side effect because the macro may evaluate it more than once. Bracket every part, leave the semicolon off, and give macros plain values rather than i++.

Try these next

  • Write #define MIN(a, b) ((a) < (b) ? (a) : (b)) and test it with expressions, not just numbers.
  • Add a header guard around a small struct definition and include the file twice.
  • Use #ifdef VERBOSE to switch an extra print line on and off at compile time.

A value and a rule

#include <stdio.h>

#define LIMIT 3
#define DOUBLE(x) ((x) * (x))

int main(void) {
    printf("Limit %d\n", LIMIT);
    printf("Squared %d\n", DOUBLE(4));
    return 0;
}

Constants and a macro

Run it, then change LIMIT and the SQUARE argument. Notice the whole program picks up the new LIMIT without any other edit.

Output
Click "Run" to execute your code

Why the brackets matter

Run it and compare the two squarings. Then remove the brackets from `SQUARE` so the body reads `x * x` and predict the new answer for `SQUARE(2 + 1)` before you run it.

Output
Click "Run" to execute your code

Defines are usually written in capitals so a reader can see at a glance that the name is replaced rather than stored. Follow that habit in your own files.

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

When does the preprocessor run?