Tutorials Logic, IN info@tutorialslogic.com

C Preprocessor #define, #include, #ifdef

Translation Directives

The C preprocessor handles directives before the compiler parses the resulting translation unit. It includes headers, expands macros, and selects conditional source text. Because macro expansion is token substitution rather than a typed function call, small mistakes can duplicate side effects, change precedence, or remove code unexpectedly.

After this lesson, you can write a guarded header, choose a typed constant or inline function instead of a macro when appropriate, create a safe function-like macro, and use conditional compilation without producing untestable platform branches.

Preprocessing Stage

The C preprocessor runs before compilation. It processes lines starting with # (called directives) and performs text substitution, file inclusion, and conditional compilation. The preprocessor output is pure C code that the compiler then compiles.

Common Preprocessor Directives

Directive Description
#include Include a header file
#define Define a macro (constant or function-like)
#undef Undefine a macro
#ifdef Compile if macro is defined
#ifndef Compile if macro is NOT defined
#if / #elif / #else / #endif Conditional compilation
#pragma Compiler-specific instructions
#error Emit a compile-time error message

Predefined Macros

Macro Description Example Value
__FILE__ Current source file name "main.c"
__LINE__ Current line number 42
__DATE__ Compilation date "Jan 15 2025"
__TIME__ Compilation time "10:30:00"
__STDC__ 1 if standard C compiler 1

#define Macros - Constants and Function-like

#define Macros - Constants and Function-like
#include <stdio.h>

// Object-like macros (constants)
#define PI        3.14159265
#define MAX_SIZE  100
#define NEWLINE   '\n'

// Function-like macros (no type checking - use with care)
#define SQUARE(x)    ((x) * (x))
#define MAX(a, b)    ((a) > (b) ? (a) : (b))
#define MIN(a, b)    ((a) < (b) ? (a) : (b))
#define ABS(x)       ((x) < 0 ? -(x) : (x))

// Multi-line macro using backslash
#define PRINT_INFO(name, val) \
    printf("%-15s = %d\n", name, val)

int main() {
    printf("PI = %.5f\n", PI);
    printf("MAX_SIZE = %d\n", MAX_SIZE);

    int x = 5;
    printf("SQUARE(%d) = %d\n", x, SQUARE(x));   // 25
    printf("MAX(3, 7)  = %d\n", MAX(3, 7));       // 7
    printf("ABS(-10)   = %d\n", ABS(-10));        // 10

    // Use side-effect-free arguments when a macro evaluates them more than once.
    printf("SQUARE(x + 1) = %d\n", SQUARE(x + 1));
    // Never call SQUARE(x++): repeated unsequenced modification is undefined behavior.

    PRINT_INFO("MAX_SIZE", MAX_SIZE);

    // #undef - remove a macro definition
    #undef MAX_SIZE
    // printf("%d", MAX_SIZE);  // ERROR: MAX_SIZE not defined

    return 0;
}

Conditional Compilation with #ifdef / #ifndef

Conditional Compilation with #ifdef / #ifndef
#include <stdio.h>

#define DEBUG    // comment this out to disable debug output
#define VERSION  2

int main() {
    int x = 42;

    // #ifdef - compile only if DEBUG is defined
    #ifdef DEBUG
        printf("[DEBUG] x = %d\n", x);
    #endif

    // #ifndef - compile only if macro is NOT defined
    #ifndef RELEASE
        printf("Running in development mode\n");
    #endif

    // #if / #elif / #else - numeric conditions
    #if VERSION == 1
        printf("Version 1 features\n");
    #elif VERSION == 2
        printf("Version 2 features\n");
    #else
        printf("Unknown version\n");
    #endif

    // Header guard pattern (prevents double inclusion)
    // Typically in .h files:
    // #ifndef MY_HEADER_H
    // #define MY_HEADER_H
    // ... header content ...
    // #endif

    return 0;
}

/*
[DEBUG] x = 42
Running in development mode
Version 2 features
*/

Predefined Macros and #pragma

Predefined Macros and #pragma
// #pragma once - modern alternative to header guards
// (supported by most compilers: GCC, Clang, MSVC)
// #pragma once

#include <stdio.h>

// Useful debug macro using predefined macros
#define LOG(msg) printf("[%s:%d] %s\n", __FILE__, __LINE__, msg)

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

    // Using LOG macro
    LOG("Program started");
    int x = 10;
    LOG("About to compute");
    printf("x = %d\n", x);
    LOG("Done");

    // #pragma message - print message at compile time
    // #pragma message("Compiling main.c...")

    // #pragma pack - control struct alignment
    // #pragma pack(1)  // pack structs with 1-byte alignment

    return 0;
}

/*
File:    predefined.c
Line:    12
Date:    Jan 15 2025
Time:    10:30:00
[predefined.c:16] Program started
x = 10
[predefined.c:19] Done
*/

Header Inclusion

#include replaces the directive with the selected header’s contents as part of preprocessing. Angle brackets request the implementation’s include search for system headers; quotes begin with the project-oriented search defined by the implementation. A header should declare an interface and include the declarations required to understand that interface.

Use an include guard with a project-unique macro so repeated inclusion does not redefine declarations. #pragma once is widely supported but is not the portable language mechanism. Avoid defining ordinary external-storage objects in headers because every translation unit that includes the header may create a definition.

Macro Safety

Parenthesize every use of a function-like macro parameter and the complete replacement expression. Even then, an argument can be evaluated more than once. SQUARE(i++) does not behave like a function and can produce undefined behavior, so macros that evaluate parameters should be called only with side-effect-free expressions.

Prefer enum constants, const objects, or static inline functions when type checking, single evaluation, or debugger visibility matters. Macros remain useful for conditional compilation, stringification, token construction, and carefully constrained generic operations.

Build Variants

#if, #ifdef, and related directives select source before type checking. Keep configuration macros centralized and test every supported branch in continuous integration; code hidden behind a platform macro can decay unnoticed on the developer’s machine.

Use #error for an unsupported or contradictory build configuration. Do not use conditional compilation to scatter product behavior throughout ordinary functions when runtime configuration would be clearer and testable in one build.

Before you move on

Directive Review

4 checks
  • Guard every public project header.
  • Parenthesize macro parameters and the full expansion.
  • Pass only side-effect-free arguments to repeated-parameter macros.
  • Compile and test every supported conditional branch.

Macro Hazards

  • Calling SQUARE(i++).

    Use a static inline function or evaluate the increment separately.
  • Defining writable global storage in a header.

    Declare it extern in the header and define it once in a source file.
  • Using an unguarded project header.

    Add a unique #ifndef, #define, and matching #endif guard.

Try this next

Build a Safe Header

0 of 2 completed

Next Step
Next Practice

Finish the concept here, then reinforce it with hands-on coding, interview prep, or a tool that matches the topic.

Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.