Introduction to the C Preprocessor
The C preprocessor is a text processing tool that runs before the actual compilation begins. It handles directives (lines starting with #) to perform text substitution, file inclusion, and conditional compilation. Understanding the preprocessor is essential for writing portable, maintainable, and efficient C code.
What is the Preprocessor?
When you compile a C program, it goes through several stages. The preprocessor is the first
stage, transforming your source code before the compiler ever sees it. All preprocessor
directives start with a # symbol and are processed line by line.
Compilation Stages
Common Preprocessor Directives
| Directive | Purpose | Example |
|---|---|---|
#define |
Define a macro (constant or function-like) | #define PI 3.14159 |
#include |
Include contents of another file | #include <stdio.h> |
#ifdef / #ifndef |
Conditional compilation based on macro existence | #ifdef DEBUG |
#if / #elif / #else |
Conditional compilation with expressions | #if VERSION >= 2 |
#endif |
End conditional block | #endif |
#undef |
Undefine a macro | #undef MAX |
#pragma |
Compiler-specific instructions | #pragma once |
#error |
Generate a compilation error | #error "Not supported" |
Viewing Preprocessor Output
You can see what the preprocessor produces using the -E flag with gcc:
# Show preprocessor output (does not compile)
gcc -E program.c -o program.i
# Or display directly to terminal
gcc -E program.c | less
Key Insight
The preprocessor only does text substitution. It does not understand C syntax, types, or scope. This is both powerful (works anywhere) and dangerous (no type checking). Always be careful with macro side effects!
Practice Questions
Task: Which of these lines are preprocessor directives?
int main() {
#include <stdio.h>
int x = 10;
#define MAX 100
printf("Hello");
#ifdef DEBUG
return 0;
}
Show Solution
Preprocessor directives (lines starting with #):
#include <stdio.h>- File inclusion#define MAX 100- Macro definition#ifdef DEBUG- Conditional compilation
Note: The placement of #include inside main() is unusual and bad practice, but technically valid for the preprocessor (it just inserts text).
Task: What does the following code become after preprocessing?
#define VALUE 42
#define DOUBLE(x) x * 2
int result = DOUBLE(VALUE);
Show Solution
// After preprocessing:
int result = 42 * 2;
First, DOUBLE(VALUE) expands to VALUE * 2, then VALUE expands to 42.
Macro Definition and Expansion
Macros are the most powerful feature of the C preprocessor. They allow you to define constants, create inline code substitutions, and build code generation tools. However, they must be used carefully to avoid subtle bugs that can be hard to debug.
Object-like Macros (Constants)
The simplest macros define constants that are replaced throughout your code:
#define PI 3.14159265359
#define MAX_BUFFER_SIZE 1024
#define APP_NAME "My Application"
#define NEWLINE '\n'
// Usage
double circumference = 2 * PI * radius;
char buffer[MAX_BUFFER_SIZE];
printf("Welcome to %s%c", APP_NAME, NEWLINE);
#define
#define creates a macro that tells the preprocessor to replace all occurrences of the macro name with its replacement text. The replacement happens before compilation, as simple text substitution.
Convention: Macro names are typically written in ALL_CAPS to distinguish them from variables and functions.
Function-like Macros
Macros can take parameters, making them act like inline functions:
// 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))
// Usage
int result = SQUARE(5); // Expands to: ((5) * (5))
int bigger = MAX(10, 20); // Expands to: ((10) > (20) ? (10) : (20))
int absolute = ABS(-42); // Expands to: ((-42) < 0 ? -(-42) : (-42))
Why All the Parentheses?
Parentheses are critical in macros to prevent operator precedence bugs:
// BAD: Missing parentheses
#define SQUARE_BAD(x) x * x
int a = SQUARE_BAD(3 + 2);
// Expands to: 3 + 2 * 3 + 2
// Evaluates as: 3 + 6 + 2 = 11 (wrong!)
// GOOD: Proper parentheses
#define SQUARE_GOOD(x) ((x) * (x))
int b = SQUARE_GOOD(3 + 2);
// Expands to: ((3 + 2) * (3 + 2))
// Evaluates as: 5 * 5 = 25 (correct!)
Multi-line Macros
Use backslash \ to continue a macro on the next line:
// Multi-line macro with do-while(0) pattern
#define SWAP(a, b, type) do { \
type temp = a; \
a = b; \
b = temp; \
} while(0)
// Usage (note: no semicolon issues)
int x = 5, y = 10;
SWAP(x, y, int);
printf("x=%d, y=%d\n", x, y); // x=10, y=5
Stringification and Token Pasting
Special operators let you manipulate macro arguments as text:
// # - Stringification (convert to string literal)
#define STRINGIFY(x) #x
#define PRINT_VAR(var) printf(#var " = %d\n", var)
int count = 42;
PRINT_VAR(count); // Expands to: printf("count" " = %d\n", count);
// Prints: count = 42
// ## - Token pasting (concatenate tokens)
#define MAKE_FUNC(name) void func_##name() { printf(#name "\n"); }
MAKE_FUNC(hello) // Creates: void func_hello() { printf("hello\n"); }
MAKE_FUNC(world) // Creates: void func_world() { printf("world\n"); }
Macro Pitfalls: Side Effects
Danger: Double Evaluation
Macro arguments are substituted literally, so expressions with side effects can be evaluated multiple times:
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int i = 5;
int result = MAX(i++, 3); // Expands to: ((i++) > (3) ? (i++) : (3))
// i gets incremented TWICE if i > 3!
// After: i = 7, result = 6 (not what you expected!)
Undefining Macros
#define DEBUG 1
// ... some code using DEBUG ...
#undef DEBUG // Remove the macro definition
// DEBUG is no longer defined here
#ifdef DEBUG
printf("This won't compile\n");
#endif
Practice Questions
Task: Define a macro ARRAY_SIZE that calculates the number of elements in an array.
Show Solution
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
// Usage
int numbers[] = {1, 2, 3, 4, 5};
printf("Array has %zu elements\n", ARRAY_SIZE(numbers)); // 5
char name[] = "Hello";
printf("String array size: %zu\n", ARRAY_SIZE(name)); // 6 (includes '\0')
Task: Create a DEBUG_PRINT macro that prints the variable name and value, but only when DEBUG is defined.
Show Solution
#define DEBUG // Comment out to disable
#ifdef DEBUG
#define DEBUG_PRINT(var, fmt) \
printf("[DEBUG] %s = " fmt "\n", #var, var)
#else
#define DEBUG_PRINT(var, fmt) // Empty - does nothing
#endif
// Usage
int count = 42;
double pi = 3.14159;
char *name = "Alice";
DEBUG_PRINT(count, "%d"); // [DEBUG] count = 42
DEBUG_PRINT(pi, "%.2f"); // [DEBUG] pi = 3.14
DEBUG_PRINT(name, "%s"); // [DEBUG] name = Alice
Task: Create MIN/MAX macros that evaluate arguments only once using GCC statement expressions.
Show Solution
// GCC extension: statement expressions ({...})
// Evaluates each argument only once!
#define SAFE_MAX(a, b) ({ \
typeof(a) _a = (a); \
typeof(b) _b = (b); \
_a > _b ? _a : _b; \
})
#define SAFE_MIN(a, b) ({ \
typeof(a) _a = (a); \
typeof(b) _b = (b); \
_a < _b ? _a : _b; \
})
// Now safe to use with side effects
int i = 5;
int result = SAFE_MAX(i++, 3);
// i is incremented only once!
// After: i = 6, result = 5
// Note: This is a GCC extension, not standard C
Conditional Compilation
Conditional compilation allows you to include or exclude code based on compile-time conditions. This is essential for platform-specific code, debug builds, feature toggles, and preventing header files from being included multiple times (include guards).
#ifdef and #ifndef
Check if a macro is defined or not defined:
// #ifdef - if defined
#ifdef DEBUG
printf("Debug mode enabled\n");
printf("Variable x = %d\n", x);
#endif
// #ifndef - if NOT defined
#ifndef RELEASE
// This code only compiles in non-release builds
runTests();
#endif
// With #else
#ifdef _WIN32
#include <windows.h>
#define CLEAR_SCREEN system("cls")
#else
#include <unistd.h>
#define CLEAR_SCREEN system("clear")
#endif
#if, #elif, #else
Use expressions and multiple conditions:
// Check macro values
#define VERSION 3
#if VERSION == 1
printf("Version 1.0\n");
#elif VERSION == 2
printf("Version 2.0\n");
#elif VERSION >= 3
printf("Version 3.0 or later\n");
#else
printf("Unknown version\n");
#endif
// Combine conditions with && and ||
#if defined(DEBUG) && defined(VERBOSE)
#define LOG(msg) printf("[DEBUG] %s\n", msg)
#elif defined(DEBUG)
#define LOG(msg) printf("%s\n", msg)
#else
#define LOG(msg) // Empty - no logging
#endif
Include Guards
The most common use of conditional compilation is preventing double inclusion of headers:
// myheader.h
#ifndef MYHEADER_H // If MYHEADER_H is not defined...
#define MYHEADER_H // Define it
// Header contents go here
typedef struct {
int x, y;
} Point;
void doSomething(void);
#endif // End of include guard
// Alternative: #pragma once (non-standard but widely supported)
#pragma once
typedef struct {
int x, y;
} Point;
void doSomething(void);
Platform-Specific Code
// Detect operating system
#if defined(_WIN32) || defined(_WIN64)
#define PLATFORM "Windows"
#define PATH_SEPARATOR '\\'
#elif defined(__APPLE__) && defined(__MACH__)
#define PLATFORM "macOS"
#define PATH_SEPARATOR '/'
#elif defined(__linux__)
#define PLATFORM "Linux"
#define PATH_SEPARATOR '/'
#else
#define PLATFORM "Unknown"
#define PATH_SEPARATOR '/'
#endif
printf("Running on %s\n", PLATFORM);
Feature Toggles
// config.h - Feature flags
#define FEATURE_LOGGING 1
#define FEATURE_ENCRYPTION 0
#define FEATURE_ANALYTICS 1
// In code
#if FEATURE_LOGGING
#include "logger.h"
#define LOG(msg) log_message(msg)
#else
#define LOG(msg)
#endif
#if FEATURE_ENCRYPTION
#include "crypto.h"
void saveData(const char *data) {
char *encrypted = encrypt(data);
writeFile(encrypted);
free(encrypted);
}
#else
void saveData(const char *data) {
writeFile(data);
}
#endif
#error and #warning
// Generate compile-time errors
#ifndef CONFIG_FILE
#error "CONFIG_FILE must be defined"
#endif
#if BUFFER_SIZE < 256
#error "BUFFER_SIZE must be at least 256"
#endif
// Generate compile-time warnings (GCC extension)
#if VERSION < 2
#warning "Version 1.x is deprecated, please upgrade"
#endif
Define Macros from Command Line
You can define macros when compiling without changing source code:
gcc -DDEBUG program.c - defines DEBUG
gcc -DVERSION=3 program.c - defines VERSION as 3
Practice Questions
Task: Write proper include guards for a header file called "utils.h".
Show Solution
// utils.h
#ifndef UTILS_H
#define UTILS_H
// Function declarations
int max(int a, int b);
int min(int a, int b);
void swap(int *a, int *b);
// Type definitions
typedef unsigned int uint;
#endif // UTILS_H
Task: Create a set of macros that provide ASSERT, LOG, and TRACE functionality only in debug builds.
Show Solution
// debug.h
#ifndef DEBUG_H
#define DEBUG_H
#include <stdio.h>
#include <stdlib.h>
#ifdef DEBUG
#define ASSERT(cond) do { \
if (!(cond)) { \
fprintf(stderr, "Assertion failed: %s\n", #cond); \
fprintf(stderr, " File: %s, Line: %d\n", __FILE__, __LINE__); \
abort(); \
} \
} while(0)
#define LOG(fmt, ...) \
printf("[LOG] " fmt "\n", ##__VA_ARGS__)
#define TRACE() \
printf("[TRACE] %s() at %s:%d\n", __func__, __FILE__, __LINE__)
#else
#define ASSERT(cond) // Empty
#define LOG(fmt, ...) // Empty
#define TRACE() // Empty
#endif
#endif // DEBUG_H
// Usage:
// gcc -DDEBUG program.c (debug build)
// gcc program.c (release build - macros do nothing)
Task: Create a portable SLEEP(ms) macro that works on Windows, Linux, and macOS.
Show Solution
// portable_sleep.h
#ifndef PORTABLE_SLEEP_H
#define PORTABLE_SLEEP_H
#if defined(_WIN32) || defined(_WIN64)
#include <windows.h>
#define SLEEP(ms) Sleep(ms)
#elif defined(__APPLE__) || defined(__linux__) || defined(__unix__)
#include <unistd.h>
#define SLEEP(ms) usleep((ms) * 1000)
#else
#error "Unsupported platform for SLEEP macro"
#endif
#endif // PORTABLE_SLEEP_H
// Usage
#include "portable_sleep.h"
int main() {
printf("Waiting 2 seconds...\n");
SLEEP(2000); // Sleep for 2000 milliseconds
printf("Done!\n");
return 0;
}
The #include Directive
The #include directive is fundamental to C programming. It tells the preprocessor to insert the contents of another file at that point in your code. Understanding how include works and the difference between angle brackets and quotes is essential.
Two Forms of #include
#include <header.h>
Angle brackets for system/standard headers
- Searches system include directories
- Used for standard library headers
- Compiler knows where to find these
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include "header.h"
Quotes for project/local headers
- Searches current directory first
- Then searches system directories
- Used for your own header files
#include "myheader.h"
#include "utils/helpers.h"
#include "../common/types.h"
How #include Works
The preprocessor literally copies and pastes the file contents:
// math_utils.h
int add(int a, int b);
int subtract(int a, int b);
// main.c
#include "math_utils.h"
int main() {
return add(5, 3);
}
// After preprocessing, main.c becomes:
int add(int a, int b);
int subtract(int a, int b);
int main() {
return add(5, 3);
}
Include Search Paths
You can add custom directories to the include search path:
# Add include directory with -I flag
gcc -I./include -I../common main.c -o program
# Now these work:
#include "myheader.h" # Searches ./include and ../common
#include <myheader.h> # Also searches these paths
Standard Library Headers
| Header | Purpose | Key Functions/Macros |
|---|---|---|
<stdio.h> |
Standard I/O | printf, scanf, fopen, fclose |
<stdlib.h> |
General utilities | malloc, free, atoi, exit, rand |
<string.h> |
String handling | strlen, strcpy, strcmp, memcpy |
<math.h> |
Math functions | sqrt, sin, cos, pow, log |
<ctype.h> |
Character handling | isalpha, isdigit, toupper, tolower |
<time.h> |
Date and time | time, clock, strftime, difftime |
<stdbool.h> |
Boolean type (C99) | bool, true, false |
<stdint.h> |
Fixed-width integers (C99) | int32_t, uint8_t, INT_MAX |
Nested Includes and Dependencies
// types.h
#ifndef TYPES_H
#define TYPES_H
typedef struct { int x, y; } Point;
#endif
// graphics.h
#ifndef GRAPHICS_H
#define GRAPHICS_H
#include "types.h" // Needs Point type
void drawPoint(Point p);
void drawLine(Point a, Point b);
#endif
// main.c
#include "types.h" // Included first
#include "graphics.h" // Also includes types.h, but guards prevent duplicate
// Without include guards, Point would be defined twice = error!
Best Practices for #include
- Always use include guards in your headers
- Include what you use directly, do not rely on transitive includes
- Put system headers before local headers
- Order includes alphabetically within each group
- Do not include .c files, only .h files
Practice Questions
Task: Which include style (angle brackets or quotes) should be used for each?
- stdio.h
- myproject.h (your own header)
- math.h
- ../utils/helper.h
Show Solution
#include <stdio.h> // System header - angle brackets
#include "myproject.h" // Local header - quotes
#include <math.h> // System header - angle brackets
#include "../utils/helper.h" // Local header with path - quotes
Task: This code gives "redefinition" errors. Fix it:
// point.h
typedef struct { int x, y; } Point;
// rect.h
#include "point.h"
typedef struct { Point topLeft, bottomRight; } Rect;
// main.c
#include "point.h"
#include "rect.h"
// Error: Point redefined!
Show Solution
// point.h - Add include guards
#ifndef POINT_H
#define POINT_H
typedef struct { int x, y; } Point;
#endif
// rect.h - Add include guards
#ifndef RECT_H
#define RECT_H
#include "point.h"
typedef struct { Point topLeft, bottomRight; } Rect;
#endif
// main.c - Now works correctly
#include "point.h" // Defines Point
#include "rect.h" // Includes point.h again, but guards skip it
Predefined Macros
C provides several predefined macros that are automatically available in every program. These are invaluable for debugging, logging, conditional compilation, and creating informative error messages. They give you information about the compilation environment.
Standard Predefined Macros
| Macro | Description | Example Value |
|---|---|---|
__FILE__ |
Current source file name (string) | "main.c" |
__LINE__ |
Current line number (integer) | 42 |
__func__ |
Current function name (C99, string) | "calculateSum" |
__DATE__ |
Compilation date (string) | "Feb 1 2026" |
__TIME__ |
Compilation time (string) | "14:30:00" |
__STDC__ |
1 if compiler conforms to C standard | 1 |
__STDC_VERSION__ |
C standard version (C99+) | 201710L (C17) |
Using Predefined Macros for Debugging
#include <stdio.h>
// Debug logging with file and line info
#define DEBUG_LOG(msg) \
printf("[%s:%d] %s\n", __FILE__, __LINE__, msg)
// Assert with detailed error info
#define ASSERT(condition) do { \
if (!(condition)) { \
fprintf(stderr, "Assertion failed: %s\n", #condition); \
fprintf(stderr, " Function: %s\n", __func__); \
fprintf(stderr, " File: %s\n", __FILE__); \
fprintf(stderr, " Line: %d\n", __LINE__); \
abort(); \
} \
} while(0)
void processData(int *data, int count) {
ASSERT(data != NULL);
ASSERT(count > 0);
DEBUG_LOG("Processing started");
// ... process data ...
DEBUG_LOG("Processing complete");
}
int main() {
processData(NULL, 5); // This will trigger assertion
return 0;
}
Build Information
#include <stdio.h>
void printBuildInfo() {
printf("=== Build Information ===\n");
printf("Compiled on: %s at %s\n", __DATE__, __TIME__);
printf("Source file: %s\n", __FILE__);
#ifdef __STDC_VERSION__
printf("C Standard: ");
#if __STDC_VERSION__ >= 201710L
printf("C17\n");
#elif __STDC_VERSION__ >= 201112L
printf("C11\n");
#elif __STDC_VERSION__ >= 199901L
printf("C99\n");
#else
printf("C89/C90\n");
#endif
#endif
#ifdef __GNUC__
printf("Compiler: GCC %d.%d.%d\n",
__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__);
#elif defined(_MSC_VER)
printf("Compiler: MSVC %d\n", _MSC_VER);
#elif defined(__clang__)
printf("Compiler: Clang %d.%d\n",
__clang_major__, __clang_minor__);
#endif
}
Compiler-Specific Macros
// GCC-specific macros
#ifdef __GNUC__
#define UNUSED __attribute__((unused))
#define DEPRECATED __attribute__((deprecated))
#define PRINTF_FORMAT(a, b) __attribute__((format(printf, a, b)))
#else
#define UNUSED
#define DEPRECATED
#define PRINTF_FORMAT(a, b)
#endif
// Usage
UNUSED static void helperFunc() { }
DEPRECATED void oldFunction() {
// Compiler warns when this is used
}
PRINTF_FORMAT(1, 2)
void myPrintf(const char *fmt, ...) {
// Compiler checks format string
}
Creating a Comprehensive Logger
// logger.h
#ifndef LOGGER_H
#define LOGGER_H
#include <stdio.h>
#include <time.h>
typedef enum { LOG_DEBUG, LOG_INFO, LOG_WARN, LOG_ERROR } LogLevel;
#define LOG(level, fmt, ...) do { \
time_t now = time(NULL); \
struct tm *t = localtime(&now); \
char timeStr[20]; \
strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", t); \
const char *levelStr[] = {"DEBUG", "INFO", "WARN", "ERROR"}; \
fprintf(stderr, "[%s] [%s] [%s:%d] " fmt "\n", \
timeStr, levelStr[level], __FILE__, __LINE__, ##__VA_ARGS__); \
} while(0)
#define LOG_DEBUG(fmt, ...) LOG(LOG_DEBUG, fmt, ##__VA_ARGS__)
#define LOG_INFO(fmt, ...) LOG(LOG_INFO, fmt, ##__VA_ARGS__)
#define LOG_WARN(fmt, ...) LOG(LOG_WARN, fmt, ##__VA_ARGS__)
#define LOG_ERROR(fmt, ...) LOG(LOG_ERROR, fmt, ##__VA_ARGS__)
#endif
// Usage
LOG_INFO("Server started on port %d", 8080);
LOG_WARN("Connection timeout after %d seconds", 30);
LOG_ERROR("Failed to open file: %s", filename);
The ##__VA_ARGS__ Trick
In macros with variable arguments (...), ##__VA_ARGS__ removes the
trailing comma if no arguments are passed. This is a GCC extension that prevents
syntax errors when calling LOG_INFO("message") without extra arguments.
Practice Questions
Task: Write a function that prints "Built on [date] at [time]" using predefined macros.
Show Solution
#include <stdio.h>
void printBuildTimestamp() {
printf("Built on %s at %s\n", __DATE__, __TIME__);
}
int main() {
printBuildTimestamp();
// Output: Built on Feb 1 2026 at 14:30:00
return 0;
}
Task: Create TRACE_ENTER and TRACE_EXIT macros that print function entry and exit.
Show Solution
#include <stdio.h>
#ifdef TRACE_ENABLED
#define TRACE_ENTER() \
printf("--> Entering %s() [%s:%d]\n", __func__, __FILE__, __LINE__)
#define TRACE_EXIT() \
printf("<-- Exiting %s() [%s:%d]\n", __func__, __FILE__, __LINE__)
#else
#define TRACE_ENTER()
#define TRACE_EXIT()
#endif
void innerFunction() {
TRACE_ENTER();
printf("Doing work...\n");
TRACE_EXIT();
}
void outerFunction() {
TRACE_ENTER();
innerFunction();
TRACE_EXIT();
}
int main() {
TRACE_ENTER();
outerFunction();
TRACE_EXIT();
return 0;
}
// Compile with: gcc -DTRACE_ENABLED program.c
Task: Create a STATIC_ASSERT macro that generates a compile-time error if a condition is false.
Show Solution
// For C11 and later, use _Static_assert
#if __STDC_VERSION__ >= 201112L
#define STATIC_ASSERT(cond, msg) _Static_assert(cond, msg)
#else
// Pre-C11 fallback: creates array with negative size if false
#define STATIC_ASSERT(cond, msg) \
typedef char static_assertion_##__LINE__[(cond) ? 1 : -1]
#endif
// Usage examples
STATIC_ASSERT(sizeof(int) == 4, "int must be 4 bytes");
STATIC_ASSERT(sizeof(void*) >= 4, "Pointer must be at least 4 bytes");
typedef struct {
int id;
char name[32];
float value;
} Record;
// Ensure struct fits in expected size
STATIC_ASSERT(sizeof(Record) <= 64, "Record struct too large");
int main() {
// If any assertion fails, compilation stops with an error
return 0;
}
Key Takeaways
Preprocessor First
The preprocessor runs before compilation, doing text substitution on # directives
#define Macros
Create constants and function-like macros, always use parentheses for safety
Conditional Compilation
#ifdef, #ifndef, #if for platform code, debug builds, and feature toggles
#include Properly
Angle brackets for system headers, quotes for local headers, always use include guards
Predefined Macros
Use __FILE__, __LINE__, __func__ for debugging and informative error messages
Macro Pitfalls
Watch for double evaluation and side effects, macros have no type safety
Knowledge Check
Quick Quiz
Test what you have learned about C preprocessor directives