C Preprocessor — Macros, Includes, and Conditional Compilation
In this tutorial, you will learn about C Preprocessor. We cover key concepts, practical examples, and best practices to help you master this topic.
The C preprocessor processes source code before compilation, handling file inclusion via #include, macro expansion via #define, conditional compilation via #ifdef/#ifndef/#endif, and utility directives like #pragma and #error to control the compilation Process.
What You Will Learn
- Defining and using macros with #define
- Including header files with #include
- Conditional compilation with #if, #ifdef, #ifndef, #else, #elif, #endif
- The # and ## operators for stringification and token pasting
- Using predefined macros (__FILE__, __LINE__, __DATE__, __TIME__)
- Variadic macros with __VA_ARGS__
- #pragma and #error directives
Why It Matters
The preprocessor is a powerful Code Generation tool that runs before the compiler. It enables platform-specific code, debugging instrumentation, assertion macros, include guards, and code that adapts to different compilers and operating systems. Durga Antivirus Pro uses preprocessor conditionals extensively to compile separate code paths for Windows (WinSock), Linux (epoll), and macOS (kqueue), all from a single source tree.
Real-World Use
A cross-platform library needs to allocate aligned memory: on Linux it uses posix_memalign, on Windows _aligned_malloc, on macOS valloc. Preprocessor conditionals select the right function for each platform without changing the caller's code.
Learning Path
flowchart LR A[Assert.h] --> B[Preprocessor\nYou are here] B --> C[Header Files] style B fill:#f90,color:#fff
Basic Macros
#include <stdio.h>
#define PI 3.1415926535
#define AREA(r) (PI * (r) * (r))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define SQUARE(x) ((x) * (x))
int main() {
double radius = 5.0;
printf("Area: %.4f\n", AREA(radius));
printf("MAX(10, 20): %d\n", MAX(10, 20));
printf("SQUARE(3 + 2): %d\n", SQUARE(3 + 2)); // ((3+2)*(3+2)) = 25
return 0;
}
Extra parentheses in macros prevent operator precedence bugs. Without them, SQUARE(3 + 2) would expand to 3 + 2 * 3 + 2 = 11.
Stringification and Token Pasting
#include <stdio.h>
// # converts macro argument to a string literal
#define STRINGIFY(x) #x
// ## concatenates two tokens
#define CONCAT(a, b) a ## b
// Practical: make a unique variable name
#define UNIQ_VAR(prefix) prefix ## __LINE__
int main() {
printf("Stringified: %s\n", STRINGIFY(hello world));
printf("Stringified int: %s\n", STRINGIFY(42));
int xy = 100;
printf("CONCAT(x, y): %d\n", CONCAT(x, y)); // expands to xy
int var_42 = 42;
printf("UNIQ_VAR(var_): %d\n", UNIQ_VAR(var_)); // expands to var_ followed by __LINE__
return 0;
}
Conditional Compilation
#include <stdio.h>
#define DEBUG 1
#define OS_LINUX
int main() {
#ifdef DEBUG
printf("Debug mode enabled\n");
#endif
#if defined(OS_LINUX)
printf("Compiling for Linux\n");
#elif defined(OS_MAC)
printf("Compiling for macOS\n");
#elif defined(OS_WINDOWS)
printf("Compiling for Windows\n");
#else
printf("Unknown OS\n");
#endif
// Debug assertion using preprocessor
#ifdef DEBUG
int x = -1;
if (x < 0) {
printf("ASSERTION FAILED: x < 0 at %s:%d\n", __FILE__, __LINE__);
}
#endif
return 0;
}
Output:
Debug mode enabled
Compiling for Linux
ASSERTION FAILED: x < 0 at <filename>:25
Include Guards
Prevent multiple inclusions of the same header:
#ifndef UTILITY_H
#define UTILITY_H
int clamp(int value, int min, int max);
double degrees_to_radians(double degrees);
#endif // UTILITY_H
Variadic Macros
#include <stdio.h>
#define LOG_INFO(format, ...) \
fprintf(stdout, "[INFO] " format "\n", ##__VA_ARGS__)
#define LOG_ERROR(format, ...) \
fprintf(stderr, "[ERROR] " format "\n", ##__VA_ARGS__)
int main() {
LOG_INFO("Server started on port %d", 8080);
LOG_INFO("User %s logged in", "alice");
LOG_ERROR("Connection failed: %s", "timeout");
LOG_INFO("No extra args"); // Works due to ## before __VA_ARGS__
return 0;
}
Predefined Macros
#include <stdio.h>
int main() {
printf("File: %s\n", __FILE__);
printf("Line: %d\n", __LINE__);
printf("Date: %s\n", __DATE__);
printf("Time: %s\n", __TIME__);
printf("STDC: %d\n", __STDC__);
#ifdef __STDC_VERSION__
printf("C Standard: %ld\n", __STDC_VERSION__);
#endif
#ifdef __GNUC__
printf("GCC Version: %d.%d\n", __GNUC__, __GNUC_MINOR__);
#endif
return 0;
}
Output (example):
File: predefined.c
Line: 7
Date: Jun 28 2026
Time: 14:00:00
STDC: 1
C Standard: 201710
GCC Version: 13.2
Pragma and Error Directives
#include <stdio.h>
// Suppress a specific warning (GCC/Clang)
#pragma GCC diagnostic ignored "-Wunused-variable"
// Message during compilation
#pragma message("Building with custom configuration")
int main() {
int unused_var; // Warning suppressed
printf("Hello\n");
// Compile-time assertion using #error
#ifndef REQUIRED_FEATURE
#error "REQUIRED_FEATURE must be defined to compile this file"
#endif
return 0;
}
Common Mistakes
Not parenthesizing macro arguments:
#define DOUBLE(x) x + xthenDOUBLE(3) * 5expands to3 + 3 * 5 = 18, not30. Always parenthesize each argument and the whole expression.Double evaluation in macros:
#define MAX(a, b) ((a) > (b) ? (a) : (b))evaluatesaorbtwice. If the argument has side effects likex++, those happen twice. Use inline functions instead for such cases.Missing include guards: Without guards, a header included twice causes duplicate definition errors. Always use #ifndef/#define/#endif or #pragma once.
Trailing semicolons in macros:
#define PRINT(msg) printf("%s\n", msg);thenif (x) PRINT("ok") else ...breaks the else. Define the macro without the semicolon and add it at the call site.Forgetting that # and ## operators only work in macros: The # operator (stringify) and ## operator (token paste) are only valid in #define macro definitions, not in regular code.
Practice Questions
- What is the purpose of include guards in header files?
- Why must macro arguments be parenthesized?
- What does the
##operator do in a macro? - How does conditional compilation help with cross-platform code?
- Challenge: Write a set of assertion macros that print the file name, line number, and a message when the assertion fails. Include a NDEBUG version that compiles to nothing, and a DEBUG version that calls abort() after printing. Use #ifdef to switch between them.
Mini Project
Build a logging system using variadic macros:
- Define macros: LOG_TRACE, LOG_DEBUG, LOG_INFO, LOG_WARN, LOG_ERROR, LOG_FATAL
- Each macro takes a format string and variable arguments
- Each log level prints a prefix: [TRACE], [DEBUG], [INFO], [WARN], [ERROR], [FATAL]
- Include FILE, LINE, and FUNCTION in each log message
- Compile-time log level filtering: if LOG_LEVEL is set to INFO, TRACE and DEBUG messages are compiled out
- Output to stderr with a timestamp using localtime_r
- Test with all log levels
FAQ
{{< faq "What is the difference between #include
What is Next
Proceed to Header Files to learn about declaring functions and types across multiple source files. Then explore Makefiles for build automation.