Skip to content
The Preprocessor
step 1/7

Reading — step 1 of 7

Learn

~3 min readFile I/O and the Preprocessor

Before the C compiler proper sees your code, the preprocessor runs. It handles #include, #define, conditional compilation, and macro expansion. Anything starting with # is a preprocessor directive, NOT C code.

#include — bringing in headers

#include <stdio.h>      /* angle brackets — system header, search system paths */
#include "my_header.h"  /* double quotes — local header, search current dir first */

At this stage the preprocessor literally PASTES the contents of the included file in place. Headers contain function prototypes, type declarations, and macros so the compiler knows what printf looks like before you call it.

#define — constants and simple macros

#define MAX_SIZE 100
#define PI 3.14159

int buffer[MAX_SIZE];           // becomes int buffer[100];
double area = PI * r * r;       // becomes 3.14159 * r * r

Text replacement, NOT a real constant. The preprocessor walks through and substitutes every occurrence.

Macros with arguments:

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

int y = SQUARE(5);              // becomes ((5) * (5)) = 25
int m = MAX(x, y);              // becomes ((x) > (y) ? (x) : (y))

Always wrap arguments AND the whole macro body in parens. Without them:

#define BAD_SQUARE(x) x * x

int y = BAD_SQUARE(2 + 3);     // becomes 2 + 3 * 2 + 3 = 11, not 25!

Constants — prefer const or enum

For real constants in modern C, prefer:

const int max_size = 100;       // typed, scoped, debugger-friendly
enum { MAX_SIZE = 100 };         // for integer constants

Over #define. Macros bypass type checking and can produce confusing error messages.

Conditional compilation — #ifdef, #ifndef, #if

Selective compilation based on macros:

#ifdef DEBUG
    printf("debug: x = %d\n", x);
#endif

#ifndef NDEBUG
    assert(condition);
#endif

#if defined(__linux__)
    /* Linux-specific code */
#elif defined(_WIN32)
    /* Windows-specific code */
#else
    #error Unsupported platform
#endif

Used for:

  • Platform-specific code paths
  • Debug vs release builds (-DDEBUG on the gcc command line defines DEBUG)
  • Optional features
  • Header guards (next)

Header guards — #ifndef + #define + #endif

Every header file should be wrapped in a guard so it gets included exactly once per translation unit:

#ifndef MY_MODULE_H
#define MY_MODULE_H

/* ... declarations ... */

#endif

Without this, including the header twice (e.g., transitively via different paths) causes "redefinition" errors.

Most compilers also support #pragma once as a non-standard alternative. Both work; #ifndef guards are the portable choice.

Predefined macros

The compiler provides several:

  • __FILE__ — current source file path
  • __LINE__ — current line number
  • __func__ (C99) — current function name
  • __DATE__, __TIME__ — when the file was compiled
  • __STDC_VERSION__ — C standard version
fprintf(stderr, "error at %s:%d in %s\n", __FILE__, __LINE__, __func__);

Common mistakes

  • Macros without parens around arguments — operator-precedence bugs that look like mysterious wrong answers.
  • Side effects inside macro argsMAX(i++, j) evaluates i++ TWICE.
  • Forgetting header guards — multiple-definition errors when a header is transitively included.
  • Confusing macro arguments with C scope — macros are pure text replacement; they don't see C variables, types, or scopes.
  • Using #define for things that should be const — loses type safety, debug visibility.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…