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:
// System headers - use angle brackets
#include <stdio.h> // Standard I/O functions
#include <stdlib.h> // Standard library functions
#include <string.h> // String manipulation functions
#include <math.h> // Mathematical functions
#include <time.h> // Time and date functions
// User headers - use double quotes
#include "myheader.h" // Your custom header file
#include "../utils.h" // Header in parent directory
#include "config.h" // Configuration header
int main() {
printf("Hello, Preprocessor!\n");
return 0;
}
/********** config.h **********/
#ifndef CONFIG_H
#define CONFIG_H
// Configuration constants
#define MAX_USERS 100
#define BUFFER_SIZE 1024
#define DEBUG_MODE 1
#define VERSION "1.0.0"
#endif // CONFIG_H
/********** utils.h **********/
#ifndef UTILS_H
#define UTILS_H
// Function declarations
void print_banner();
int calculate_sum(int a, int b);
void log_message(const char* message);
#endif // UTILS_H
/********** main.c **********/
#include <stdio.h>
#include "config.h"
#include "utils.h"
int main() {
printf("Program Version: %s\n", VERSION);
printf("Max Users: %d\n", MAX_USERS);
print_banner();
int result = calculate_sum(10, 20);
printf("Sum: %d\n", result);
#if DEBUG_MODE
log_message("Debug mode is active");
#endif
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>
// Simple constants
#define PI 3.14159265359
#define MAX_SIZE 100
#define PROGRAM_NAME "My Application"
#define NEWLINE '\n'
#define TAB '\t'
// Expressions as macros
#define MINUTES_PER_HOUR 60
#define HOURS_PER_DAY 24
#define MINUTES_PER_DAY (MINUTES_PER_HOUR * HOURS_PER_DAY)
// Conditional values
#define DEBUG 1
#define VERSION 2
int main() {
double radius = 5.0;
double area = PI * radius * radius;
double circumference = 2 * PI * radius;
printf("Program: %s\n", PROGRAM_NAME);
printf("Radius: %.2f\n", radius);
printf("Area: %.2f\n", area);
printf("Circumference: %.2f\n", circumference);
printf("Minutes in a day: %d\n", MINUTES_PER_DAY);
#if DEBUG
printf("Debug information printed...\n");
#endif
return 0;
}
#include <stdio.h>
// Simple function-like macros
#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 (use backslash for continuation)
#define PRINT_ERROR(msg) \
printf("ERROR: %s\n", msg); \
printf("File: %s, Line: %d\n", __FILE__, __LINE__)
// Stringizing operator (#)
#define STRINGIZE(x) #x
// Token pasting operator (##)
#define CONCAT(a, b) a##b
int main() {
int num1 = 10, num2 = 20;
printf("Square of %d: %d\n", num1, SQUARE(num1));
printf("Max of %d and %d: %d\n", num1, num2, MAX(num1, num2));
printf("Absolute of -15: %d\n", ABS(-15));
PRINT_ERROR("Division by zero");
// Stringizing example
printf("Stringized: %s\n", STRINGIZE(Hello World));
// Token pasting example
int value1 = 100, value2 = 200;
int result = CONCAT(value, 1) + CONCAT(value, 2);
printf("Concatenated result: %d\n", result);
return 0;
}
#include <stdio.h>
// DANGEROUS: Missing parentheses
#define SQUARE_BAD(x) x * x
// SAFE: Parentheses around parameters and whole expression
#define SQUARE_GOOD(x) ((x) * (x))
// DANGEROUS: Multiple evaluation
#define MAX_BAD(a, b) ((a) > (b) ? (a) : (b))
int main() {
printf("=== MACRO PITFALLS DEMONSTRATION ===\n\n");
// Problem 1: Operator precedence
int x = 5;
printf("With SQUARE_BAD: %d + 1 squared = ", x);
printf("%d (WRONG!)\n", SQUARE_BAD(x + 1)); // Expands to: x + 1 * x + 1
printf("With SQUARE_GOOD: %d + 1 squared = ", x);
printf("%d (CORRECT)\n", SQUARE_GOOD(x + 1)); // Expands to: ((x + 1) * (x + 1))
// Problem 2: Multiple evaluation
int counter = 0;
int a = 5;
int b = MAX_BAD(++a, 10); // Expands to: ((++a) > (10) ? (++a) : (10))
printf("\nAfter MAX_BAD(++a, 10):\n");
printf("a = %d (expected 6, got %d)\n", a, a);
printf("b = %d\n", b);
// Problem 3: Side effects in macros
#define INCREMENT_BAD(x) x++
#define INCREMENT_GOOD(x) ((x) + 1)
int val = 5;
int result = INCREMENT_BAD(val) * 2;
printf("\nINCREMENT_BAD(val) * 2 = %d\n", result);
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_LEVEL during compilation or here
// Compile with: gcc -DDEBUG_LEVEL=2 program.c
#ifndef DEBUG_LEVEL
#define DEBUG_LEVEL 0 // Default: no debugging
#endif
// Debug macros based on level
#if DEBUG_LEVEL >= 1
#define DEBUG_PRINT(msg) printf("[DEBUG] %s\n", msg)
#else
#define DEBUG_PRINT(msg) // Empty macro - no code generated
#endif
#if DEBUG_LEVEL >= 2
#define DEBUG_PRINT_DETAILED(msg, value) \
printf("[DEBUG] %s: %d\n", msg, value)
#else
#define DEBUG_PRINT_DETAILED(msg, value)
#endif
#if DEBUG_LEVEL >= 3
#define DEBUG_PRINT_VERBOSE(msg, file, line) \
printf("[DEBUG] %s (File: %s, Line: %d)\n", msg, file, line)
#else
#define DEBUG_PRINT_VERBOSE(msg, file, line)
#endif
int factorial(int n) {
DEBUG_PRINT("Entering factorial function");
DEBUG_PRINT_DETAILED("Parameter n", n);
if (n < 0) {
DEBUG_PRINT_VERBOSE("Negative input", __FILE__, __LINE__);
return -1;
}
int result = 1;
for (int i = 1; i <= n; i++) {
result *= i;
DEBUG_PRINT_DETAILED("Loop iteration", i);
}
DEBUG_PRINT_DETAILED("Result", result);
DEBUG_PRINT("Exiting factorial function");
return result;
}
int main() {
printf("Debug Level: %d\n", DEBUG_LEVEL);
int num = 5;
int fact = factorial(num);
if (fact > 0) {
printf("Factorial of %d is %d\n", num, fact);
} else {
printf("Invalid input!\n");
}
return 0;
}
#include <stdio.h>
// Platform detection (usually defined by compiler)
// Common predefined macros:
// __linux__ - Linux systems
// _WIN32, _WIN64 - Windows systems
// __APPLE__ - macOS systems
// __unix__ - UNIX systems
// __x86_64__ - 64-bit x86 architecture
// __i386__ - 32-bit x86 architecture
int main() {
printf("=== PLATFORM INFORMATION ===\n\n");
// Operating System
#ifdef __linux__
printf("Operating System: Linux\n");
#define PLATFORM_NAME "Linux"
#elif defined(_WIN32) || defined(_WIN64)
printf("Operating System: Windows\n");
#define PLATFORM_NAME "Windows"
#elif defined(__APPLE__)
printf("Operating System: macOS\n");
#define PLATFORM_NAME "macOS"
#else
printf("Operating System: Unknown\n");
#define PLATFORM_NAME "Unknown"
#endif
// Architecture
#ifdef __x86_64__
printf("Architecture: 64-bit x86\n");
#elif defined(__i386__)
printf("Architecture: 32-bit x86\n");
#elif defined(__arm__)
printf("Architecture: ARM\n");
#else
printf("Architecture: Unknown\n");
#endif
// Compiler
#ifdef __GNUC__
printf("Compiler: GCC version %d.%d.%d\n",
__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__);
#elif defined(_MSC_VER)
printf("Compiler: Microsoft Visual C++\n");
#else
printf("Compiler: Unknown\n");
#endif
// Compilation date and time
printf("Compiled on: %s at %s\n", __DATE__, __TIME__);
// Platform-specific code
printf("\n=== PLATFORM-SPECIFIC GREETING ===\n");
#ifdef __linux__
printf("Hello Linux user!\n");
// Linux-specific code here
#elif defined(_WIN32) || defined(_WIN64)
printf("Hello Windows user!\n");
// Windows-specific code here
#elif defined(__APPLE__)
printf("Hello macOS user!\n");
// macOS-specific code here
#else
printf("Hello from an unknown platform!\n");
#endif
return 0;
}
#include <stdio.h>
// Feature toggles (can be defined during compilation)
// Compile with: gcc -DFEATURE_A -DFEATURE_B program.c
// Version information
#define SOFTWARE_VERSION_MAJOR 2
#define SOFTWARE_VERSION_MINOR 1
#define SOFTWARE_VERSION_PATCH 0
int main() {
printf("Software Version: %d.%d.%d\n\n",
SOFTWARE_VERSION_MAJOR,
SOFTWARE_VERSION_MINOR,
SOFTWARE_VERSION_PATCH);
printf("=== ACTIVE FEATURES ===\n");
// Check and enable features
#ifdef FEATURE_A
printf("✓ Feature A is ENABLED\n");
// Feature A implementation
printf(" Performing Feature A operations...\n");
#else
printf("✗ Feature A is DISABLED\n");
#endif
#ifdef FEATURE_B
printf("✓ Feature B is ENABLED\n");
// Feature B implementation
printf(" Performing Feature B operations...\n");
#else
printf("✗ Feature B is DISABLED\n");
#endif
#ifdef FEATURE_C
printf("✓ Feature C is ENABLED\n");
printf(" Performing Feature C operations...\n");
#else
printf("✗ Feature C is DISABLED\n");
#endif
printf("\n=== BUILD CONFIGURATION ===\n");
// Build type
#if defined(DEBUG_BUILD)
printf("Build Type: DEBUG\n");
printf(" - Debug symbols included\n");
printf(" - Optimizations disabled\n");
#elif defined(RELEASE_BUILD)
printf("Build Type: RELEASE\n");
printf(" - Debug symbols stripped\n");
printf(" - Optimizations enabled\n");
#else
printf("Build Type: DEFAULT\n");
#endif
// Conditional code based on version
#if SOFTWARE_VERSION_MAJOR >= 2
printf("\nVersion 2.0+ features available:\n");
printf(" - New user interface\n");
printf(" - Enhanced security\n");
#if SOFTWARE_VERSION_MINOR >= 1
printf(" - Performance improvements (v2.1+)\n");
#endif
#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>
// #undef example
#define TEMP_MACRO 100
int main() {
printf("=== #undef DIRECTIVE ===\n");
printf("TEMP_MACRO before #undef: %d\n", TEMP_MACRO);
#undef TEMP_MACRO
// This would cause an error if uncommented:
// printf("TEMP_MACRO after #undef: %d\n", TEMP_MACRO);
printf("\n=== #error DIRECTIVE ===\n");
// Platform validation
#if !defined(__linux__) && !defined(_WIN32) && !defined(__APPLE__)
#error "Unsupported platform! This code requires Linux, Windows, or macOS."
#endif
// Version validation
#ifndef REQUIRED_VERSION
#define REQUIRED_VERSION 2
#endif
#if SOFTWARE_VERSION_MAJOR < REQUIRED_VERSION
#error "This program requires version 2.0 or higher"
#endif
printf("\n=== #line DIRECTIVE ===\n");
// Report original line numbers
printf("Current line: %d\n", __LINE__);
printf("Current file: %s\n", __FILE__);
// Change line number and filename
#line 1000 "custom_file.c"
printf("New line: %d\n", __LINE__);
printf("New file: %s\n", __FILE__);
// Restore original
#line __LINE__ "other-directives.c"
printf("\n=== #pragma DIRECTIVE ===\n");
// Common #pragma directives (compiler-specific)
// GCC: Ignore specific warnings
// #pragma GCC diagnostic ignored "-Wunused-variable"
// Visual C++: Pack structure to 1-byte alignment
// #pragma pack(push, 1)
// struct PackedStruct {
// char a;
// int b;
// };
// #pragma pack(pop)
// Once-only headers (non-standard but widely supported)
// #pragma once
printf("\n=== defined() OPERATOR ===\n");
#if defined(DEBUG_MODE) && DEBUG_MODE == 1
printf("Debug mode is active\n");
#elif defined(DEBUG_MODE)
printf("Debug mode is defined but not equal to 1\n");
#else
printf("Debug mode is not defined\n");
#endif
// Combined conditions
#if defined(FEATURE_A) || defined(FEATURE_B)
printf("Either Feature A or Feature B is enabled\n");
#endif
#if defined(FEATURE_A) && defined(FEATURE_B)
printf("Both Feature A and Feature B are enabled\n");
#endif
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>
void display_compilation_info() {
printf("=== COMPILATION INFORMATION ===\n\n");
printf("Source file: %s\n", __FILE__);
printf("Current line: %d\n", __LINE__);
printf("Compilation date: %s\n", __DATE__);
printf("Compilation time: %s\n", __TIME__);
printf("Current function: %s\n", __func__);
#ifdef __STDC__
printf("ISO C compliant: Yes (__STDC__ = %d)\n", __STDC__);
#else
printf("ISO C compliant: No\n");
#endif
#ifdef __STDC_VERSION__
printf("C standard version: %ld\n", __STDC_VERSION__);
// Interpret version
#if __STDC_VERSION__ >= 201112L
printf(" C11 or later\n");
#elif __STDC_VERSION__ >= 199901L
printf(" C99\n");
#elif __STDC_VERSION__ >= 199409L
printf(" C89 with Amendment 1\n");
#else
printf(" C89/C90\n");
#endif
#else
printf("C standard version: Pre-C89\n");
#endif
#ifdef __cplusplus
printf("Compiling as C++\n");
#else
printf("Compiling as C\n");
#endif
}
// Custom assert macro using predefined macros
#define CUSTOM_ASSERT(condition) \
do { \
if (!(condition)) { \
printf("Assertion failed: %s\n", #condition); \
printf("File: %s, Line: %d\n", __FILE__, __LINE__); \
printf("Function: %s\n", __func__); \
return -1; \
} \
} while(0)
int process_data(int value) {
CUSTOM_ASSERT(value >= 0);
CUSTOM_ASSERT(value < 1000);
printf("Processing value: %d\n", value);
return value * 2;
}
int main() {
display_compilation_info();
printf("\n=== CUSTOM ASSERT DEMO ===\n");
// This will succeed
int result1 = process_data(50);
printf("Result 1: %d\n", result1);
// This will fail the assertion
printf("\nTesting with invalid data:\n");
int result2 = process_data(-5); // This triggers assertion
printf("Result 2: %d\n", result2); // This line won't be reached
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