Skip to content

C Preprocessor — Macros, Includes, and Conditional Compilation

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about C Preprocessor. We cover key concepts, practical examples, and best practices to help you master this topic.

The C preprocessor processes source code before compilation, handling file inclusion via #include, macro expansion via #define, conditional compilation via #ifdef/#ifndef/#endif, and utility directives like #pragma and #error to control the compilation Process.

What You Will Learn

  • Defining and using macros with #define
  • Including header files with #include
  • Conditional compilation with #if, #ifdef, #ifndef, #else, #elif, #endif
  • The # and ## operators for stringification and token pasting
  • Using predefined macros (__FILE__, __LINE__, __DATE__, __TIME__)
  • Variadic macros with __VA_ARGS__
  • #pragma and #error directives

Why It Matters

The preprocessor is a powerful Code Generation tool that runs before the compiler. It enables platform-specific code, debugging instrumentation, assertion macros, include guards, and code that adapts to different compilers and operating systems. Durga Antivirus Pro uses preprocessor conditionals extensively to compile separate code paths for Windows (WinSock), Linux (epoll), and macOS (kqueue), all from a single source tree.

Real-World Use

A cross-platform library needs to allocate aligned memory: on Linux it uses posix_memalign, on Windows _aligned_malloc, on macOS valloc. Preprocessor conditionals select the right function for each platform without changing the caller's code.

Learning Path

flowchart LR
  A[Assert.h] --> B[Preprocessor\nYou are here]
  B --> C[Header Files]
  style B fill:#f90,color:#fff

Basic Macros

#include <stdio.h>

#define PI 3.1415926535
#define AREA(r) (PI * (r) * (r))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define SQUARE(x) ((x) * (x))

int main() {
    double radius = 5.0;
    printf("Area: %.4f\n", AREA(radius));
    printf("MAX(10, 20): %d\n", MAX(10, 20));
    printf("SQUARE(3 + 2): %d\n", SQUARE(3 + 2));  // ((3+2)*(3+2)) = 25

    return 0;
}

Extra parentheses in macros prevent operator precedence bugs. Without them, SQUARE(3 + 2) would expand to 3 + 2 * 3 + 2 = 11.

Stringification and Token Pasting

#include <stdio.h>

// # converts macro argument to a string literal
#define STRINGIFY(x) #x

// ## concatenates two tokens
#define CONCAT(a, b) a ## b

// Practical: make a unique variable name
#define UNIQ_VAR(prefix) prefix ## __LINE__

int main() {
    printf("Stringified: %s\n", STRINGIFY(hello world));
    printf("Stringified int: %s\n", STRINGIFY(42));

    int xy = 100;
    printf("CONCAT(x, y): %d\n", CONCAT(x, y));  // expands to xy

    int var_42 = 42;
    printf("UNIQ_VAR(var_): %d\n", UNIQ_VAR(var_));  // expands to var_ followed by __LINE__

    return 0;
}

Conditional Compilation

#include <stdio.h>

#define DEBUG 1
#define OS_LINUX

int main() {
    #ifdef DEBUG
        printf("Debug mode enabled\n");
    #endif

    #if defined(OS_LINUX)
        printf("Compiling for Linux\n");
    #elif defined(OS_MAC)
        printf("Compiling for macOS\n");
    #elif defined(OS_WINDOWS)
        printf("Compiling for Windows\n");
    #else
        printf("Unknown OS\n");
    #endif

    // Debug assertion using preprocessor
    #ifdef DEBUG
        int x = -1;
        if (x < 0) {
            printf("ASSERTION FAILED: x < 0 at %s:%d\n", __FILE__, __LINE__);
        }
    #endif

    return 0;
}

Output:

Debug mode enabled
Compiling for Linux
ASSERTION FAILED: x < 0 at <filename>:25

Include Guards

Prevent multiple inclusions of the same header:

#ifndef UTILITY_H
#define UTILITY_H

int clamp(int value, int min, int max);
double degrees_to_radians(double degrees);

#endif  // UTILITY_H

Variadic Macros

#include <stdio.h>

