The C preprocessor runs before the compiler sees your code. It performs text substitution, conditional compilation, and file inclusion. Understanding it prevents a class of bugs that are invisible to the compiler but break your program at runtime.

How the Preprocessor Works

The build pipeline is: source → preprocessor → compiler → linker → executable. The preprocessor output is pure C with all macros expanded and includes inlined — you can inspect it:

gcc -E program.c -o program.i   /* saves preprocessor output */

#include — Insert File Contents

#include <stdio.h>     /* system header — searched in standard paths */
#include "myheader.h"  /* project header — searched relative to current file first */

#include literally copies the content of the named file into your source at that point. This is why header files use include guards — to prevent the same content being pasted twice when multiple files include the same header.

#define — Macros

Constants

#define PI        3.14159265358979
#define MAX_SIZE  1024
#define APP_NAME  "Online C Compiler"

int arr[MAX_SIZE];   /* preprocessed to: int arr[1024]; */

The preprocessor replaces every occurrence of PI with 3.14159265358979 before compilation. There is no type, no scope, no debugger symbol. Prefer const variables for typed constants:

const int MAX_SIZE = 1024;   /* has type, scope, and debugger visibility */

Function-like macros

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

The extra parentheses are essential. Without them:

#define BAD_SQUARE(x)  x * x
BAD_SQUARE(3 + 1)   /* becomes: 3 + 1 * 3 + 1 = 7, not 16 */

#define GOOD_SQUARE(x) ((x) * (x))
GOOD_SQUARE(3 + 1)  /* becomes: ((3+1) * (3+1)) = 16 */

Always parenthesize both the entire macro and every parameter.

The double-evaluation problem

#define MAX(a, b)  ((a) > (b) ? (a) : (b))

int x = 5;
int result = MAX(x++, 3);
/* expands to: ((x++) > (3) ? (x++) : (3)) */
/* x is incremented TWICE if x > 3 */

Function-like macros evaluate their arguments multiple times. Use inline functions instead when arguments might have side effects:

static inline int max(int a, int b) { return a > b ? a : b; }

#ifdef, #ifndef, #if — Conditional Compilation

#include <stdio.h>

#define DEBUG 1

int main() {
    int result = 42;

#ifdef DEBUG
    printf("Debug: result = %dn", result);
#endif

    printf("Result: %dn", result);
    return 0;
}

Remove #define DEBUG (or compile with -UDEBUG) and the debug printf disappears completely — zero runtime cost. Enable it with -DDEBUG on the compiler command line:

gcc -DDEBUG -o program program.c   /* defines DEBUG without editing source */

Use this pattern rather than commenting out debug code — it is cleaner and compilers can still type-check the disabled code.

Include Guards — Prevent Double Inclusion

/* myheader.h */
#ifndef MYHEADER_H
#define MYHEADER_H

typedef struct {
    int x;
    int y;
} Point;

void move(Point *p, int dx, int dy);

#endif /* MYHEADER_H */

The first time myheader.h is included, MYHEADER_H is not defined, so everything between #ifndef and #endif is processed and MYHEADER_H is defined. The second time it is included, MYHEADER_H is already defined, so the entire content is skipped.

Modern compilers support #pragma once as a simpler alternative:

#pragma once   /* same effect as include guards — non-standard but universally supported */

#pragma — Compiler Hints

#pragma once                    /* prevent double inclusion */
#pragma GCC optimize("O3")     /* request O3 for this file */
#pragma pack(1)                 /* pack structs with no padding */

Pragmas are implementation-defined. Unknown pragmas are ignored. Use them sparingly — they reduce portability. Prefer compiler flags over #pragma GCC extensions where possible.

Predefined Macros

#include <stdio.h>

int main() {
    printf("File:     %sn", __FILE__);
    printf("Line:     %dn", __LINE__);
    printf("Function: %sn", __func__);
    printf("Date:     %sn", __DATE__);
    return 0;
}

These are useful for debug logging and assertions. __LINE__ and __FILE__ are expanded by the preprocessor at each use, so they correctly report the file and line where they appear.

TL;DR

  • #include <> for system headers, #include "" for your own
  • Every header file needs include guards or #pragma once
  • Always parenthesize macro arguments and the whole macro expression
  • Avoid function-like macros for anything with side effects — use inline functions
  • Use -DFLAG on the command line to define macros without editing source
  • Use const variables instead of #define for typed constants
  • Test any macro or directive behaviour in our c online compiler — paste and run in seconds