The C preprocessor handles directives before the compiler parses the resulting translation unit. It includes headers, expands macros, and selects conditional source text. Because macro expansion is token substitution rather than a typed function call, small mistakes can duplicate side effects, change precedence, or remove code unexpectedly.
After this lesson, you can write a guarded header, choose a typed constant or inline function instead of a macro when appropriate, create a safe function-like macro, and use conditional compilation without producing untestable platform branches.
The C preprocessor runs before compilation. It processes lines starting with # (called directives) and performs text substitution, file inclusion, and conditional compilation. The preprocessor output is pure C code that the compiler then compiles.
| Directive | Description |
|---|---|
| #include | Include a header file |
| #define | Define a macro (constant or function-like) |
| #undef | Undefine a macro |
| #ifdef | Compile if macro is defined |
| #ifndef | Compile if macro is NOT defined |
| #if / #elif / #else / #endif | Conditional compilation |
| #pragma | Compiler-specific instructions |
| #error | Emit a compile-time error message |
| Macro | Description | Example Value |
|---|---|---|
| __FILE__ | Current source file name | "main.c" |
| __LINE__ | Current line number | 42 |
| __DATE__ | Compilation date | "Jan 15 2025" |
| __TIME__ | Compilation time | "10:30:00" |
| __STDC__ | 1 if standard C compiler | 1 |
#include <stdio.h>
// Object-like macros (constants)
#define PI 3.14159265
#define MAX_SIZE 100
#define NEWLINE '\n'
// Function-like macros (no type checking - use with care)
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define ABS(x) ((x) < 0 ? -(x) : (x))
// Multi-line macro using backslash
#define PRINT_INFO(name, val) \
printf("%-15s = %d\n", name, val)
int main() {
printf("PI = %.5f\n", PI);
printf("MAX_SIZE = %d\n", MAX_SIZE);
int x = 5;
printf("SQUARE(%d) = %d\n", x, SQUARE(x)); // 25
printf("MAX(3, 7) = %d\n", MAX(3, 7)); // 7
printf("ABS(-10) = %d\n", ABS(-10)); // 10
// Use side-effect-free arguments when a macro evaluates them more than once.
printf("SQUARE(x + 1) = %d\n", SQUARE(x + 1));
// Never call SQUARE(x++): repeated unsequenced modification is undefined behavior.
PRINT_INFO("MAX_SIZE", MAX_SIZE);
// #undef - remove a macro definition
#undef MAX_SIZE
// printf("%d", MAX_SIZE); // ERROR: MAX_SIZE not defined
return 0;
}
#include <stdio.h>
#define DEBUG // comment this out to disable debug output
#define VERSION 2
int main() {
int x = 42;
// #ifdef - compile only if DEBUG is defined
#ifdef DEBUG
printf("[DEBUG] x = %d\n", x);
#endif
// #ifndef - compile only if macro is NOT defined
#ifndef RELEASE
printf("Running in development mode\n");
#endif
// #if / #elif / #else - numeric conditions
#if VERSION == 1
printf("Version 1 features\n");
#elif VERSION == 2
printf("Version 2 features\n");
#else
printf("Unknown version\n");
#endif
// Header guard pattern (prevents double inclusion)
// Typically in .h files:
// #ifndef MY_HEADER_H
// #define MY_HEADER_H
// ... header content ...
// #endif
return 0;
}
/*
[DEBUG] x = 42
Running in development mode
Version 2 features
*/
// #pragma once - modern alternative to header guards
// (supported by most compilers: GCC, Clang, MSVC)
// #pragma once
#include <stdio.h>
// Useful debug macro using predefined macros
#define LOG(msg) printf("[%s:%d] %s\n", __FILE__, __LINE__, msg)
int main() {
// Predefined macros
printf("File: %s\n", __FILE__);
printf("Line: %d\n", __LINE__);
printf("Date: %s\n", __DATE__);
printf("Time: %s\n", __TIME__);
// Using LOG macro
LOG("Program started");
int x = 10;
LOG("About to compute");
printf("x = %d\n", x);
LOG("Done");
// #pragma message - print message at compile time
// #pragma message("Compiling main.c...")
// #pragma pack - control struct alignment
// #pragma pack(1) // pack structs with 1-byte alignment
return 0;
}
/*
File: predefined.c
Line: 12
Date: Jan 15 2025
Time: 10:30:00
[predefined.c:16] Program started
x = 10
[predefined.c:19] Done
*/
#include replaces the directive with the selected header’s contents as part of preprocessing. Angle brackets request the implementation’s include search for system headers; quotes begin with the project-oriented search defined by the implementation. A header should declare an interface and include the declarations required to understand that interface.
Use an include guard with a project-unique macro so repeated inclusion does not redefine declarations. #pragma once is widely supported but is not the portable language mechanism. Avoid defining ordinary external-storage objects in headers because every translation unit that includes the header may create a definition.
Parenthesize every use of a function-like macro parameter and the complete replacement expression. Even then, an argument can be evaluated more than once. SQUARE(i++) does not behave like a function and can produce undefined behavior, so macros that evaluate parameters should be called only with side-effect-free expressions.
Prefer enum constants, const objects, or static inline functions when type checking, single evaluation, or debugger visibility matters. Macros remain useful for conditional compilation, stringification, token construction, and carefully constrained generic operations.
#if, #ifdef, and related directives select source before type checking. Keep configuration macros centralized and test every supported branch in continuous integration; code hidden behind a platform macro can decay unnoticed on the developer’s machine.
Use #error for an unsupported or contradictory build configuration. Do not use conditional compilation to scatter product behavior throughout ordinary functions when runtime configuration would be clearer and testable in one build.
Try this next
0 of 2 completed
Explore 500+ free tutorials across 20+ languages and frameworks.