The Preprocessor

Before compiling, the preprocessor rewrites your source code: it pastes in header files, replaces macros, and includes or removes code depending on conditions. Used well, it gives you constants, include guards and platform-specific code; used carelessly, it produces some of C's most confusing bugs.

Directives

DirectiveDoes
#include <file.h>Paste a system/library header
#include "file.h"Paste your own header (searched in the current folder first)
#define NAME valueDefine a macro
#undef NAMERemove a macro
#if, #ifdef, #ifndef, #elif, #else, #endifConditional compilation
#error "message"Stop compilation with a message
#pragma …Compiler-specific instructions

Function-like macros

Macros can take parameters. They're pure text substitution, which leads to two classic pitfalls:

C
#include <stdio.h>

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

int main(void) {
    printf("%d\n", SQUARE_BAD(2 + 3));   // becomes 2 + 3 * 2 + 3
    printf("%d\n", SQUARE(2 + 3));       // becomes ((2 + 3) * (2 + 3))
    printf("%d\n", MAX(7, 12));
    return 0;
}
Output
11
25
12
  1. Missing parentheses change the meaning with operator precedence. Parenthesise every parameter and the whole result.
  2. Repeated evaluation: MAX(i++, j) may increment i twice, because the argument is pasted in twice.

For anything that behaves like a function, write a real function — marked static inline if you want it to be cheap. Compilers optimise small functions just as well, and functions have types and no surprises.

C
#include <stdio.h>

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

int main(void) {
    int i = 5;
    int biggest = max_int(i++, 3);     // i is incremented exactly once
    printf("%d, i is now %d\n", biggest, i);
    return 0;
}
Output
5, i is now 6

Include guards

If a header is included twice in the same file (often indirectly), its definitions would be duplicated. An include guard makes the second inclusion empty:

H geometry.h
#ifndef GEOMETRY_H
#define GEOMETRY_H

typedef struct { double x, y; } Point;
double distance(Point a, Point b);

#endif

Most compilers also support the shorter #pragma once at the top of a header; it isn't standard, but it's widely used. Lesson 52 puts headers to work.

Conditional compilation

Code can be included only in some builds — debug output, platform-specific code, optional features:

C
#include <stdio.h>

#define DEBUG 1

#if DEBUG
#define LOG(msg) fprintf(stderr, "[debug] %s:%d: %s\n", __FILE__, __LINE__, msg)
#else
#define LOG(msg) ((void) 0)
#endif

int main(void) {
    LOG("starting");
    int total = 0;
    for (int i = 1; i <= 4; i++) total += i;
    LOG("loop finished");
    printf("total = %d\n", total);
    return 0;
}
Output
[debug] main.c:12: starting
[debug] main.c:15: loop finished
total = 10

Instead of editing the file, you can define macros on the command line: gcc -DDEBUG=0 main.c. __FILE__ and __LINE__ are predefined macros holding the current file name and line number.

Platform detection

Compilers predefine macros that identify the system:

C
#include <stdio.h>

int main(void) {
#if defined(_WIN32)
    const char *os = "Windows";
#elif defined(__APPLE__)
    const char *os = "macOS";
#elif defined(__linux__)
    const char *os = "Linux";
#else
    const char *os = "something else";
#endif
    printf("Compiled for %s, C standard %ld\n", os, (long) __STDC_VERSION__);
    return 0;
}
Output
Compiled for macOS, C standard 201710

__STDC_VERSION__ is 201710L for C17 and 202311L for C23.

Stringising and pasting

Inside a macro, #x turns an argument into a string, and a ## b joins two tokens. A neat use is printing an expression together with its value:

C
#include <stdio.h>

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

int main(void) {
    int width = 7, height = 6;
    SHOW(width * height);
    SHOW(width > height);
    return 0;
}
Output
width * height = 42
width > height = 1

Seeing the result

When a macro misbehaves, look at what the preprocessor produced: gcc -E main.c prints the source after preprocessing (lesson 4).

Try it yourself

Use an X-macro: one list that generates both an enum and its matching names, so they can never get out of step.

Try it yourself · main.c
Edit the code, copy it into main.c, then compile and run it with cc -std=c17 -Wall main.c -o main && ./main. How to set up a C compiler
Output
0 APPLE
1 BANANA
2 CHERRY
3 DATE