The C Preprocessor
Learn #define, #include, #ifdef, function-like macros, include guards, and #pragma in C.
#include — Including Headers
#include is processed before compilation begins. The preprocessor replaces the directive with the full text of the named file. This is how function prototypes, type definitions, and macro definitions are shared across multiple source files without duplication.
/* System headers — searched in compiler's include path */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
/* Project headers — searched relative to current file first */
#include "myproject.h"
#include "utils/helpers.h"
Headers typically contain:
- Function prototypes
- Type definitions (struct, enum, typedef)
- Macro definitions
- extern variable declarations
#define — Constants and Macros
#define performs textual substitution before compilation. Unlike const variables, #define constants have no type and no scope — they are replaced everywhere in the file. This makes them suitable for values that must be usable as array sizes or in other compile-time contexts.
#include <stdio.h>
/* Object-like macros — simple text substitution before compilation */
#define PI 3.14159265358979
#define MAX_BUFFER 4096
#define APP_VERSION "2.1.0"
#define NEWLINE '\n'
/* Multi-line macro using backslash continuation */
#define HUGE_NUMBER \
1000000000LL
int main(void) {
double circumference = 2 * PI * 5.0;
printf("Circumference: %.4f\n", circumference);
char buf[MAX_BUFFER]; /* MAX_BUFFER is replaced with 4096 before compiling */
printf("Version: %s\n", APP_VERSION);
return 0;
}
Function-Like Macros
Function-like macros look like function calls but expand inline. They work on any type (unlike typed functions), but come with serious pitfalls. Always parenthesize every argument and the entire expression to prevent operator precedence surprises.
#include <stdio.h>
/* Always parenthesize arguments and the entire expression */
#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))
/* Why parentheses matter — precedence bugs without them */
#define BAD_SQUARE(x) x * x /* BAD_SQUARE(1+2) = 1+2*1+2 = 5 */
#define GOOD_SQUARE(x) ((x) * (x)) /* GOOD_SQUARE(1+2) = ((1+2)*(1+2)) = 9 */
/* Multi-statement macros: wrap in do { } while(0) so they behave like a statement */
#define SWAP(a, b, type) do { \
type _tmp = (a); \
(a) = (b); \
(b) = _tmp; \
} while (0)
int main(void) {
int x = 3, y = 7;
printf("SQUARE(5) = %d\n", SQUARE(5));
printf("MAX(3, 7) = %d\n", MAX(x, y));
printf("ABS(-42) = %d\n", ABS(-42));
SWAP(x, y, int);
printf("After SWAP: x=%d, y=%d\n", x, y);
/* DANGER: side effects in macro arguments cause double evaluation */
int a = 5;
int result = SQUARE(a++); /* expands to ((a++) * (a++)) — undefined behavior! */
(void)result;
return 0;
}
Macro pitfalls:
- No type checking —
MAX("a", 3)compiles but does something meaningless - Multiple evaluation of arguments —
MAX(a++, b++)increments twice - Hygiene issues with variable names — use names like
_tmpthat are unlikely to clash
The Stringizing # and Token-Pasting ## Operators
These two operators enable advanced macro techniques. # converts a macro argument into a string literal at compile time — useful for debugging output that shows both the variable name and its value. ## concatenates two tokens into one — useful for generating unique names.
#include <stdio.h>
/* # turns an argument into a string literal */
#define PRINT_INT(x) printf(#x " = %d\n", (x))
#define STRINGIFY(x) #x
/* ## pastes two tokens together to create a new identifier */
#define MAKE_VAR(prefix, num) prefix##num
#define DECLARE_COUNTER(n) static int counter_##n = 0
int main(void) {
int speed = 120;
PRINT_INT(speed); /* prints: speed = 120 */
PRINT_INT(2 + 3); /* prints: 2 + 3 = 5 */
printf("%s\n", STRINGIFY(Hello World)); /* Hello World */
int MAKE_VAR(val, 42) = 100; /* creates: int val42 = 100 */
printf("%d\n", val42);
DECLARE_COUNTER(requests);
counter_requests++;
printf("requests: %d\n", counter_requests);
return 0;
}
Conditional Compilation
Conditional compilation lets you include or exclude code based on defined macros. This is the mechanism behind debug-only logging, platform-specific code, and feature flags. The preprocessor evaluates the conditions and removes the excluded branches before the compiler even sees the code.
#include <stdio.h>
#define VERSION 2
/* #define DEBUG */ /* uncomment to enable debug output */
/* #define PLATFORM_WINDOWS */
/* ifdef: include code only when a macro is defined */
#ifdef DEBUG
#define LOG(fmt, ...) fprintf(stderr, "[DEBUG] " fmt "\n", ##__VA_ARGS__)
#else
#define LOG(fmt, ...) /* expands to nothing in release builds */
#endif
/* #if: include code based on a numeric comparison */
#if VERSION >= 2
#define HAS_NEW_FEATURE 1
#else
#define HAS_NEW_FEATURE 0
#endif
/* Platform detection — adapt to the compiler's predefined macros */
#if defined(_WIN32) || defined(_WIN64)
#define OS_WINDOWS
#define PATH_SEP '\\'
#elif defined(__APPLE__)
#define OS_MACOS
#define PATH_SEP '/'
#elif defined(__linux__)
#define OS_LINUX
#define PATH_SEP '/'
#else
#define OS_UNKNOWN
#define PATH_SEP '/'
#endif
int main(void) {
LOG("Starting up"); /* only prints if DEBUG is defined */
#if HAS_NEW_FEATURE
printf("New feature enabled\n");
#endif
#ifdef OS_LINUX
printf("Running on Linux\n");
#elif defined(OS_WINDOWS)
printf("Running on Windows\n");
#else
printf("Unknown platform\n");
#endif
return 0;
}
Checking Feature Support
/* Provide fallback definitions for older compilers lacking stdint.h */
#ifndef UINT32_MAX
typedef unsigned int uint32_t;
#define UINT32_MAX 0xFFFFFFFFU
#endif
/* Compile-time assertion — fails with a cryptic error if condition is false */
#define STATIC_ASSERT(cond, msg) \
typedef char static_assert_##msg[(cond) ? 1 : -1]
STATIC_ASSERT(sizeof(int) == 4, int_must_be_4_bytes);
Include Guards
Without include guards, a header included multiple times (directly or transitively) would produce duplicate type definitions and compile errors. Include guards make headers idempotent — safe to include any number of times.
/* mylib.h */
#ifndef MYLIB_H
#define MYLIB_H
/* All header content goes here — only processed once per translation unit */
typedef struct {
int x, y;
} Point;
Point point_add(Point a, Point b);
double point_distance(Point a, Point b);
#endif /* MYLIB_H */
The pattern: #ifndef HEADER_H / #define HEADER_H / content / #endif. The guard name should be unique — typically the filename in uppercase with dots replaced by underscores.
#pragma once is a non-standard but universally supported alternative that is simpler:
#pragma once
typedef struct { int x, y; } Point;
Point point_add(Point a, Point b);
Use #pragma once unless you need strict C standard portability. GCC, Clang, and MSVC all support it.
#pragma and Predefined Macros
#pragma directives give compiler-specific instructions. The predefined macros like __FILE__, __LINE__, and __func__ are invaluable for adding context to debug output and error messages.
#include <stdio.h>
/* Suppress specific warnings temporarily */
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-variable"
int unused = 42;
#pragma GCC diagnostic pop
/* Pack a struct with no padding — use for network protocols and file formats */
#pragma pack(push, 1)
struct PackedHeader {
uint8_t type;
uint32_t length;
uint16_t checksum;
}; /* exactly 7 bytes — no padding inserted */
#pragma pack(pop)
int main(void) {
/* Predefined macros provide context about where code is executing */
printf("File: %s\n", __FILE__);
printf("Line: %d\n", __LINE__);
printf("Function: %s\n", __func__); /* C99 */
printf("Date: %s\n", __DATE__);
printf("Time: %s\n", __TIME__);
/* Useful for quick debug traces */
#define HERE printf("Reached %s:%d\n", __FILE__, __LINE__)
HERE;
return 0;
}