Docs

Preprocessor

Introduction to the C Preprocessor

Table of Contents

  1. •What is the Preprocessor?
  2. •The Compilation Pipeline
  3. •Preprocessor Directives Overview
  4. •The #include Directive
  5. •Basic Macros with #define
  6. •Conditional Compilation
  7. •Predefined Macros
  8. •Preprocessor Operators
  9. •Viewing Preprocessor Output
  10. •Best Practices
  11. •Summary

What is the Preprocessor?

The C preprocessor is a text processing tool that runs before the actual compilation of C code. It transforms your source code by handling directives (lines starting with #) before the compiler sees it.

Key Characteristics

  1. •Text-based transformation: Works with text, not C syntax
  2. •Runs before compilation: Compiler never sees preprocessor directives
  3. •No type checking: Operates on raw text
  4. •Token substitution: Replaces macros with their definitions

Preprocessor vs Compiler

Source File (.c)
      │
      ā–¼
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│  Preprocessor   │ ← Handles #include, #define, #ifdef, etc.
│                 │   Text substitution phase
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
         │
         ā–¼
   Translation Unit (Preprocessed .i file)
         │
         ā–¼
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│    Compiler     │ ← Parses C syntax, generates assembly
│                 │   Type checking happens here
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
         │
         ā–¼
   Assembly Code (.s)
         │
         ā–¼
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│   Assembler     │ ← Converts to machine code
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
         │
         ā–¼
   Object File (.o)
         │
         ā–¼
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│     Linker      │ ← Combines objects, resolves symbols
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
         │
         ā–¼
   Executable

The Compilation Pipeline

Stage 1: Preprocessing

/* Before preprocessing: main.c */
#include <stdio.h>
#define MAX_SIZE 100
#define DOUBLE(x) ((x) * 2)

int main(void) {
    int arr[MAX_SIZE];
    int value = DOUBLE(5);
    printf("Value: %d\n", value);
    return 0;
}
/* After preprocessing (simplified): */
/* Contents of stdio.h expanded here... */

int main(void) {
    int arr[100];             /* MAX_SIZE replaced */
    int value = ((5) * 2);    /* DOUBLE(5) replaced */
    printf("Value: %d\n", value);
    return 0;
}

What the Preprocessor Does

ActionDirectiveExample
File inclusion#include#include <stdio.h>
Macro definition#define#define PI 3.14159
Macro undefinition#undef#undef DEBUG
Conditional compilation#if, #ifdef, #ifndef#ifdef DEBUG
Line control#line#line 100 "file.c"
Error generation#error#error "Invalid config"
Compiler hints#pragma#pragma once

Preprocessor Directives Overview

Syntax Rules

  1. •Start with #: Must be first non-whitespace on line
  2. •One per line: Each directive on its own line
  3. •No semicolon: Directives don't end with ;
  4. •Line continuation: Use \ to continue on next line
/* Directive can have leading whitespace */
    #include <stdio.h>    /* OK */

/* # can be separated from directive name */
#    define VALUE 10      /* OK but unusual */

/* Multi-line directive */
#define LONG_MACRO(a, b, c) \
    do { \
        printf("%d %d %d\n", a, b, c); \
    } while(0)

/* WRONG: No semicolon! */
#include <stdio.h>;       /* Error! */

All Preprocessor Directives

DirectivePurpose
#includeInclude header files
#defineDefine macros
#undefUndefine macros
#ifConditional based on expression
#ifdefConditional if macro defined
#ifndefConditional if macro not defined
#elifElse-if for conditionals
#elseElse for conditionals
#endifEnd conditional block
#errorGenerate compile-time error
#warningGenerate compile-time warning (non-standard)
#pragmaCompiler-specific commands
#lineControl line numbers and filename
# (null directive)Does nothing

The #include Directive

Two Forms

/* Angle brackets: Search system/standard paths first */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* Quotes: Search current directory first, then system paths */
#include "myheader.h"
#include "utils/helper.h"
#include "../common/types.h"

Include Search Paths

#include <header.h>
    1. System include directories (e.g., /usr/include)
    2. Compiler-specified directories (-I option)

#include "header.h"
    1. Directory of the including file
    2. Current working directory
    3. Same search as <header.h>

What Happens During Include

/* file: myheader.h */
#define MAX 100
void print_hello(void);

/* file: main.c */
#include "myheader.h"

int main(void) {
    int arr[MAX];
    print_hello();
    return 0;
}

/* After preprocessing main.c: */
#define MAX 100
void print_hello(void);

int main(void) {
    int arr[MAX];
    print_hello();
    return 0;
}

Include Guards

Prevent multiple inclusion of same header:

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

/* Header contents here */
typedef struct {
    int x, y;
} Point;

void process_point(Point *p);

#endif /* MYHEADER_H */

Modern alternative (non-standard but widely supported):

#pragma once

/* Header contents */

Basic Macros with #define

Object-like Macros (Constants)

/* Simple constant */
#define PI 3.14159265358979

/* Integer constant */
#define BUFFER_SIZE 1024

/* String constant */
#define APP_NAME "MyApplication"

/* Empty macro (just for conditional compilation) */
#define DEBUG

/* Multi-token replacement */
#define AUTHOR "John Doe", "2024"

/* Usage */
double area = PI * radius * radius;
char buffer[BUFFER_SIZE];
printf("Application: %s\n", APP_NAME);

Function-like Macros

/* Basic function macro */
#define SQUARE(x) ((x) * (x))

/* Multiple parameters */
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))

