C Preprocessor Directives - Complete Guide
Master C preprocessor directives including #include, #define, macros, conditional compilation (#ifdef, #ifndef), and programming best practices.
4 Directive Types
Complete coverage
Practical Examples
Real-world usage
Macro Functions
Powerful text substitution
Introduction to C Preprocessor
The C preprocessor is a tool that processes your source code before it's compiled. It handles directives (lines starting with #) to perform text manipulation, file inclusion, and conditional compilation.
Preprocessor Phases:
Why Use Preprocessor?
- Code Reusability: Include standard and custom headers
- Constants: Define symbolic constants
- Macros: Create inline functions
- Conditional Compilation: Platform-specific code
- Debugging: Include/exclude debug code
Preprocessor Directives
- #include: File inclusion
- #define: Macro definition
- #ifdef/#ifndef: Conditional compilation
- #if/#else/#endif: Conditional blocks
- #pragma: Compiler-specific instructions
- #error: Force compilation error
Important Preprocessor Facts
Preprocessor directives begin with # and are processed before compilation. They are not C statements (no semicolon needed). The preprocessor performs text substitution - it doesn't understand C syntax, only manipulates text.
C Preprocessor Directives Comparison
Here is a comprehensive comparison of all preprocessor directives in C with their key characteristics:
| Directive | Syntax | Purpose | Common Uses |
|---|---|---|---|
|
#include
File Inclusion
|
|
|
Standard headers
Custom headers
<> for system, "" for local files
|
|
#define
Macro Definition
|
|
|
Constants
Inline functions
No type checking, pure text replacement
|
|
#ifdef/#ifndef
Conditional Compilation
|
|
|
Portability
Debug builds
Compile different code based on definitions
|
|
#pragma
Compiler Specific
|
|
|
Compiler-specific
Non-portable
Implementation-defined behavior
|
The #include Directive - Complete Guide
The #include directive tells the preprocessor to insert the contents of another file into the current file. This is essential for including header files that contain function declarations, macros, and type definitions.
Syntax:
#include <filename> // System headers
#include "filename" // User headers
Search Order:
Key Characteristics:
- Angle brackets (<>): Search system/include directories only
- Double quotes (""): Search current directory first, then system directories
- No semicolon: Preprocessor directives don't end with semicolons
- Text insertion: File contents are literally inserted at the #include location
#include Directive Examples:
#include <stdio.h>
int main(void) {
printf("stdio included\n");
return 0;
}
/* mylib.h */
#ifndef MYLIB_H
#define MYLIB_H
int add(int a, int b);
#endif
/* main.c */
#include "mylib.h"
#include <stdio.h>
int main(void) { printf("%d\n", add(2, 3)); return 0; }
Always use include guards (#ifndef/#define/#endif) in header files to prevent multiple inclusions and compilation errors.
#define HEADER_NAME_H
// Header contents here
#endif // HEADER_NAME_H
The #define Directive and Macros
The #define directive creates macros - symbolic names that represent values or code fragments. Macros are replaced by their definitions during preprocessing.
Syntax:
#define IDENTIFIER replacement_text // Object-like macro
#define MACRO(parameters) replacement_text // Function-like macro
Macro Expansion Process:
How Macros Work:
area = PI * r * r;
Key Characteristics:
- Text substitution: Simple replacement, no type checking
- No memory allocation: Macros don't consume memory at runtime
- Scope: From point of definition to end of file or #undef
- Naming convention: Use uppercase for macros (common practice)
#define Directive Examples:
#include <stdio.h>
#define PI 3.14159
#define MAX 100
int main(void) {
printf("PI=%.5f MAX=%d\n", PI, MAX);
return 0;
}
#include <stdio.h>
#define SQUARE(x) ((x) * (x))
#define MAX(a,b) ((a) > (b) ? (a) : (b))
int main(void) {
printf("%d %d\n", SQUARE(5), MAX(3, 7));
return 0;
}
#include <stdio.h>
#define DOUBLE(x) ((x) + (x))
int main(void) {
printf("%d\n", DOUBLE(2 + 3));
return 0;
}
Macro Best Practices:
- Use parentheses: Always put parameters and entire expression in parentheses
- Avoid side effects: Don't use expressions with side effects as macro arguments
- Use uppercase names: Convention to distinguish macros from variables
- Prefer const over #define: Use const variables instead of macros when possible
- Keep macros simple: Complex logic should be in functions, not macros
- Use inline functions: For function-like macros, consider inline functions instead
- Document macros: Comment complex macros explaining their purpose and usage
Conditional Compilation Directives
Conditional compilation directives allow you to include or exclude code based on conditions that can be evaluated at compile time. This is essential for platform-specific code, debugging, and feature toggles.
Syntax:
#ifdef MACRO_NAME
// code to include if MACRO_NAME is defined
#endif
#ifndef MACRO_NAME
// code to include if MACRO_NAME is NOT defined
#endif
#if EXPRESSION
// code to include if EXPRESSION is true (non-zero)
#elif OTHER_EXPRESSION
// alternative code
#else
// default code
#endif
Conditional Compilation Flow:
Conditional Compilation Examples:
#include <stdio.h>
#define DEBUG 1
#if DEBUG
#define LOG(msg) printf("LOG: %s\n", msg)
#else
#define LOG(msg)
#endif
int main(void) {
LOG("started");
return 0;
}
#include <stdio.h>
#ifdef _WIN32
#define OS_NAME "Windows"
#else
#define OS_NAME "Unix-like"
#endif
int main(void) {
printf("OS: %s\n", OS_NAME);
return 0;
}
#include <stdio.h>
#define FEATURE_X 1
int main(void) {
#if FEATURE_X
printf("Feature X enabled\n");
#else
printf("Feature X disabled\n");
#endif
return 0;
}
Other Preprocessor Directives
Beyond the commonly used directives, C provides several other preprocessor directives for specialized tasks.
| Directive | Purpose | Example |
|---|---|---|
#undef |
Undefines a previously defined macro | #undef MACRO_NAME |
#pragma |
Compiler-specific instructions (implementation-defined) | #pragma once #pragma pack(1) |
#error |
Generates a compilation error with message | #error "Unsupported platform" |
#line |
Changes the current line number and filename | #line 100 "myfile.c" |
defined() |
Operator to test if a macro is defined (used with #if) | #if defined(MACRO) |
#include <stdio.h>
#line 100 "custom.c"
int main(void) {
printf("Other directives demo\n");
return 0;
}
Predefined Macros
The C preprocessor provides several predefined macros that give information about the compilation environment.
| Macro | Description | Example Value |
|---|---|---|
__FILE__ |
Current source filename as a string literal | "program.c" |
__LINE__ |
Current line number as a decimal constant | 42 |
__DATE__ |
Compilation date as a string (Mmm dd yyyy) | "Jan 25 2024" |
__TIME__ |
Compilation time as a string (hh:mm:ss) | "14:30:45" |
__func__ |
Current function name as a string (C99) | "main" |
__STDC__ |
Defined as 1 if compiler conforms to ISO C | 1 |
__STDC_VERSION__ |
C standard version (long integer) | 201112L (C11) |
__cplusplus |
Defined for C++ compilation | (not defined in C) |
#include <stdio.h>
int main(void) {
printf("File: %s\n", __FILE__);
printf("Line: %d\n", __LINE__);
return 0;
}
Common Mistakes and Best Practices
Preprocessor Best Practices:
- Use const instead of #define for constants: const provides type safety
- Use inline functions instead of complex macros: Better debugging and type checking
- Always use header guards: Prevent multiple inclusion
- Parenthesize macro arguments: Avoid operator precedence issues
- Use uppercase for macro names: Distinguish from variables/functions
- Keep macros simple: Complex logic belongs in functions
- Document conditional compilation: Explain why code is included/excluded
- Use #pragma sparingly: Compiler-specific code reduces portability
- Test both sides of conditionals: Ensure code works with/without features
- Use #error for required configuration: Fail early with helpful messages
When to Use Macros vs Alternatives:
| Use Case | Macro | Alternative | Recommendation |
|---|---|---|---|
| Simple constants | #define PI 3.14 | const double PI = 3.14; | Use const (type safety) |
| Inline code | #define SQUARE(x) ((x)*(x)) | inline int square(int x) { return x*x; } | Use inline function |
| Debugging | #ifdef DEBUG | if (debug_mode) {} | Use macros (compile-time) |
| Platform-specific code | #ifdef _WIN32 | Runtime detection | Use macros (compile-time) |
Key Takeaways
- C preprocessor directives begin with # and are processed before compilation
- #include inserts file contents (<> for system, "" for local files)
- #define creates macros for constants and code substitution
- Always use parentheses in macro definitions to avoid precedence issues
- #ifdef/#ifndef check if a macro is defined/not defined
- #if/#elif/#else/#endif provide conditional compilation
- Use header guards (#ifndef/#define/#endif) to prevent multiple inclusions
- Predefined macros like __FILE__, __LINE__, __DATE__ provide compilation info
- #error generates compilation errors with custom messages
- #pragma provides compiler-specific instructions (use sparingly)
- Prefer const variables over #define for constants (type safety)
- Prefer inline functions over complex macros (better debugging)
- Use conditional compilation for platform-specific code and debugging