Lesson 147 min read

Preprocessor & Macros

The preprocessor is a copy-paste robot that runs BEFORE the compiler even sees your code.

The Invisible First Step

Before the C compiler translates your code into machine instructions, a preprocessor runs through it first. Think of it as a copy-paste robot with a simple job: find special directives (lines starting with #), follow their instructions, and produce a new version of your source file. The compiler never sees the # lines β€” it only sees the result.

The preprocessor does three main things:

  • File inclusion β€” #include pastes the contents of another file
  • Macro substitution β€” #define replaces text patterns
  • Conditional compilation β€” #ifdef/#ifndef includes or skips blocks of code

Constant Defines

#include <stdio.h>
#define PI 3.14159
#define MAX_STUDENTS 100
#define GREETING "Hello, World!"
int main() {
double area = PI * 5.0 * 5.0;
printf("%s\n", GREETING);
printf("Area of circle (r=5): %.2f\n", area);
printf("Max students allowed: %d\n", MAX_STUDENTS);
return 0;
}
Output
Hello, World!
Area of circle (r=5): 78.54
Max students allowed: 100

Parameterized Macros

Macros can take parameters β€” they look like functions but behave very differently. A function call jumps to another piece of code. A macro is literally text replacement β€” the preprocessor pastes the expanded code directly where the macro is used.

This makes macros fast (no function call overhead) but dangerous (no type checking, unexpected side effects).

Parameterized Macros

#include <stdio.h>
// ALWAYS parenthesize parameters and the whole expression!
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define SQUARE(x) ((x) * (x))
#define ABS(x) ((x) < 0 ? -(x) : (x))
int main() {
printf("MAX(3, 7) = %d\n", MAX(3, 7));
printf("MIN(10, 4) = %d\n", MIN(10, 4));
printf("SQUARE(5) = %d\n", SQUARE(5));
printf("ABS(-42) = %d\n", ABS(-42));
// Careful! SQUARE(1+2) becomes ((1+2)*(1+2)) = 9
// Without parens it would be 1+2*1+2 = 5 (WRONG!)
printf("SQUARE(1+2) = %d\n", SQUARE(1 + 2));
return 0;
}
Output
MAX(3, 7)    = 7
MIN(10, 4)   = 4
SQUARE(5)    = 25
ABS(-42)     = 42
SQUARE(1+2)  = 9
Note: Macros don't respect scope or types β€” they're literally text replacement. Use parentheses around EVERYTHING in macro definitions: each parameter AND the entire expression. Without them, operator precedence can silently produce wrong results. #define SQUARE(x) x*x looks fine until someone writes SQUARE(1+2) and gets 1+2*1+2 = 5 instead of 9.

Header Guards

When you #include a header file, the preprocessor literally pastes its contents into your source. If two files both include the same header, you get duplicate definitions β€” and the compiler complains.

Header guards prevent this. They use #ifndef / #define / #endif to say: "Only paste this content if it hasn't been pasted already."

Header Guard Pattern

// ========== math_utils.h ==========
#ifndef MATH_UTILS_H // If not already defined...
#define MATH_UTILS_H // ...define it (mark as included)
#define PI 3.14159
double circle_area(double radius);
double circle_circumference(double radius);
#endif // MATH_UTILS_H
// ========== math_utils.c ==========
#include "math_utils.h"
double circle_area(double radius) {
return PI * radius * radius;
}
double circle_circumference(double radius) {
return 2.0 * PI * radius;
}
// ========== main.c ==========
#include <stdio.h>
#include "math_utils.h" // Safe to include multiple times
#include "math_utils.h" // Second include is harmlessly skipped
int main() {
printf("Area: %.2f\n", circle_area(5.0));
printf("Circumference: %.2f\n", circle_circumference(5.0));
return 0;
}
Output
Area: 78.54
Circumference: 31.42

Conditional Compilation

Sometimes you want code that only exists under certain conditions β€” debug logging that disappears in release builds, or platform-specific code for Windows vs. Linux. The preprocessor can include or exclude entire blocks of code at compile time.

Conditional Debug Logging

#include <stdio.h>
// Define DEBUG to enable debug output
// In production, comment this line out or compile without -DDEBUG
#define DEBUG
#ifdef DEBUG
#define LOG(msg, ...) printf("[DEBUG] " msg "\n", ##__VA_ARGS__)
#else
#define LOG(msg, ...) // Expands to nothing!
#endif
int factorial(int n) {
LOG("factorial(%d) called", n);
if (n <= 1) return 1;
int result = n * factorial(n - 1);
LOG("factorial(%d) = %d", n, result);
return result;
}
int main() {
int result = factorial(5);
printf("5! = %d\n", result);
return 0;
}
Output
[DEBUG] factorial(5) called
[DEBUG] factorial(4) called
[DEBUG] factorial(3) called
[DEBUG] factorial(2) called
[DEBUG] factorial(1) called
[DEBUG] factorial(2) = 2
[DEBUG] factorial(3) = 6
[DEBUG] factorial(4) = 24
[DEBUG] factorial(5) = 120
5! = 120

Advanced: Stringification (#) and Token Pasting (##)

Two special operators work inside macros:

  • # (stringification) β€” turns a macro parameter into a string literal
  • ## (token pasting) β€” glues two tokens together into one identifier

These are niche but powerful. You'll see them in testing frameworks and code generators.

Stringification and Token Pasting

#include <stdio.h>
// # turns the argument into a string
#define PRINT_VAR(var) printf(#var " = %d\n", var)
// ## glues tokens together
#define MAKE_FUNC(name) \
int func_##name() { return __LINE__; }
MAKE_FUNC(hello) // Creates: int func_hello() { ... }
MAKE_FUNC(world) // Creates: int func_world() { ... }
int main() {
int score = 42;
int lives = 3;
PRINT_VAR(score); // Expands to: printf("score" " = %d\n", score)
PRINT_VAR(lives);
printf("func_hello returns: %d\n", func_hello());
printf("func_world returns: %d\n", func_world());
return 0;
}
Output
score = 42
lives = 3
func_hello returns: 10
func_world returns: 11
Note: Many modern C projects use #pragma once instead of traditional header guards. It does the same thing β€” prevents double-inclusion β€” but with a single line. Not technically standard C, but supported by every major compiler (GCC, Clang, MSVC).
Challenge

Quick check

When does the preprocessor run?
← File I/OFunction Pointers & Callbacks β†’