/* With side effects awareness needed! */
#define ABS(x) ((x) < 0 ? -(x) : (x))

/* Usage */
int sq = SQUARE(5);      /* Expands to: ((5) * (5)) */
int m = MAX(10, 20);     /* Expands to: ((10) > (20) ? (10) : (20)) */

/* DANGER: Side effects! */
int a = 5;
int sq = SQUARE(a++);    /* ((a++) * (a++)) - undefined behavior! */

Why Parentheses Matter

/* BAD: No parentheses */
#define DOUBLE(x) x * 2

int result = DOUBLE(3 + 4);  /* 3 + 4 * 2 = 11 (wrong!) */

/* GOOD: Proper parentheses */
#define DOUBLE(x) ((x) * 2)

int result = DOUBLE(3 + 4);  /* ((3 + 4) * 2) = 14 (correct!) */

Undefining Macros

#define VALUE 100

/* Use VALUE here */
int x = VALUE;

#undef VALUE

/* VALUE is no longer defined */
/* int y = VALUE; */  /* Error! */

/* Can redefine after undef */
#define VALUE 200
int z = VALUE;  /* z = 200 */

Conditional Compilation

Basic Conditionals

/* Check if macro is defined */
#ifdef DEBUG
    printf("Debug mode enabled\n");
#endif

/* Check if macro is NOT defined */
#ifndef NDEBUG
    assert(ptr != NULL);
#endif

/* Using #if with expressions */
#if MAX_SIZE > 100
    printf("Large buffer mode\n");
#elif MAX_SIZE > 50
    printf("Medium buffer mode\n");
#else
    printf("Small buffer mode\n");
#endif

The defined() Operator

/* Equivalent ways to check if defined */
#ifdef DEBUG
    /* code */
#endif

#if defined(DEBUG)
    /* code */
#endif

/* Can combine with logical operators */
#if defined(DEBUG) && defined(VERBOSE)
    printf("Verbose debug mode\n");
#endif

#if defined(WINDOWS) || defined(_WIN32)
    /* Windows-specific code */
#endif

#if !defined(HEADER_H)
#define HEADER_H
    /* header contents */
#endif

Platform-Specific Code

#if defined(_WIN32) || defined(_WIN64)
    #include <windows.h>
    #define PLATFORM "Windows"
#elif defined(__linux__)
    #include <unistd.h>
    #define PLATFORM "Linux"
#elif defined(__APPLE__)
    #include <TargetConditionals.h>
    #define PLATFORM "macOS"
#else
    #define PLATFORM "Unknown"
#endif

int main(void) {
    printf("Running on %s\n", PLATFORM);
    return 0;
}

Predefined Macros

Standard Predefined Macros

#include <stdio.h>

int main(void) {
    /* Current date (MMM DD YYYY) */
    printf("Compiled on: %s\n", __DATE__);

    /* Current time (HH:MM:SS) */
    printf("Compiled at: %s\n", __TIME__);

    /* Current source file name */
    printf("File: %s\n", __FILE__);

    /* Current line number */
    printf("Line: %d\n", __LINE__);

    /* Current function name (C99) */
    printf("Function: %s\n", __func__);

    /* STDC version (C95 and later) */
    #ifdef __STDC_VERSION__
    printf("C Standard: %ld\n", __STDC_VERSION__);
    #endif

    return 0;
}

Common STDC_VERSION Values

ValueStandard
Not definedC89/C90
199409LC95
199901LC99
201112LC11
201710LC17
202311LC23

Compiler-Specific Macros

/* GCC version */
#ifdef __GNUC__
    printf("GCC version: %d.%d.%d\n",
           __GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__);
#endif

/* Clang */
#ifdef __clang__
    printf("Clang version: %d.%d\n",
           __clang_major__, __clang_minor__);
#endif

/* MSVC */
#ifdef _MSC_VER
    printf("MSVC version: %d\n", _MSC_VER);
#endif

Preprocessor Operators

