The Preprocessor

Before the C compiler sees a single token, the preprocessor rewrites your text: lines starting with # are directives. This textual phase is C's answer to modules and metaprogramming — and its most abused corner. Understand its model (pure text substitution, zero types, zero scopes) and you will write macros that help instead of hurt.

The Compilation Pipeline

Compilation is four stages. The preprocessor runs first, on raw text; only its output is C. That is why a macro can never check types and a #define can be passed arguments — and why misuse is so easy to trigger.

C compilation pipeline: preprocessor, compiler, assembler, linker

Figure 1 — four stages; the preprocessor is pure text transformation, before the compiler interprets anything.

#include — Reusing Text

#include pastes another file into yours. Angle brackets search the standard include paths (<stdio.h>), quotes search the current directory first ("myheader.h"). Every header should be wrapped in an include guard so pasting it twice — which happens easily through nested includes — costs nothing.

#ifndef POINT_H        // if the guard macro is not defined yet...
#define POINT_H         // ...define it now (so later includes skip the body)

struct Point { int x, y; };

#endif                  // end of the guarded region

Since C11 there is also #pragma once, a simpler non-standard-but-universally-supported guard. Pick one style and use it everywhere.

#define — Object & Function Macros

#define NAME value creates an object-like macro: every NAME is replaced by its value. #define SQUARE(x) ((x)*(x)) is function-like: the arguments are substituted textually. The parentheses above are not decoration — without them SQUARE(a+b) computes a+b*a+b, and without the outer pair 1/SQUARE(2) computes 1/2*2. Every macro parameter must be wrapped in parentheses.

#include <stdio.h>

#define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0]))  // safe: parenthesized
#define SQUARE(x)    ((x) * (x))                    // safe: parenthesized

int main(void) {
    int arr[] = {1, 2, 3, 4, 5};
    printf("len = %zu\n", ARRAY_LEN(arr));   // 5 — parentheses preserved it
    printf("sq  = %d\n", SQUARE(2 + 3));     // 25, not 11
    return 0;
}

Function-like macros still have sharp edges: arguments are evaluated every time they appear, so side effects like SQUARE(i++) are undefined behavior; and macros have no type checking. Modern C prefers static inline functions (typed, safe, still zero overhead) for logic, and macros only for genuine text needs — like the compile-time ARRAY_LEN trick.

Conditional Compilation

#if, #ifdef, #ifndef, and #else/#elif decide at compile time which chunks of text survive. Typical uses: debug builds, platform-specific code, and feature detection. The preprocessor keeps no runtime cost — the loser branches simply vanish.

#include <stdio.h>

// one source, many targets: pick the branch from the -D flag
#ifdef _WIN32
    #define CLEAR "cls"
#else
    #define CLEAR "clear"
#endif

#if defined(NDEBUG)               // release build? strip the trace
    #define TRACE(msg) ((void)0)  // compiles to nothing
#else
    #define TRACE(msg) fprintf(stderr, "%s:%d %s\n", __FILE__, __LINE__, msg)
#endif

int main(void) {
    TRACE("starting");            // in debug: prints file and line
    printf("clear command: %s\n", CLEAR);
    return 0;
}

Compile with -DNDEBUG to activate the release branch, -D_DEBUG or nothing for the debug one. Multi-line macros end each line with a backslash — a trailing space after \ is an infamous silent bug.

Predefined Macros

The compiler defines useful facts about itself and the source. You already met __FILE__ and __LINE__; the full family includes __DATE__, __TIME__, __func__ (the current function's name), and feature macros such as __STDC_VERSION__ that report which C standard is active.

#include <stdio.h>

int main(void) {
    printf("file:  %s\n", __FILE__);        // the .c file name
    printf("line:  %d\n", __LINE__);        // this very line
    printf("std:   %ld\n", __STDC_VERSION__); // 201112L = C11, 201710L = C17
    return 0;
}

Preprocessor Discipline

Half the C bugs in the wild are preprocessor accidents. Follow these rules and you will be on the safe side of them:

  • Prefer enum and const for constants — they have types and show up in the debugger.
  • Prefer static inline functions over function-like macros — typed, checked, one evaluation.
  • Reserve macros for: include guards, conditional compilation, and compile-time tricks like offsetof/ARRAY_LEN.
  • Always parenthesize every macro parameter and the whole body.
  • Name macros in UPPER_SNAKE_CASE so they stand out from functions.

Next: dynamic memory — the heap, and the discipline that keeps it honest.