The Preprocessor & Macros

C · Lesson 24 of 32 7 min read Updated

Before the compiler sees a single line of your C code, another program runs over it: the preprocessor. It handles every line that starts with #. It pastes header files into your code, replaces macro names with their text, and can leave out whole sections depending on settings you choose.

In this lesson you will see the preprocessor's output, write macros that do not break, turn debugging code on and off with a compiler option, use the predefined macros, and meet the preprocessor features added in C23.

What the preprocessor does

The preprocessor works on text, not on C meaning. It knows nothing about types or variables. You can see its output with gcc -E, which stops after preprocessing.

C
// area.c
#define PI 3.14159
#define CIRCLE_AREA(r) (PI * (r) * (r))

double garden = CIRCLE_AREA(7.0);
Terminal
gcc -E area.c

After a few marker lines that record file names and line numbers, the output ends with:

Output
double garden = (3.14159 * (7.0) * (7.0));

The macros have vanished, replaced by their text. This is what the compiler actually compiles. When a macro misbehaves, gcc -E is the quickest way to see why.

#include

#include pastes the entire contents of another file at that point. There are two forms:

  • #include <stdio.h> with angle brackets searches the system's standard header folders. Use it for the standard library and installed libraries.
  • #include "marks.h" with quotes searches the current file's folder first, then the system folders. Use it for your own headers, as in Header Files & Multi-file Programs.

Object-like and function-like macros

An object-like macro replaces a name with text: #define MAX_STUDENTS 60. A function-like macro takes arguments: #define SQUARE(x) ((x) * (x)). There must be no space between the macro name and its opening bracket. Because the replacement is plain text, brackets are essential:

C
#include <stdio.h>

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

int main(void)
{
    printf("SQUARE_BAD(2 + 3) = %d\n", SQUARE_BAD(2 + 3));
    printf("SQUARE(2 + 3)     = %d\n", SQUARE(2 + 3));
    printf("100 / SQUARE(5)   = %d\n", 100 / SQUARE(5));
    return 0;
}
Output
SQUARE_BAD(2 + 3) = 11
SQUARE(2 + 3)     = 25
100 / SQUARE(5)   = 4

SQUARE_BAD(2 + 3) becomes 2 + 3 * 2 + 3, which is 11. The rule: put brackets around every parameter and around the whole replacement. Even then, 100 / SQUARE_BAD(5) would become 100 / 5 * 5, which is 100, not 4.

Prefer functions when you can

Brackets cannot fix one problem: a macro argument may be evaluated more than once.

C
#include <stdio.h>

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

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

int main(void)
{
    int x = 5, y = 3;
    int m = MAX(x++, y);     // x++ appears twice after expansion
    printf("MAX:     m = %d, x = %d\n", m, x);

    x = 5;
    m = max_int(x++, y);     // x++ runs exactly once
    printf("max_int: m = %d, x = %d\n", m, x);
    return 0;
}
Output
MAX:     m = 6, x = 7
max_int: m = 5, x = 6

The macro expanded to ((x++) > (y) ? (x++) : (y)), so x was incremented twice and the "maximum" was 6, a value that was never passed in. A static inline function evaluates each argument once, checks types, and shows up in the debugger. Modern compilers make small functions just as fast as macros, so use functions for calculations and constexpr for constants, and keep macros for jobs only the preprocessor can do, like the ones below.

Stringising and token pasting

Inside a macro, #param turns an argument into a string literal, and a##b glues two pieces of text into one name.

C
#include <stdio.h>

#define SHOW(expr) printf(#expr " = %d\n", (expr))
#define MAKE_GETTER(field) int get_##field(void) { return field; }

int price = 1299;
int stock = 42;
MAKE_GETTER(price)
MAKE_GETTER(stock)

int main(void)
{
    SHOW(7 * 6);
    SHOW(price / 100);
    printf("get_stock() = %d\n", get_stock());
    return 0;
}
Output
7 * 6 = 42
price / 100 = 12
get_stock() = 42

SHOW(7 * 6) becomes printf("7 * 6" " = %d\n", (7 * 6)), and the two adjacent strings join. This is handy for debugging output. MAKE_GETTER(stock) builds a function named get_stock. Use ## sparingly: generated names are hard to search for.

Conditional compilation

#if, #ifdef, #ifndef, #elif, #else and #endif keep or remove code before compilation. A common use is debug logging that costs nothing in the final program. The option -DNAME defines a macro from the command line.

C
#include <stdio.h>