#define LOG_INFO(format, ...) \
    fprintf(stdout, "[INFO] " format "\n", ##__VA_ARGS__)

#define LOG_ERROR(format, ...) \
    fprintf(stderr, "[ERROR] " format "\n", ##__VA_ARGS__)

int main() {
    LOG_INFO("Server started on port %d", 8080);
    LOG_INFO("User %s logged in", "alice");
    LOG_ERROR("Connection failed: %s", "timeout");
    LOG_INFO("No extra args");  // Works due to ## before __VA_ARGS__

    return 0;
}

Predefined Macros

#include <stdio.h>

int main() {
    printf("File: %s\n", __FILE__);
    printf("Line: %d\n", __LINE__);
    printf("Date: %s\n", __DATE__);
    printf("Time: %s\n", __TIME__);
    printf("STDC: %d\n", __STDC__);

    #ifdef __STDC_VERSION__
        printf("C Standard: %ld\n", __STDC_VERSION__);
    #endif

    #ifdef __GNUC__
        printf("GCC Version: %d.%d\n", __GNUC__, __GNUC_MINOR__);
    #endif

    return 0;
}

Output (example):

File: predefined.c
Line: 7
Date: Jun 28 2026
Time: 14:00:00
STDC: 1
C Standard: 201710
GCC Version: 13.2

Pragma and Error Directives

#include <stdio.h>

// Suppress a specific warning (GCC/Clang)
#pragma GCC diagnostic ignored "-Wunused-variable"

// Message during compilation
#pragma message("Building with custom configuration")

int main() {
    int unused_var;  // Warning suppressed
    printf("Hello\n");

    // Compile-time assertion using #error
    #ifndef REQUIRED_FEATURE
        #error "REQUIRED_FEATURE must be defined to compile this file"
    #endif

    return 0;
}

Common Mistakes

  1. Not parenthesizing macro arguments: #define DOUBLE(x) x + x then DOUBLE(3) * 5 expands to 3 + 3 * 5 = 18, not 30. Always parenthesize each argument and the whole expression.

  2. Double evaluation in macros: #define MAX(a, b) ((a) > (b) ? (a) : (b)) evaluates a or b twice. If the argument has side effects like x++, those happen twice. Use inline functions instead for such cases.

  3. Missing include guards: Without guards, a header included twice causes duplicate definition errors. Always use #ifndef/#define/#endif or #pragma once.

  4. Trailing semicolons in macros: #define PRINT(msg) printf("%s\n", msg); then if (x) PRINT("ok") else ... breaks the else. Define the macro without the semicolon and add it at the call site.

  5. Forgetting that # and ## operators only work in macros: The # operator (stringify) and ## operator (token paste) are only valid in #define macro definitions, not in regular code.

Practice Questions

  1. What is the purpose of include guards in header files?
  2. Why must macro arguments be parenthesized?
  3. What does the ## operator do in a macro?
  4. How does conditional compilation help with cross-platform code?
  5. Challenge: Write a set of assertion macros that print the file name, line number, and a message when the assertion fails. Include a NDEBUG version that compiles to nothing, and a DEBUG version that calls abort() after printing. Use #ifdef to switch between them.

Mini Project

Build a logging system using variadic macros:

  • Define macros: LOG_TRACE, LOG_DEBUG, LOG_INFO, LOG_WARN, LOG_ERROR, LOG_FATAL
  • Each macro takes a format string and variable arguments
  • Each log level prints a prefix: [TRACE], [DEBUG], [INFO], [WARN], [ERROR], [FATAL]
  • Include FILE, LINE, and FUNCTION in each log message
  • Compile-time log level filtering: if LOG_LEVEL is set to INFO, TRACE and DEBUG messages are compiled out
  • Output to stderr with a timestamp using localtime_r
  • Test with all log levels

FAQ

{{< faq "What is the difference between #include and #include "file"?" "Angle brackets search system include directories. Quotes search the current directory first, then system include directories. Use quotes for project headers, angle brackets for standard library headers." >}}

Can I define a macro that expands to multiple lines?

Yes, use backslash-newline line continuation. Each line except the last must end with a backslash. This is commonly used in header guards and multi-line macros.

What does #pragma once do?

It tells the compiler to include the file only once per translation unit, even if #included multiple times. It is an alternative to traditional include guards, supported by most modern compilers.

Are macros type-safe?

No, macros perform text substitution with no type checking. Use const, enum, inline functions, or _Generic for type-safe alternatives. Since C11, _Generic provides type-dependent macro-like behavior with type safety.

What is the difference between #define and typedef?

#define is a text substitution macro. typedef creates a type alias at the language level. typedef respects scope and can create array, function pointer, and structure types correctly.

What is Next

Proceed to Header Files to learn about declaring functions and types across multiple source files. Then explore Makefiles for build automation.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro

Home Browse C