Notifications

No notifications

/Phase 4

The Preprocessor (#include, #define, #ifdef)

The Compiler Before the Compiler ⚙️

Before C ever compiles your code, a tool called the preprocessor runs over it and edits the source — pasting in headers, expanding macros, stripping comments, conditionally hiding code. Every line that starts with # is a preprocessor directive.

#include <stdio.h>          /* paste contents of stdio.h here */
#define PI 3.14159f         /* every PI later -> 3.14159f      */
#define SQUARE(x) ((x)*(x)) /* function-like macro              */

int main(void) { printf("%f\n", PI); printf("%d\n", SQUARE(5)); /* expands to ((5)*(5)) */ return 0; }

The Big Three Directives

DirectivePurpose
#includePaste another file in here
#defineReplace a name with text
#ifdef / #ifndef / #endifCompile this block only sometimes

#include "..." vs #include <...>

#include <stdio.h>     /* angle brackets — search SYSTEM directories */
#include "myutils.h"   /* quotes — search MY project first, then system */

Header Guards (always do this)

#ifndef MYUTILS_H
#define MYUTILS_H
/* declarations */
#endif

Stops the same header being pasted twice into the same .c file.

On this page

Detailed Theory

The preprocessor is a text-substitution engine. It runs first, transforms your source, and hands the result to the actual compiler. Every # directive is a message to the preprocessor — not to the compiler.

You can see exactly what it produces with gcc -E file.c (try it!).

#include — Paste a File

#include <stdio.h>     /* system header — found via -I include path */
#include "config.h"    /* user header  — relative to current file first */

The preprocessor literally drops the contents of that file at this exact line. That's why headers contain only declarations (int add(int, int);), not definitions — definitions would be repeated and cause "multiple definition" errors at link time.

Header Guards — Prevent Double Inclusion

If utils.h is included by both a.h and b.h, and your .c includes both, you'd paste utils.h twice — duplicate type definitions = error. Guards fix it:

/* utils.h */
#ifndef UTILS_H
#define UTILS_H

void greet(const char *name); typedef struct { int x, y; } Point;

#endif /* UTILS_H */

Modern compilers also accept #pragma once (one line, less error-prone — but not in the C standard):

#pragma once

#define — Object-Like Macros

#define PI         3.14159f
#define MAX_USERS  1024
#define APP_NAME   "CampusCrate"

Every occurrence of the name (outside a string) is replaced before compilation. Conventionally written in UPPER_SNAKE_CASE.

> Modern C prefers const or enum for constants — they have a type and respect scope: > >

> static const int  MAX_USERS = 1024;
> enum { BUFFER_SIZE = 256 };
>

#define — Function-Like Macros

#define SQUARE(x)  ((x) * (x))
#define MAX(a, b)  ((a) > (b) ? (a) : (b))

Wrap arguments AND the whole expansion in parentheses. Otherwise:

#define SQUARE(x) x * x
SQUARE(2 + 3)        /* -> 2 + 3 * 2 + 3 = 11, not 25! */

The Side-Effect Trap

#define MAX(a, b) ((a) > (b) ? (a) : (b))
int x = MAX(i++, 5);   /* i++ may execute TWICE — undefined */

A real function evaluates each argument once. A macro pastes the text — so i++ appears twice in the expansion. For anything non-trivial, prefer an inline function.

#if / #ifdef / #ifndef / #else / #endif — Conditional Compilation

Compile (or skip) blocks of code based on macros:

#define DEBUG 1

#if DEBUG printf("x = %d\n", x); #endif

#ifdef _WIN32 /* Windows-specific code */ #elif defined(__linux__) /* Linux-specific code */ #else /* fallback */ #endif

#ifndef NDEBUG assert(p != NULL); #endif

Used heavily for cross-platform code, debug logging, and feature flags.

Built-In Predefined Macros

The compiler gives you a few for free:

MacroValue
__FILE__Current source file name
__LINE__Current line number
__DATE__Compilation date
__TIME__Compilation time
__func__Current function name (C99)
__STDC_VERSION__C standard in use

Great for logging:

#define LOG(msg) fprintf(stderr, "[%s:%d %s] %s\n", \
                         __FILE__, __LINE__, __func__, msg)

Multi-Line Macros

End each line (except the last) with \:

#define SWAP(a, b) do {           \
    int _tmp = (a);               \
    (a) = (b);                    \
    (b) = _tmp;                   \
} while (0)

The do { ... } while (0) trick lets the macro be used like a single statement, semicolon and all.

#undef — Cancel a Macro

#define MAX 100
/* ... use MAX ... */
#undef MAX             /* MAX is no longer defined */

#pragma & #error

#pragma once             /* alternative to header guards */
#pragma pack(1)          /* tighten struct packing */

#if !defined(__STDC__) #error "C99 or later required" #endif

#error aborts compilation with your message — useful to enforce assumptions.

Stringify (#) and Token-Paste (##)

Inside a macro, #x becomes the string literal of x, and a##b glues two tokens together:

#define STR(x)        #x
#define MAKE_VAR(n)   var_##n

printf("%s\n", STR(hello)); /* prints "hello" */ int MAKE_VAR(1) = 42; /* declares var_1 = 42 */

Powerful but easy to abuse.

Preprocessor Cheat-Sheet

Want toUse
Pull in a library#include
Pull in your file#include "myfile.h"
Define a constant#define NAME value
Define a macro#define F(x) ((x) + 1)
Conditional code#ifdef / #ifndef / #endif
Header guard#ifndef X_H ... #define X_H ... #endif
Cancel a macro#undef NAME
Force compile error#error "message"

The preprocessor is old, weird, and incredibly useful — and explains a lot of the surprising behaviour you'll see in real C codebases.