Stringification (#)

Converts macro parameter to string literal:

#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)

/* Direct stringification */
printf("%s\n", STRINGIFY(hello));  /* Prints: hello */

/* Stringify a macro value (need two levels) */
#define VERSION 2
printf("%s\n", TOSTRING(VERSION));  /* Prints: 2 */
printf("%s\n", STRINGIFY(VERSION)); /* Prints: VERSION */

Token Pasting (##)

Joins two tokens into one:

#define CONCAT(a, b) a##b

int CONCAT(my, Variable) = 10;   /* Creates: int myVariable = 10; */
int CONCAT(array, 1) = 20;       /* Creates: int array1 = 20; */

/* Useful for generating identifiers */
#define DECLARE_ARRAY(type, name, size) \
    type name##_array[size]; \
    int name##_count = 0

DECLARE_ARRAY(int, numbers, 100);
/* Creates:
   int numbers_array[100];
   int numbers_count = 0;
*/

Examples Using Both

#define DEBUG_VAR(var) printf(#var " = %d\n", var)

int count = 42;
DEBUG_VAR(count);  /* Prints: count = 42 */

#define ENUM_ENTRY(name) name,
#define ENUM_STRING(name) #name,

/* Auto-generate enum and string array */
#define COLORS(X) \
    X(RED) \
    X(GREEN) \
    X(BLUE)

enum Color { COLORS(ENUM_ENTRY) };
const char *color_names[] = { COLORS(ENUM_STRING) };

/* Expands to:
   enum Color { RED, GREEN, BLUE, };
   const char *color_names[] = { "RED", "GREEN", "BLUE", };
*/

Viewing Preprocessor Output

Using GCC

# Preprocess only, output to stdout
gcc -E source.c

# Preprocess to file
gcc -E source.c -o source.i

# Preprocess with line markers
gcc -E source.c

# Preprocess without line markers
gcc -E -P source.c

# Show include paths
gcc -v source.c

# Show which headers are included
gcc -H source.c

Using Clang

# Preprocess only
clang -E source.c

# Preprocess to file
clang -E source.c -o source.i

Example

/* file: example.c */
#include <stdio.h>
#define MAX 100
#define DOUBLE(x) ((x) * 2)

int main(void) {
    int arr[MAX];
    int val = DOUBLE(5);
    return 0;
}
$ gcc -E -P example.c

Output (simplified):

/* stdio.h contents here... */

int main(void) {
    int arr[100];
    int val = ((5) * 2);
    return 0;
}

Best Practices

1. Use UPPERCASE for Macro Names

/* GOOD */
#define MAX_BUFFER_SIZE 1024
#define PI 3.14159

/* BAD - Looks like a variable */
#define maxSize 1024

2. Parenthesize Macro Arguments

/* BAD */
#define SQUARE(x) x * x

/* GOOD */
#define SQUARE(x) ((x) * (x))

3. Use do-while(0) for Multi-Statement Macros

/* BAD - Breaks in if-else */
#define LOG(msg) printf("LOG: "); printf(msg);

/* GOOD */
#define LOG(msg) do { \
    printf("LOG: "); \
    printf(msg); \
} while(0)

/* Usage - works correctly */
if (error)
    LOG("An error occurred\n");
else
    LOG("All good\n");

4. Prefer inline Functions Over Macros (When Possible)

/* Macro - no type safety */
#define MAX(a, b) ((a) > (b) ? (a) : (b))

/* Inline function - type safe */
static inline int max_int(int a, int b) {
    return a > b ? a : b;
}

5. Always Use Include Guards

/* file: header.h */
#ifndef HEADER_H
#define HEADER_H

/* Contents */

#endif /* HEADER_H */

6. Avoid Side Effects in Macro Arguments

#define SQUARE(x) ((x) * (x))

/* DANGEROUS */
int a = 5;
int s = SQUARE(a++);  /* Undefined behavior! */

/* SAFE */
int s = SQUARE(a);
a++;

Summary

Key Concepts

  1. •Preprocessor runs before compilation
  2. •Operates on text, not C semantics
  3. •Directives start with #
  4. •#include copies file contents
  5. •#define creates text substitutions
  6. •Conditional directives enable/disable code sections

Common Directives Quick Reference

/* File inclusion */
#include <header.h>
#include "header.h"

/* Macro definition */
#define NAME value
#define FUNC(x) ((x) + 1)
#undef NAME

/* Conditionals */
#if expression
#ifdef MACRO
#ifndef MACRO
#elif expression
#else
#endif

/* Other */
#error "message"
#pragma directive
#line number "filename"

Preprocessor Operators

OperatorPurposeExample
#Stringification#define STR(x) #x
##Token pasting#define CONCAT(a,b) a##b
defined()Check if defined#if defined(DEBUG)

Next Steps

After understanding preprocessor basics:

  • •Learn about advanced macro techniques
  • •Study conditional compilation patterns
  • •Explore header file organization
  • •Practice with real-world projects

This tutorial is part of the C Programming Learning Series.

Preprocessor - C Programming Tutorial | DeepML