#ifdef DEBUG
#define LOG(fmt, ...) fprintf(stderr, "[%s:%d] " fmt "\n", __FILE__, __LINE__, __VA_ARGS__)
#else
#define LOG(fmt, ...) ((void) 0)
#endif

int main(void)
{
    int total = 0;
    for (int i = 1; i <= 3; i++) {
        total += i * 10;
        LOG("i = %d, total = %d", i, total);
    }
    printf("Total: %d\n", total);
    return 0;
}
Terminal
gcc -std=c23 -Wall -Wextra -DDEBUG log.c -o log
./log
Output
[log.c:14] i = 1, total = 10
[log.c:14] i = 2, total = 30
[log.c:14] i = 3, total = 60
Total: 60

Built without -DDEBUG, the same program prints only Total: 60, because every LOG becomes ((void) 0), which does nothing. ... and __VA_ARGS__ make a variadic macro that accepts any number of extra arguments. __FILE__ and __LINE__ are filled in where the macro is used, so each message says exactly where it came from.

Predefined macros and the standard version

The compiler predefines several macros. __FILE__ is the current file name, __LINE__ the current line, and __DATE__ and __TIME__ the moment of compilation. __func__ is not a macro but a predefined variable holding the current function's name. __STDC_VERSION__ tells you which C standard is being used:

C
#include <stdio.h>

int main(void)
{
#if __STDC_VERSION__ >= 202311L
    puts("Compiled as C23 or newer");
#elif __STDC_VERSION__ >= 201710L
    puts("Compiled as C17");
#else
    puts("Compiled as an older standard");
#endif
    printf("__STDC_VERSION__ = %ld\n", __STDC_VERSION__);
    printf("This is line %d of %s, in function %s\n", __LINE__, __FILE__, __func__);
    return 0;
}

Compiled as version.c with GCC 15 and -std=c23:

Output
Compiled as C23 or newer
__STDC_VERSION__ = 202311
This is line 13 of version.c, in function main

The values are 201112L for C11, 201710L for C17 and 202311L for C23. GCC 13 and 14 used a provisional value, 202000L, in C23 mode, so on those versions this program prints "Compiled as C17" even with -std=c23.

Common mistakes

  • Missing brackets in a macro. Bracket every parameter and the whole replacement.
  • A semicolon at the end of a #define. It is pasted everywhere the macro is used.
  • A space before the bracket. #define SQUARE (x) ... defines an object-like macro whose text starts with (x).
  • Arguments with side effects. MAX(i++, j) may increment i twice. Use an inline function.
  • Multi-statement macros. Two statements in a macro break inside an if without braces. Wrap them as do { ... } while (0), or better, write a function.

Key points

  • The preprocessor handles # lines before compilation, working on plain text; gcc -E shows its output.
  • #include <...> is for system headers and #include "..." for your own.
  • Bracket every macro parameter and the whole replacement; prefer inline functions and constexpr where possible.
  • # turns a macro argument into a string and ## pastes tokens together.
  • #ifdef with -DNAME switches code such as debug logging on and off at compile time.
  • __FILE__, __LINE__, __func__ and __STDC_VERSION__ describe the code being compiled; C23 adds #embed, #elifdef and #warning.

Exercise

Write a macro ARRAY_LEN(a) that gives the number of elements in an array, and a macro CHECK(cond) that prints Check failed: followed by the condition's text and the line number when the condition is false. Use them on an array of five fees to print its length and to check that the first fee is below 1000 and the last is below 500.

Show solution
C
#include <stdio.h>

#define ARRAY_LEN(a) (sizeof (a) / sizeof (a)[0])
#define CHECK(cond) \
    do { \
        if (!(cond)) { \
            printf("Check failed: %s (line %d)\n", #cond, __LINE__); \
        } \
    } while (0)

int main(void)
{
    int fees[] = {750, 1200, 300, 450, 600};

    printf("Number of fees: %zu\n", ARRAY_LEN(fees));
    CHECK(fees[0] < 1000);
    CHECK(fees[4] < 500);
    return 0;
}
Output
Number of fees: 5
Check failed: fees[4] < 500 (line 17)

A backslash at the end of a line continues the macro on the next line. #cond turns the condition into text, and do { ... } while (0) makes the macro behave like a single statement, so it is safe even inside an if without braces. __LINE__ gives the line where CHECK was used.

Quick Quiz

1. What does gcc -E file.c show?

2. With #define DOUBLE(x) x + x, what is DOUBLE(3) * 2?

3. What does #expr do inside a macro?

4. What value does __STDC_VERSION__ have for C23 in GCC 15?

Learn C with a mentor

Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call