C Preprocessor Directives: A Complete Guide

Predefined Symbols

The C language provides several predefined symbols that are available for direct use during preprocessing:

__FILE__  // Source file being compiled
__DATE__  // Compilation date
__TIME__  // Compilation time
__LINE__  // Current line number in the source
__STDC__  // 1 if compiler conforms to ANSI C, undefined otherwise

Example usage:

#include <stdio.h>

int main()
{
    printf("File: %s\n", __FILE__);
    printf("Compiled on: %s\n", __DATE__);
    printf("Compiled at: %s\n", __TIME__);
    printf("Line number: %d\n", __LINE__);
    return 0;
}

#define Constants

The #define directive creates symbolic constants with this basic syntax:

#define NAME value

Practical examples:

#define BUFFER_SIZE 4096
#define MAX_CONNECTIONS 100
#define forever for(;;)
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define SWAP(a, b) do { \
    int temp = (a); \
    (a) = (b); \
    (b) = temp; \
} while(0)

Important: Avoid trailing semicolons in #define declarations. A semicolon can introduce syntax errors:

#define THRESHOLD 100;

int main()
{
    int value = THRESHOLD;  // Expands to: int value = 100;;
    return 0;
}

#define Macros

The #define directive can also define parameterized macros, which substitute code patterns with parameters:

#define MACRO_NAME(param1, param2) replacement_text

Critical rule: The opening parenthesis must immediately follow the macro name with no whitespace between them.

Basic macro example:

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

Calling SQUARE(5) produces ((5) * (5)), yielding 25.

Without parentheses, SQUARE(3+2) expands to 3+2*3+2, which equals 11 instead of the expected 25.

Another example with addition:

#define DOUBLE(x) ((x) + (x))

DOUBLE(5) correctly yields ((5) + (5)) = 10.

Macros with Side Effects

Arguments appearing multiple times in a macro definition can cause issues when they contain side effects (increment/decrement operations):

#include <stdio.h>

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

int main()
{
    int x = 5;
    int y = 8;
    int result = MAX(x++, y++);
    
    printf("x=%d, y=%d, result=%d\n", x, y, result);
    return 0;
}

This expands to:

int result = ((x++) > (y++) ? (x++) : (y++));

Both x and y get incremented, resulting in x=6, y=9, result=8. The behavior may difffer from expectations.

Macro Substitution Rules

  1. When a macro is invoked, parameters are first checked for any #define symbols, which get replaced first.
  2. The macro body substitutes parameter names with their argument values.
  3. The resulting code is rescanned for additional #define symbols, and the process repeats.

Restrictions:

  • Macros may reference other defined symbols within parameters or the body.
  • Self-referencing macros are not allowed.
  • String literal contents are not scanned during macro expansion.
#define N 100
const char* msg = "N";  // The N inside quotes stays as "N"

Macros vs Functions

Advantages of macros:

  • No function call overhead, resulting in faster execution for simple operations.
  • Type-agnostic: macros work with any data type, unlike functions constrained by parameter types.
  • Direct code substitution avoids stack operations.

Disadvantages of macros:

  • Expanded code increases binary size on every use.
  • Difficult to debug since the expanded code doesn't exist as a discrete unit.
  • Lack of type checking can introduce erors.
  • Operator precedence issues may arise without careful parenthesization.

Unique macro capability—passing types as arguments:

#define ALLOCATE(type, count) \
    (type*)malloc(sizeof(type) * (count))

// Usage:
int* numbers = ALLOCATE(int, 50);
char* buffer = ALLOCATE(char, 256);

This expands to:

int* numbers = (int*)malloc(sizeof(int) * 50);
char* buffer = (char*)malloc(sizeof(char) * 256);

Note: The backslash \ continues lines but should not appear on the final line.

The # and ## Operators

Stringification Operator (#)

The # operator converts a macro parameter into a string literal:

#define STRINGIZE(x) #x

const char* msg = STRINGIZE(Hello);  // "Hello"

Practical logging example:

#define LOG_INT(expr) \
    printf(#expr " = %d\n", expr)

int main()
{
    int value = 42;
    LOG_INT(value);  // Prints: value = 42
    return 0;
}

Token Pasting Operator (##)

The ## operator merges tokens to form new identifiers:

#define MAKE_GETTER(type, name) \
    type get_##name(void) { \
        return name; \
    }

int counter = 0;
MAKE_GETTER(int, counter)
// Generates: int get_counter(void) { return counter; }

Generic type-safe maximum function:

#define DEFINE_MAX(type) \
    type type##_max(type a, type b) \
    { \
        return (a > b) ? a : b; \
    }

DEFINE_MAX(int)
DEFINE_MAX(double)

int main()
{
    int imax = int_max(3, 7);
    double dmax = double_max(2.5, 3.7);
    printf("Max int: %d\n", imax);
    printf("Max double: %.2f\n", dmax);
    return 0;
}

#undef Directive

The #undef directive removes a previously defined macro:

#define BUFFER_SIZE 1024

size_t size = BUFFER_SIZE;  // Valid

#undef BUFFER_SIZE

// BUFFER_SIZE is no longer defined here

Conditional Compilation

Conditional compilation controls which code blocks get compiled:

// Basic conditional
#if DEBUG_MODE == 1
    printf("Debug logging enabled\n");
#endif

// Multi-branch conditional
#if LEVEL == 1
    #define MAX_CLIENTS 10
#elif LEVEL == 2
    #define MAX_CLIENTS 50
#else
    #define MAX_CLIENTS 100
#endif

// Definition checks
#if defined(ENABLE_FEATURE_X)
    include_feature_x();
#endif

#if !defined(DISABLE_LOGGING)
    log_message("Application started");
#endif

// Nested conditionals
#if defined(LINUX)
    #if defined(DEBUG)
        enable_linux_debug();
    #endif
#elif defined(WINDOWS)
    #if defined(DEBUG)
        enable_windows_debug();
    #endif
#endif

Header File Inclusion

Local vs Library Includes

#include "custom_utils.h"     // Search local directory first
#include <stdio.h>           // Search standard system directories

Angle brackets skip the current directory and search system paths directly.

Preventing Multiple Inclusion

Headers can be included multiple times through nested includes, causing redefinition errors. Two common solutions:

Include guards (portable):

#ifndef PROJECT_UTILS_H
#define PROJECT_UTILS_H

// Header contents here
struct Config {
    int timeout;
    char* name;
};

#endif

Pragma once (modern, non-portable):

#pragma once

// Header contents here

Additional Preprocessor Directives

  • #error — Generates a compilation error with a custom message
  • #pragma — Implementation-specific directives for compiler behavior
  • #line — Controls line numbers reported in diagnostics
#if __STDC_VERSION__ < 201112L
    #error "C11 or later required"
#endif

#pragma warning(disable: 4996)  // MSVC: suppress specific warnings
#pragma pack(1)                  // Pack structures tightly

Tags: c-programming preprocessor Macros conditional-compilation header-files

Posted on Fri, 03 Jul 2026 16:06:38 +0000 by ramma03