devnotes.

Macros and Preprocessor Directives

The preprocessor runs before real compilation, handling text substitution and conditional inclusion of code through directives starting with #.

#define PI 3.14159                   // object-like macro: pure text substitution
#define SQUARE(x) ((x) * (x))        // function-like macro

#ifdef DEBUG
    #define LOG(msg) printf("[DEBUG] %s\n", msg)
#else
    #define LOG(msg)                 // expands to nothing in a non-debug build
#endif

int main(void) {
    printf("%f\n", PI);
    printf("%d\n", SQUARE(5));       // expands to ((5) * (5)) -> 25
    LOG("Starting program");
    return 0;
}

Because macros are pure text substitution, parentheses around both the whole macro body and each parameter (((x) * (x)), not x * x) are essential — without them, SQUARE(2 + 3) would expand to 2 + 3 * 2 + 3 (wrong: 11) instead of ((2 + 3) * (2 + 3)) (correct: 25), since normal operator precedence would apply to the expanded text.

#ifdef/#ifndef/#else/#endif conditionally include or exclude code before compilation even sees it — commonly used for debug-only logging (as above), platform-specific code paths, or include guards that prevent a header file from being processed twice in the same compilation:

#ifndef MYHEADER_H
#define MYHEADER_H
// header content here
#endif

Many modern compilers also support #pragma once as a simpler, non-standard alternative to include guards. In modern C, prefer const/constexpr over #define for constants and inline/regular functions over function-like macros whenever possible — macros bypass the type system entirely and can silently evaluate an argument more than once, causing subtle bugs with expressions that have side effects.

Macros and Preprocessor Directives in C | DevNotes