C in 100 Seconds: Preprocessor Directives
Video: C in 100 Seconds: Preprocessor Directives — define, ifdef, ifndef | Episode 40 by Taught by Celeste AI - AI Coding Coach
C Preprocessor Directives: define, ifdef, ifndef
#define X 5for constants.#ifdef DEBUGfor conditional compilation.#ifndef GUARDfor include guards. The text-substitution layer that runs before the C compiler.
The preprocessor is a separate program (or stage) that runs before compilation. It manipulates source text — substituting macros, including files, conditionally compiling. It doesn't understand C; it's a glorified search-and-replace.
The basic shape
#include <stdio.h>
#define PI 3.14159
#define MAX_SIZE 100
#define SQUARE(x) ((x) * (x))
int main() {
printf("PI: %f\n", PI);
printf("MAX_SIZE: %d\n", MAX_SIZE);
printf("SQUARE(5): %d\n", SQUARE(5));
#ifdef DEBUG
printf("Debug mode is ON\n");
#else
printf("Debug mode is OFF\n");
#endif
#ifndef VERSION
#define VERSION "1.0.0"
#endif
printf("Version: %s\n", VERSION);
#if MAX_SIZE > 50
printf("Large buffer mode\n");
#else
printf("Small buffer mode\n");
#endif
return 0;
}
#define — substitution
#define PI 3.14159
After preprocessing, every occurrence of PI in the source is replaced with 3.14159. Pure text substitution.
double area = PI * r * r;
// becomes:
double area = 3.14159 * r * r;
The compiler never sees PI — only 3.14159.
Macros with arguments
#define SQUARE(x) ((x) * (x))
int s = SQUARE(5);
// becomes:
int s = ((5) * (5));
Function-like macros take arguments. The arguments are substituted as text, not evaluated.
The parens matter:
#define BAD_SQUARE(x) x * x
int b = BAD_SQUARE(2 + 3); // 2 + 3 * 2 + 3 = 11, not 25!
BAD_SQUARE(2 + 3) becomes 2 + 3 * 2 + 3. Operator precedence makes that 11. With parens ((2 + 3) * (2 + 3)), you get 25.
Rule: wrap each argument in parens, and wrap the whole result in parens.
Macros vs functions
| Aspect | Macro | Function |
|---|---|---|
| Type checking | None | Yes |
| Multiple types | Works for any | Need overloading or generic |
| Side effects | Re-evaluates args | Single evaluation |
| Debugging | Hard (substituted text) | Easy (real call) |
| Inlining | Always | Depends on compiler |
SQUARE(i++) doubles the increment because i++ appears twice. Functions evaluate args once.
For type-safe alternatives:
static inline int square(int x) { return x * x; }
Inline functions get the speed of macros with the safety of functions. Modern C: prefer inline functions for the math; reserve macros for things functions can't do (cross-type, code-gen, conditionals).
#ifdef and #ifndef
#ifdef DEBUG
printf("Debug: x = %d\n", x);
#endif
#ifdef NAME is true if NAME is defined; false otherwise. The block between #ifdef and #endif is included or excluded from the compiled output.
#ifndef NAME is the opposite — true if NOT defined.
Activated via:
gcc -DDEBUG main.c # define DEBUG
-DNAME defines a macro from the command line. -DNAME=value defines with a value.
#if — numeric conditions
#if MAX_SIZE > 50
// ... included if MAX_SIZE > 50 ...
#elif MAX_SIZE > 10
// ...
#else
// ...
#endif
#if evaluates an arithmetic expression involving #define'd constants. More flexible than #ifdef for value-based decisions.
defined(NAME) lets you combine:
#if defined(DEBUG) && !defined(NDEBUG)
// debug output
#endif
NDEBUG (no-debug) is the convention used by assert.h — defining it disables assert.
Include guards
// myfile.h
#ifndef MYFILE_H
#define MYFILE_H
// declarations
#endif
The pattern from episode 16. Without it, multiple #include "myfile.h" cause re-declarations.
Modern alternative: #pragma once (compiler-specific, but widely supported).
#define for "feature flags"
#define MAX_USERS 100
#define ENABLE_LOGGING
#ifdef ENABLE_LOGGING
void log_event(const char *msg) {
fprintf(stderr, "[LOG] %s\n", msg);
}
#else
#define log_event(msg) ((void)0) // no-op
#endif
log_event("started");
When ENABLE_LOGGING is defined, real logging. Otherwise, the macro expands to a no-op. The compiler optimizes away the no-op.
Heavy in embedded code where every byte matters.
Stringification (#)
#define LOG(x) printf("%s = %d\n", #x, x)
int n = 42;
LOG(n); // prints: "n = 42"
#x in a macro turns the argument into a string literal. LOG(n) becomes printf("%s = %d\n", "n", n);.
Useful for debug-print macros that show the variable name.
Token pasting (##)
#define MAKE_VAR(name) int var_##name = 0
MAKE_VAR(foo); // int var_foo = 0;
MAKE_VAR(bar); // int var_bar = 0;
## joins two tokens into one identifier. Used for code generation patterns.
Common preprocessor macros (built-in)
__FILE__ // current source file path (string)
__LINE__ // current line number (int)
__DATE__ // compilation date
__TIME__ // compilation time
__func__ // current function name (C99+)
Useful for debug logging:
#define DEBUG_LOG(msg) \
fprintf(stderr, "%s:%d: %s\n", __FILE__, __LINE__, msg)
Common mistakes
Missing parens in macros. #define DOUBLE(x) x * 2 then DOUBLE(1+1) is 1+1*2 = 3, not 4.
Side effects in macro args. SQUARE(i++) increments twice. MIN(a++, b) may compare wrong.
Missing \ for multi-line macros. Macros are one-line by default. Continue with \.
#define shadows parameters. #define PI 3.14; void foo(int PI) — PI is replaced inside foo too. Confusing.
Stale #ifdefs. Code blocks behind dead #defines rot — never compiled, never tested.
#include cycles. A.h includes B.h includes A.h. Guards prevent infinite expansion but the result depends on order.
What's next
Episode 41: multi-file projects. Multiple .c files compiled together, Makefile-driven builds.
Recap
#define NAME value for constants; #define NAME(arg) ... for function-like macros (always parenthesise!). #ifdef/#ifndef/#endif for conditional compilation. #if expression for value-based conditions. Include guards #ifndef X #define X ... #endif (or #pragma once). #x for stringification; ## for token pasting. Built-in macros: __FILE__, __LINE__, __func__. Modern C: prefer static inline functions over function-like macros where possible.
Next episode: multi-file projects.