Skip to main content

The Preprocessor: Macros, Include Guards, Conditional Compilation

What This Concept Is

The C preprocessor is a text-substitution pass that runs before the compiler sees your source. It understands:

  • #include "file.h" or #include <file.h> - paste the file here
  • #define NAME replacement - every later occurrence of NAME becomes replacement
  • #define MACRO(x) (x)*(x) - function-like macros (parameters substituted textually)
  • #undef NAME - remove a prior definition
  • #if, #ifdef, #ifndef, #elif, #else, #endif - conditional inclusion of source
  • #error, #pragma, #line - compiler directives
  • Predefined macros: __FILE__, __LINE__, __DATE__, __STDC__, __STDC_VERSION__

The preprocessor does not know C grammar. It is pure text. That is its power and its biggest hazard.

Why It Matters Here

You meet the preprocessor in three daily tasks:

  • guarding headers against multiple inclusion
  • defining constants and small inline operations
  • selecting code for different platforms or build configurations

Every one of those has a canonical idiom and a classic bug. Learn the idioms up front.

Concrete Example

Include guards - every header you write uses them:

/* math_util.h */
#ifndef MATH_UTIL_H
#define MATH_UTIL_H
int add(int a, int b);
#endif

Without the guard, a header included twice (transitively) would declare things twice and some compilers would complain or accept partially.

Function-like macro, parenthesized correctly:

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

int y = SQUARE(1 + 2); /* expands to ((1 + 2) * (1 + 2)) = 9, correct */

Without the inner parentheses, SQUARE(1 + 2) would expand to 1 + 2 * 1 + 2 = 5. Always parenthesize both the whole body and every parameter use.

Conditional compilation for platform selection:

#if defined(__linux__)
#include <unistd.h>
#elif defined(_WIN32)
#include <windows.h>
#else
#error "Unsupported platform"
#endif

Compile-time debug switch:

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

Toggle with gcc -DDEBUG ....

Common Confusion / Misconception

"Macros are like functions." They are text substitution. #define MAX(a, b) ((a) > (b) ? (a) : (b)) called as MAX(i++, j++) evaluates one argument twice, incrementing it twice. Prefer static inline functions for anything non-trivial; reserve macros for literal constants, type-independent operations, and compile-time switches.

"The header is included, so its declarations only exist once." The textual content is pasted every time. The #ifndef guard makes the second paste empty so the names are not redeclared.

How To Use It

  1. Every header file: unique #ifndef X_H / #define X_H / #endif guard based on the path/name.
  2. Prefer const and enum to object-like #define for constants with types.
  3. Parenthesize macro bodies and every argument inside them.
  4. Do not put semicolons at the end of #defines unless the macro is meant to terminate a statement.
  5. Use #pragma once only if you already ship on a compiler family that supports it; #ifndef guards are portable.

Check Yourself

  1. Why does #define DOUBLE(x) x+x; int y = DOUBLE(3) * 2; not produce 12?
  2. What does #ifdef NDEBUG typically guard?
  3. Why do header guards need a unique identifier per header?

Mini Drill or Application

  1. Write a header guard for stringutil.h correctly, and also wrong (missing #endif); observe the error.
  2. Define SQUARE(x) two ways, one with full parentheses and one without. Call each with SQUARE(1 + 2) and explain both results.
  3. Add a -DVERBOSE compile flag and gate a printf behind #ifdef VERBOSE.
  4. Write a macro that prints file:line: message using __FILE__ and __LINE__.

Read This Only If Stuck