CodeMIRAI
0 XP

The preprocessor

4 min read 6 runnable examples Quiz included

Before the compiler sees your program, the preprocessor reads it and rewrites it. It doesn't understand C at all: it works on text, following the lines that start with #, called directives. It pastes in header files, replaces names with their values, and keeps or drops whole blocks of code.

DirectiveWhat it does
#includepastes another file in
#definedefines a name (a macro) to be replaced
#undefforgets a macro
#if, #elif, #else, #endifkeeps or drops code depending on a condition
#ifdef, #ifndefthe same, depending on whether a macro is defined
#errorstops compiling with a message
#pragmaan instruction for one particular compiler

#include

#include <stdio.h> is replaced by the entire contents of the file stdio.h: thousands of lines of declarations. That's how your program learns that printf exists.

  • #include <name.h> searches the system's standard folders: use it for the standard library.
  • #include "name.h" looks next to your own file first: use it for your own headers.

A larger program is split into files: shop.c with the code, and shop.h with the declarations other files need (prototypes, #defines, structure types). Every file that uses the shop code includes shop.h.

#define: constants

C
#include <stdio.h>
#define SEATS 40
#define ROWS 5

int main() {
    int booked = 31;
    printf("%d of %d seats left\n", SEATS - booked, SEATS);
    printf("%d seats in each of %d rows\n", SEATS / ROWS, ROWS);
    return 0;
}

Every SEATS becomes 40 before compiling, so these names cost nothing at run time. A macro's value can be any text: a decimal (#define GST_RATE 0.18) or a string (#define SHOP_NAME "CodeMIRAI Cafe") work just the same in C. By convention macro names are in CAPITALS, and a #define line has no = and no semicolon: #define SEATS 40; would paste 40; everywhere, semicolon included.

#define or const? A const variable has a type the compiler checks and appears by name in a debugger, so many programmers prefer it for values; #define is still common, and is needed where C insists on a constant, such as an array size in older code.

Macros with parameters

A macro can take arguments, and is replaced by its body with the arguments pasted in:

C
#include <stdio.h>
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))

int main() {
    printf("%d\n", SQUARE(7));
    printf("%d\n", SQUARE(1 + 2));
    printf("%d\n", MAX(12, 30));
    return 0;
}

It looks like a function call, but it's text substitution, and that has traps.

Trap 1: brackets

Without brackets around each use of the parameter and around the whole body, the substituted text can mean something else:

C
#include <stdio.h>
#define BAD_SQUARE(x) x * x

int main() {
    printf("%d\n", BAD_SQUARE(1 + 2));    // becomes 1 + 2 * 1 + 2
    return 0;
}

BAD_SQUARE(1 + 2) becomes 1 + 2 * 1 + 2, which is 5, not 9. Always bracket every parameter and the whole body, as SQUARE does.

Trap 2: side effects

An argument used twice in the body is evaluated twice:

C
#include <stdio.h>
#define SQUARE(x) ((x) * (x))

int main() {
    int i = 3;
    int r = SQUARE(i++);      // becomes ((i++) * (i++))
    printf("r = %d, i = %d\n", r, i);
    return 0;
}
Output
r = 12, i = 5

i went up twice, and modifying a variable twice in one expression is undefined behaviour: the result isn't even guaranteed. A real function evaluates its argument once. Prefer functions for anything more than a tiny calculation.

Conditional compilation

#if keeps or drops code before compiling. The classic use is a debug switch: extra checks and messages while you develop, gone from the finished program.

C
#include <stdio.h>
#define DEBUG 1

int main() {
    int total = 0;
    for (int i = 1; i <= 3; i++) {
        total += i * 10;
#if DEBUG
        printf("[debug] i = %d, total = %d\n", i, total);
#endif
    }
    printf("total = %d\n", total);
    return 0;
}
Output
[debug] i = 1, total = 10
[debug] i = 2, total = 30
[debug] i = 3, total = 60
total = 60

Change DEBUG to 0 and the debug lines aren't just skipped: they're removed before compiling, as if never written. With GCC you can also set a macro from the command line without editing the file: gcc -DDEBUG=1 shop.c.

#ifdef NAME asks whether NAME is defined at all, and #ifndef whether it isn't. Programs use them to adapt to the system they're compiled on:

C
#ifdef _WIN32
    printf("Running on Windows\n");
#else
    printf("Running on Linux or macOS\n");
#endif

_WIN32 is defined by compilers on Windows, so only one of the two lines is compiled.

Header guards

If two headers both include shop.h, its declarations would be pasted twice, and defining a structure type twice is an error. A header guard makes a header safe to include more than once:

C
// shop.h
#ifndef SHOP_H
#define SHOP_H

#define MAX_ITEMS 100
double total_price(const double prices[], int n);

#endif

The first time, SHOP_H isn't defined, so the contents are kept and SHOP_H gets defined. Every later time, #ifndef SHOP_H is false and the whole file is skipped. Many compilers also accept #pragma once at the top of a header to do the same in one line.

Predefined macros, # and ##

The preprocessor defines some names itself:

C
#include <stdio.h>
#define SHOW(expr) printf(#expr " = %d\n", expr)
#define JOIN(a, b) a##b

int main() {
    printf("This is line %d\n", __LINE__);
    printf("Inside the function %s\n", __func__);
    SHOW(6 * 7);
    int JOIN(seat, 12) = 3;       // declares a variable named seat12
    printf("seat12 = %d\n", seat12);
    return 0;
}
Output
This is line 6
Inside the function main
6 * 7 = 42
seat12 = 3
NameGives
__LINE__the current line number
__FILE__the source file's name, as it was given to the compiler (often with its folder)
__DATE__, __TIME__when it was compiled
__func__the current function's name (strictly a variable, not a macro)

In a macro body, #x turns the argument into a string ("stringizing"), which is how SHOW prints the expression itself, and a##b glues two pieces into one name. Both are handy in debugging helpers, and best used sparingly.

Seeing what the preprocessor did

gcc -E file.c stops after preprocessing and prints the result: every #include expanded, every macro replaced, every dropped #if block gone. When a macro misbehaves, this shows exactly what the compiler received.

Finished this lesson?Mark it complete to earn +20 XP and move your progress on.