Preprocessor Directives
Preprocessor Directives
Before your C source file is compiled into machine code it passes through several distinct phases. Understanding these phases — especially the first one — is essential for writing well-structured C programs.
The Compilation Pipeline
- Preprocessing — the preprocessor reads your source file and handles all lines beginning with
#. It performs text substitution, file inclusion, and conditional compilation. The output is a pure C translation unit with no#directives left. - Compilation — the compiler translates the preprocessed C source into assembly or object code.
- Linking — the linker combines object files and resolves external references (functions from other files or libraries) into the final executable.
#include — File Inclusion
#include pastes the contents of another file in place:
#include <stdio.h>— angle brackets search the system/standard include directories first.#include "myheader.h"— quotes search the current project directory first, then fall back to system directories.
Use angle brackets for standard library and third-party library headers. Use quotes for your own project headers.
#define — Symbolic Constants
#define NAME value creates a symbolic constant. The preprocessor replaces every occurrence of NAME with value before compilation begins. Unlike const variables, macros have no type and no scope:
#define MAX_SIZE 100Use #undef NAME to remove a macro definition so a new one can be given.
Conditional Compilation
Conditional directives allow you to include or exclude code depending on whether a macro is defined or what value it holds. This is invaluable for cross-platform code and debug builds:
#ifdef NAME— true ifNAMEis defined (regardless of value).#ifndef NAME— true ifNAMEis NOT defined. Classic use: include guards.#if expression— evaluates a constant integer expression.#elif expression— else-if branch.#else— else branch.#endif— closes the conditional block.
Include Guards
When a header is included in multiple source files (or indirectly included more than once), its contents would otherwise be pasted repeatedly, causing redefinition errors. Include guards prevent this:
#ifndef MY_HEADER_H
#define MY_HEADER_H
/* header content */
#endifMany compilers also support #pragma once as a non-standard but widely accepted alternative. Using include guards is more portable; #pragma once is terser. Many modern projects use both.
Predefined Macros
The C standard guarantees several built-in macros that the preprocessor defines automatically:
| Macro | Expands to |
|---|---|
__FILE__ | Current source file name (string literal) |
__LINE__ | Current line number (integer) |
__DATE__ | Compilation date as "Mmm dd yyyy" |
__TIME__ | Compilation time as "hh:mm:ss" |
__func__ | Current function name (C99, not a macro but behaves like one) |
These are extremely useful for logging, assertions, and debugging without a debugger.
Code Examples
/* --- mathutils.h --- */
#ifndef MATHUTILS_H
#define MATHUTILS_H
/* Only compiled once even if included multiple times */
#define PI 3.14159265358979323846
double circle_area(double radius);
double circle_circumference(double radius);
#endif /* MATHUTILS_H */
/* --- mathutils.c --- */
#include "mathutils.h" /* quotes for own headers */
#include <math.h> /* angle brackets for system headers */
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 "mathutils.h"
int main(void) {
printf("Area = %.4f\n", circle_area(5.0));
printf("Circum. = %.4f\n", circle_circumference(5.0));
return 0;
}The include guard (#ifndef/#define/#endif) ensures mathutils.h is processed only once per translation unit, preventing duplicate declarations. PI is defined once and shared across both files.
#include <stdio.h>
/* Compile with -DDEBUG to enable debug output:
gcc -DDEBUG main.c -o main */
#ifdef DEBUG
#define LOG(fmt, ...) fprintf(stderr, "[DEBUG %s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)
#else
#define LOG(fmt, ...) /* nothing — stripped in release builds */
#endif
int divide(int a, int b) {
LOG("divide called: a=%d, b=%d", a, b);
if (b == 0) {
fprintf(stderr, "Error: division by zero\n");
return 0;
}
int result = a / b;
LOG("result = %d", result);
return result;
}
int main(void) {
int x = divide(10, 2);
int y = divide(7, 0);
printf("x=%d y=%d\n", x, y);
return 0;
}When compiled with -DDEBUG the LOG macro expands to fprintf with file and line info. In release builds LOG expands to nothing — zero overhead. The ##__VA_ARGS__ trick removes the trailing comma when no extra args are given.
#include <stdio.h>
void show_location(void) {
/* __func__ is a C99 predefined identifier, behaves like a string macro */
printf("Function : %s\n", __func__);
printf("File : %s\n", __FILE__);
printf("Line : %d\n", __LINE__);
printf("Compiled : %s at %s\n", __DATE__, __TIME__);
}
/* Portable compile-time assertion using preprocessor */
#define STATIC_ASSERT(cond, msg) typedef char static_assert_##msg[(cond) ? 1 : -1]
STATIC_ASSERT(sizeof(int) >= 4, int_must_be_at_least_4_bytes);
int main(void) {
show_location();
/* Platform detection example */
#if defined(_WIN32)
printf("Platform : Windows\n");
#elif defined(__linux__)
printf("Platform : Linux\n");
#elif defined(__APPLE__)
printf("Platform : macOS\n");
#else
printf("Platform : Unknown\n");
#endif
return 0;
}__FILE__, __LINE__, __DATE__, __TIME__ are replaced at compile time. STATIC_ASSERT uses a trick: declaring an array of size -1 causes a compile error if the condition is false — giving you a compile-time check with zero runtime cost.
Quick Quiz
1. What is the purpose of an include guard (#ifndef / #define / #endif) in a header file?
2. What is the difference between #include <file.h> and #include "file.h"?
3. Which predefined macro gives the current source file's line number?
4. During which phase of compilation are #define macros processed?
Was this lesson helpful?