Preprocessor & Macros

Before a single line of C is actually compiled, the preprocessor runs a purely textual pass over the source — expanding macros, including files, and switching code in or out — using directives that all start with #.

Every C source file passes through a stage that has nothing to do with the C language's own grammar, types, or execution model — the preprocessor. It runs first, reading the raw text of your file and mechanically rewriting it: pulling in the contents of other files, replacing macro names with their definitions, and deciding which chunks of source even survive to reach the compiler. Only after all of that is finished does the actual compiler see the result and start reasoning about types, expressions, and statements. This page walks through the whole preprocessor — directives, #include and header guards, object-like and function-like macros, the classic parenthesization and multiple-evaluation traps, conditional compilation, the # and ## operators, predefined macros, and where macros should give way to const, enum, or an inline function — building toward a single worked example that ties every piece together.

Where the Preprocessor Fits in the Build Pipeline

A C build is not one single step. Before the compiler proper ever analyzes types or generates code, a separate pass called the preprocessor rewrites the source text. It expands #include directives by pulling in the referenced file's contents, replaces macro names with their defined text, evaluates conditional directives to decide which lines survive, and strips out comments. What comes out the other end is called a preprocessed translation unit — ordinary C source, with none of the # directives left in it — and that is what the compiler actually compiles.

Everything above the middle line is text manipulation; nothing there knows what a type or a function call is. Only once the arrow reaches the compiler does C's actual language semantics come into play.

Before the compiler ever sees this function body, the preprocessor has already pulled in stdio.h's declarations and replaced every occurrence of PI with the literal text 3.14159.

C Source Code
     |
     v
Preprocessor
     |
     +-- Expand #include
     +-- Expand macros
     +-- Evaluate conditional directives
     +-- Remove comments
     |
     v
Preprocessed Translation Unit
     |
     v
Compiler
     |
     v
Assembly / Object Code
#include <stdio.h>
#define PI 3.14159

int main(void)
{
    printf("%f\n", PI);
}

What Is a Preprocessor Directive

A preprocessor directive is a line that begins with the # character. Lines like #define MAX 100, #include <stdio.h>, and #ifdef DEBUG are instructions to the preprocessor, not ordinary C statements — the compiler's normal statement grammar never sees them at all, because the preprocessor consumes and acts on them first.

Contrast an ordinary C statement such as if (x > 0) { printf("Positive"); } — this is real C code that the compiler parses and generates instructions for — with a directive such as #define MAX 100, which the preprocessor consumes during its own pass and which never becomes part of the compiled program's logic directly.

#define MAX 100
#include <stdio.h>
#ifdef DEBUG
/* these three lines are all preprocessor directives */

if (x > 0)
{
    printf("Positive");
}
/* this is ordinary C code, parsed by the compiler */

Including Header Files with #include

printf itself is defined in the C standard library, not in this file — its declaration becomes visible here only because #include <stdio.h> spliced stdio.h's contents in before the compiler looked at main.

#include <stdio.h>

int main(void)
{
    printf("Hello\n");
    return 0;
}

Angle Brackets vs Quotes

Angle brackets are conventionally used for system or implementation-provided headers, while quotes are conventionally used for project or local headers that live alongside your own source files.

The exact search rules for each form are implementation-defined — different compilers and build setups can search slightly different directories in a different order. Treat the bracket-vs-quote distinction as a convention about intent ("library header" vs "my own header"), not a guarantee of a specific search algorithm.

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

#include "config.h"
#include "device.h"

Writing and Sharing Header Files

A header file typically holds declarations, type definitions, and macros that are meant to be shared across more than one source file — it says what exists without necessarily providing the implementation. A .c file that needs those declarations includes the header instead of retyping them.

math_utils.h only declares add and subtract; their actual bodies would live in a separate math_utils.c compiled and linked in alongside this file. Sharing the header lets multiple .c files agree on the same interface.

/* math_utils.h */
int add(int a, int b);
int subtract(int a, int b);
#include "math_utils.h"
#include <stdio.h>

int main(void)
{
    int result = add(10, 20);
    printf("%d\n", result);
    return 0;
}

The Multiple-Inclusion Problem

Because #include is just textual splicing, the same header can end up pulled into a single translation unit more than once, through both a direct include and an indirect one reached through some other header.

main.c includes both A.h and B.h, and each of those independently includes common.h. Without protection, common.h's text is spliced into main.c's translation unit twice.

Left unprotected, a header containing type definitions or function definitions being processed twice in one translation unit typically produces a redefinition diagnostic — this is exactly the failure header guards exist to prevent.

main.c
  |
  +-- include A.h
  |       |
  |       +-- include common.h
  |
  +-- include B.h
          |
          +-- include common.h

Header Guards: #ifndef, #define, #endif

The traditional fix for the multiple-inclusion problem is a header guard: a conditional block, wrapped around the entire header, that checks whether a unique guard macro has already been defined.

The pattern reads as: "if MYHEADER_H has not been defined yet, define it now and process everything up to #endif." Once MYHEADER_H exists, a second inclusion sees it already defined and skips straight to #endif, so the body is never processed twice.

Interview framing: header guards exist to prevent the contents of a header from being processed more than once within a single translation unit, which would otherwise trigger redefinition errors for any type or function definitions the header contains.

#ifndef MYHEADER_H
#define MYHEADER_H

int add(int a, int b);

#endif
First inclusion:
MYHEADER_H not defined
        |
        v
define MYHEADER_H
        |
        v
process header contents

Second inclusion:
MYHEADER_H already defined
        |
        v
skip contents

#pragma once

Many compilers support an alternative to the #ifndef/#define/#endif pattern: a single line, #pragma once, placed at the top of a header.

This tells the implementation to process the header only once per translation unit, achieving the same effect as a manual header guard with one line instead of three.

#pragma once

int add(int a, int b);

What Is a Macro

A macro is a name introduced with #define. Once defined, every later occurrence of that name in the source is replaced by its associated replacement text during preprocessing — this is a purely textual substitution, carried out before the compiler ever looks at types or expressions.

MAX_SIZE never exists as a variable or a runtime value — by the time the compiler sees this line, MAX_SIZE has already been replaced by the text 100.

Interview framing: a macro is a preprocessing definition created with #define that participates in textual macro replacement — it is not a variable, not a function, and not itself a piece of the C language's runtime.

#define MAX_SIZE 100

int array[MAX_SIZE];

/* after preprocessing, conceptually: */
int array[100];

Object-Like Macros

A macro defined without parameters is called an object-like macro. It behaves like a named constant piece of text — wherever its name appears, the preprocessor drops in the replacement text.

PI is replaced by 3.14159265359 wherever it appears; the function body the compiler actually sees already has the literal number spliced in.

#define PI 3.14159265359
#define MAX_SIZE 100
#define VERSION 3

float area(float radius)
{
    return PI * radius * radius;
}

Function-Like Macros

A macro can also accept arguments, in which case it is called a function-like macro. It looks similar to a function call syntactically, but the mechanism behind it is entirely different — textual substitution of the arguments into the macro's replacement text, not a function call.

The parentheses right after SQUARE, with no space, are what make this a function-like macro rather than an object-like one whose replacement text happens to contain parentheses.

Even though SQUARE(5) looks exactly like a function call, no function call ever happens — the entire expression is rewritten as plain text before the compiler starts reasoning about the program at all. This distinction is the source of nearly every macro pitfall on this page.

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

int result = SQUARE(5);

/* preprocessing conceptually produces: */
int result = ((5) * (5));

Why Parentheses Matter in Macro Parameters

Because macro expansion is textual substitution, a macro parameter that isn't wrapped in its own parentheses can interact badly with whatever operators surround it at the call site — normal operator precedence rules apply to the expanded text, not to the caller's intent.

The caller wrote 2 + 3 intending it as a single value to square, but with x unparenthesized, multiplication's higher precedence grabs only the 3 next to it, not the whole 2 + 3.

Wrapping x in its own parentheses on every use, and wrapping the whole expression in parentheses too, forces the caller's argument to be evaluated as one unit no matter what operators surround the macro call.

Pitfall — missing parentheses generalizes beyond SQUARE: #define DOUBLE(x) x + x looks harmless until it meets int result = 10 * DOUBLE(5);, which expands to 10 * 5 + 5 (15... actually 50 + 5 = 55, not the intended 10 * 10 = 100). The fix is the same one: #define DOUBLE(x) ((x) + (x)).

Interview framing: macro parameters are parenthesized to prevent the caller's own operators from interacting unexpectedly with the operators inside the macro's replacement text — parentheses make the substituted argument behave as a single, self-contained unit regardless of what surrounds the macro call.

#define SQUARE(x) x * x

int result = SQUARE(2 + 3);

/* expands to: */
int result = 2 + 3 * 2 + 3;
/* which, by precedence, is 2 + (3 * 2) + 3 = 11, not 25 */
#define SQUARE(x) ((x) * (x))

int result = SQUARE(2 + 3);

/* expands to: */
int result = ((2 + 3) * (2 + 3));
/* = 25, as intended */

The Multiple-Evaluation Danger

Even a correctly parenthesized macro can still cause trouble, because a function-like macro's parameter can appear more than once in its replacement text. If the argument passed in has a side effect, that side effect happens once for every occurrence of the parameter — not once total.

SQUARE(x) mentions x twice in its replacement text, so i++ is textually duplicated too — the increment doesn't happen once, it happens twice, and there is no defined order between them.

Rule of thumb: never pass an expression with a side effect — i++, --i, or a function call with observable effects — to a macro that might evaluate its parameter more than once. If you can't be sure how many times a macro's parameter appears in its body, treat every argument as if it will be evaluated multiple times.

Interview framing: the problem with SQUARE(i++) is that a typical SQUARE macro evaluates its argument more than once, so i is incremented multiple times with no defined sequencing between those increments — the behavior is undefined, and the actual output can vary between compilers.

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

int i = 5;
int result = SQUARE(i++);

/* expands approximately to: */
int result = ((i++) * (i++));
/* i is modified more than once with no sequencing between the modifications -- undefined behavior */

Macro vs Function

A function-like macro and an actual function can look deceptively similar at the call site, but the mechanism behind each is completely different, and the difference has real consequences.

square(int x) has a declared parameter type — the compiler checks and, if needed, converts whatever is passed to it. SQUARE(x)'s x has no inherent C type at all; it is whatever text was written at the call site, dropped verbatim into the expansion. For ordinary calculations, a real function is usually the safer and clearer choice; a macro is worth reaching for only when you specifically need preprocessing behavior a function can't give you.

Interview framing: is a macro a function? No. A function is a C-language construct with normal type checking and function-call semantics; a function-like macro is handled entirely by the preprocessor, before the compiler's semantic analysis even begins.

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

int square(int x)
{
    return x * x;
}
Macro:
Preprocessor expansion
      |
      v
text/code substitution

Function:
Compiler sees function call
      |
      v
normal C type checking
      |
      v
function execution

Macros Do Not Have a Type

value in the second line is a genuine C object with the type int. VALUE in the first line is not an object at all — it never had a type, an address, or a lifetime to begin with.

This distinction reaches further than it first looks: it affects type checking (a macro's replacement text is not type-checked at the point it's written), debugging (a debugger generally can't show you "the value of VALUE" the way it shows a variable), scope, addressability (you cannot take &VALUE the way you can take &value), evaluation, and lifetime.

#define VALUE 10

const int value = 10;

Macro Replacement Is Not Variable Assignment

It helps to be explicit about what #define MAX 100 does not do: it does not create a variable, it does not reserve a memory object, and it does not produce a runtime value. What it does create is a macro name that the preprocessor will replace with 100 wherever it later appears.

#define MAX 100

/*
 MAX -> not a variable
 MAX -> not a memory object
 MAX -> not a runtime value
 MAX -> a macro name, replaced with 100 at preprocessing time
*/

#undef

A macro can be removed with #undef. After #undef, the name is no longer recognized as a macro, so later occurrences of it in the source are left untouched rather than being replaced.

a is set using the macro's value while MAX is still defined. After #undef MAX, any later MAX in the file would just be an ordinary (and here, undeclared) identifier, not a macro replacement.

#define MAX 100
int a = MAX;

#undef MAX
/* MAX is no longer a macro from this point on */

Redefining Macros

A macro name can be given a new #define, but redefining it with a different, incompatible replacement list without first undefining it is something to avoid — it can produce a compiler diagnostic, and even where it's tolerated it makes code harder to reason about.

If a macro genuinely needs a new value later in the same translation unit, undefine it explicitly with #undef before redefining it. Relying on an implementation to silently accept a conflicting redefinition is not something to depend on.

#define VALUE 10
/* ... later, without an #undef first: */
#define VALUE 20   /* risky -- avoid casually redefining like this */
#undef VALUE
#define VALUE 20   /* safe -- the old definition is explicitly removed first */

Conditional Compilation

The preprocessor can include or exclude entire sections of source text based on conditions evaluated at compile time, using #if, #ifdef, #ifndef, #elif, #else, and #endif. Whatever branch is not selected simply never reaches the compiler — it is removed from the translation unit before compilation begins.

If DEBUG is defined at this point in the file, the printf line is kept in the preprocessed output. If DEBUG is not defined, the line is discarded entirely, as if it had never been written.

#ifdef DEBUG
printf("Debug information\n");
#endif

#ifdef

Because DEBUG has been defined (even with no replacement value at all), #ifdef DEBUG is true and the printf line is included.

#define DEBUG

#ifdef DEBUG
printf("Debug mode\n");
#endif

#ifndef

If BUFFER_SIZE has not already been defined by this point (for example, by a compiler flag or an earlier header), it is given a default value of 256 here.

#ifndef BUFFER_SIZE
#define BUFFER_SIZE 256
#endif

#if and Preprocessing Constant Expressions

VERSION >= 3 is evaluated by the preprocessor using VERSION's macro-replacement value, entirely before compilation. There is no runtime comparison happening here at all.

Interview framing: #ifdef DEBUG only checks whether DEBUG is defined at all, regardless of its value. #if VERSION >= 2 evaluates an actual constant expression, so it needs VERSION to expand to something usable in arithmetic — use #ifdef/#ifndef for pure presence checks and #if for value-based conditions.

#define VERSION 3

#if VERSION >= 3
printf("Version 3 or newer\n");
#endif

#if, #elif, #else Chains

Like a runtime if/else if/else chain, #if/#elif/#else lets the preprocessor pick exactly one branch among several, based on constant-expression conditions evaluated in order.

Only the branch whose condition is actually true survives into the preprocessed output; the others are discarded as if they were never written.

#define VERSION 2

#if VERSION == 1
printf("Version 1\n");
#elif VERSION == 2
printf("Version 2\n");
#else
printf("Other version\n");
#endif
VERSION == 2
     |
     v
+----------------+
| #if VERSION==1 |---- skipped
+----------------+
| #elif ==2      |---- selected
+----------------+
| #else          |---- skipped
+----------------+

The defined Operator

Inside a #if expression, the defined operator tests whether a macro has been defined, returning 1 or 0 for use in the surrounding constant expression. This lets #if subsume what #ifdef does, and lets it combine multiple presence checks in one condition.

defined lets you combine presence checks with && and || directly inside a single #if, something #ifdef alone cannot express.

#if defined(DEBUG)
printf("Debug enabled\n");
#endif

/* equivalent to: */
#ifdef DEBUG
printf("Debug enabled\n");
#endif
#if defined(DEBUG) && defined(TEST_MODE)
printf("Debug test mode\n");
#endif

#if 0 for Temporarily Excluding Code

A common technique for temporarily disabling a block of code is to wrap it in #if 0 ... #endif. Because 0 is always false, the preprocessor never even attempts to tokenize the section's contents as active code — it is dropped outright.

#if 0
printf("This code is excluded\n");
#endif

Compile-Time Configuration

Conditional compilation is the mechanism that lets a build be configured before compilation even starts — a build system can define macros externally (for example, through a compiler flag like -DFEATURE_A), and the preprocessor then selects which code actually gets compiled based on those definitions.

#define FEATURE_A

#ifdef FEATURE_A
void featureA(void)
{
    /* implementation */
}
#endif
Build configuration
       |
       v
Preprocessor macro definitions
       |
       v
Conditional compilation
       |
       v
Selected program variant

Platform-Specific Code

One of the most common real-world uses of conditional compilation is supporting multiple target platforms from a single source file, by branching on macros that identify the platform.

The exact predefined macros that identify a platform or compiler depend entirely on the specific compiler and toolchain in use — never assume a macro like WINDOWS or LINUX exists without checking your implementation's documentation, and prefer the macros a toolchain actually documents over ones you've merely seen elsewhere.

#ifdef WINDOWS
    /* Windows-specific code */
#elif defined(LINUX)
    /* Linux-specific code */
#else
    /* Other platform */
#endif

Debug Builds and the LOG Macro

A very common pattern combines a function-like macro with conditional compilation to make logging calls disappear entirely in a release build, rather than merely doing nothing at runtime.

In a debug build (DEBUG defined), LOG("Starting application") expands to an actual printf call. In a release build, LOG expands to nothing at all -- the call site is simply erased, at zero runtime cost.

Because the release-build LOG expands to nothing, any argument passed to it is never evaluated at all in that build. If a caller ever writes LOG(compute_expensive_message()), that call happens in debug builds but silently vanishes in release builds -- a mismatch worth avoiding by keeping macro arguments free of side effects your code actually depends on.

#ifdef DEBUG
#define LOG(msg) printf("DEBUG: %s\n", msg)
#else
#define LOG(msg)
#endif

int main(void)
{
    LOG("Starting application");
    return 0;
}

The Stringification Operator #

Inside a function-like macro's replacement text, the # operator converts the macro argument that follows it into a string literal, using the argument's own source text.

STRINGIFY(Hello) expands to "Hello" -- the identifier Hello is turned into the literal string "Hello", not looked up as a value.

SHOW_NAME(count) expands to printf("count" " = %d\n", count); -- the adjacent string literals "count" and " = %d\n" are concatenated by the C language itself, producing the single string "count = %d\n", and the program prints "count = 10".

Interview framing: what does # do inside a macro? It stringifies its argument -- it turns the literal source text of the argument into a quoted string literal, rather than evaluating it as an expression.

#define STRINGIFY(x) #x

#include <stdio.h>

int main(void)
{
    printf("%s\n", STRINGIFY(Hello));
    return 0;
}
#define SHOW_NAME(x) printf(#x " = %d\n", x)

int main(void)
{
    int count = 10;
    SHOW_NAME(count);
    return 0;
}

The Token-Pasting Operator ##

The ## operator glues two adjacent preprocessing tokens together into a single new token. It is most often used to build an identifier out of a fixed prefix and a macro argument.

CONCAT(value, 1) pastes the tokens value and 1 together into the single identifier value1 before the compiler ever sees the line -- value1 must already be a valid identifier once pasted.

Interview framing: ## is the token-pasting operator -- it combines two preprocessing tokens into one, which lets macros synthesize identifiers (or other tokens) that don't exist anywhere in the source as written.

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

int CONCAT(value, 1) = 100;

/* produces approximately: */
int value1 = 100;

A Practical ## Example: Generating Names

Token pasting becomes useful in practice when you want a family of related, predictably-named identifiers generated from a single macro, rather than typed out by hand for every variant.

CREATE_REGISTER(status, 10) pastes status onto register_, producing the declaration int register_status = 10; -- a distinct, real identifier the compiler treats exactly as if you'd typed it directly.

Token pasting is powerful, but it comes at a real readability cost -- a name like register_status doesn't appear anywhere in the source as written, which makes it invisible to a plain text search and harder for a reader (or an IDE's "find references") to trace back to its macro. Reach for ## deliberately, and keep this kind of macro metaprogramming to a minimum.

#define CREATE_REGISTER(name, value) \
    int register_##name = value

int main(void)
{
    CREATE_REGISTER(status, 10);
    /* produces approximately: int register_status = 10; */
    return 0;
}

Multi-Line Macros with Backslash Continuation

A macro's replacement text is normally confined to one line, but a trailing backslash at the very end of a line tells the preprocessor that the macro definition continues onto the next line, letting a macro span multiple lines of source.

Each backslash must be the very last character on its line -- not even trailing whitespace after it -- for the continuation to work correctly.

A macro made of multiple statements like this has a hidden problem: it isn't safely usable everywhere a single statement is. Written as if (condition) PRINT_VALUES(10, 20); else ..., only the first printf ends up attached to the if, and the second one runs unconditionally -- the do { } while (0) pattern in the next section exists specifically to fix this.

#define PRINT_VALUES(a, b) \
    printf("%d\n", (a));   \
    printf("%d\n", (b));

int main(void)
{
    PRINT_VALUES(10, 20);
    return 0;
}

The do { } while (0) Safety Pattern

When a macro's replacement text needs more than one statement, wrapping the whole body in do { ... } while (0) makes the macro behave, syntactically, exactly like a single statement -- it can be followed by a semicolon and safely dropped into an if/else without changing the program's structure.

The do { ... } while (0) loop always executes its body exactly once -- it isn't really a loop in practice, it's a container that lets several statements be followed by one semicolon and still attach correctly to the preceding if.

Compare this to a naive multi-statement macro like #define LOG(x) printf("%d\n", (x)); with a trailing semicolon baked in -- that trailing semicolon creates an empty statement after an if's single macro call, and can silently detach an else. Wrapping in do { } while (0) and requiring the caller to supply the semicolon avoids both problems at once.

#define SWAP(a, b)       \
    do {                 \
        int temp = (a);  \
        (a) = (b);       \
        (b) = temp;      \
    } while (0)

int main(void)
{
    int x = 3, y = 9;
    int condition = 1;

    if (condition)
        SWAP(x, y);
    else
        printf("No swap\n");

    return 0;
}

Macro Scope

Macros do not follow C's ordinary variable scoping rules at all. A macro, once defined, is available everywhere in the file from that point forward -- across function boundaries, across blocks -- until it is explicitly undefined with #undef or the translation unit's preprocessing ends.

VALUE is visible inside both function1 and function2 even though neither function declared it -- macros are not limited by function or block scope the way an ordinary variable declared inside a function would be.

This is yet another way #define VALUE 100 and int value = 100; are fundamentally different: the int variable would obey ordinary block or file scope rules, while the macro's "scope" is really just "from its #define to wherever it gets #undef'd or the file ends."

#define VALUE 100

void function1(void)
{
    printf("%d\n", VALUE);
}

void function2(void)
{
    printf("%d\n", VALUE);
}

Predefined Macros

C implementations automatically provide a handful of predefined macros without any #define needed. The most commonly used ones are __FILE__, __LINE__, __DATE__, and __TIME__.

__FILE__ expands to the current source filename, __LINE__ to the current line number, and __DATE__/__TIME__ to the date and time of translation -- these are especially useful inside error-reporting or logging macros.

The exact set of predefined macros beyond what the C standard itself requires depends on the implementation -- many compilers add their own (for identifying themselves, the target architecture, and so on), so consult your toolchain's documentation rather than assuming a macro exists.

#include <stdio.h>

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

__func__

C also provides __func__, which, inside a function body, gives the name of the enclosing function as a string.

Calling test() prints "Function: test" -- __func__ automatically reflects whichever function it appears inside.

__func__ is technically not a macro at all, unlike __FILE__ and __LINE__ -- it is a predefined identifier with special semantics defined directly by the C standard, implicitly declared as a local static array of char at the start of each function. In practice it is used exactly like the predefined macros above.

#include <stdio.h>

void test(void)
{
    printf("Function: %s\n", __func__);
}

int main(void)
{
    test();
    return 0;
}

Macro Expansion Through Nested Macros

A macro's replacement text can itself contain other macro names, in which case the preprocessor keeps expanding until no more macro names remain to be replaced (subject to rules that prevent infinite recursion on self-referential names).

AREA is replaced by its own replacement text: (WIDTH * HEIGHT)

WIDTH is replaced by 10, and HEIGHT is replaced by 20 The final result is: int area = (10 * 20);

#define WIDTH 10
#define HEIGHT 20
#define AREA (WIDTH * HEIGHT)

int area = AREA;

Why Macro Expansion Isn't Simple Find-and-Replace

It's a useful first approximation to describe macro expansion as "find the macro name and replace it with its text" -- for a simple case like #define VALUE 10 turning VALUE + VALUE into 10 + 10, that mental model works fine.

But real macro expansion follows specific tokenization and rescanning rules that a plain text find-and-replace does not capture: it operates on preprocessing tokens rather than raw characters, it must correctly match parentheses and commas to separate a function-like macro's arguments, it interacts specially with # and ## (which suppress the normal expansion of the operand they touch), it can trigger further expansion of macros that appear inside an already-expanded macro's output (rescanning), and it has explicit rules to stop a self-referential macro from expanding forever.

"Find and replace" is a fine intuition for a beginner meeting macros for the first time, but it is technically incomplete -- once you're debugging a macro that isn't expanding the way you expect, especially one involving # or ##, you need the more precise tokenization-and-rescanning model to understand what actually happened.

#define VALUE 10

VALUE + VALUE

/* becomes approximately: */
10 + 10

More Macro Pitfalls to Watch For

Beyond the parenthesization and multiple-evaluation traps already covered, a handful of other macro pitfalls are common enough to call out on their own.

Macros can hide complexity. A short-looking macro invocation can expand into a large number of tokens once every use, every nested macro, and every stringify/paste operation is unfolded -- this can make compiler error messages confusing and debugging harder than the equivalent function call would be, since a debugger generally steps over the macro's expanded text as if it were a single line.

Macro names can collide. Because macro names are replaced everywhere they appear in a translation unit with no notion of namespace, a short, generic macro name like MAX or MIN can silently conflict with an identically-named macro from another header or library. Prefer longer, more specific names for project-wide macros, such as SAMPLEAPP_MAX_SIZE instead of just MAX_SIZE, when the name is meant to be broadly visible.

Macros don't provide normal type checking. A macro such as #define MAX(a, b) ((a) > (b) ? (a) : (b)) will happily accept ints, floats, pointers, or anything comparable with > -- its behavior then depends entirely on how the surrounding expression's types and conversions play out, with none of the type-checking a real function's declared parameter types would give you.

#define SAMPLEAPP_MAX_SIZE 100

Macro vs const

For a simple named constant, #define and const represent two very different mechanisms, and the choice between them matters more than it might first appear.

MAX_SIZE is pure preprocessing replacement text: no C object is created merely by defining it, it has no C type, and it doesn't follow ordinary variable scope. max_size, by contrast, is an actual C object: it has a type (int), a scope, a storage duration, and it is subject to all of C's normal language rules, including type checking.

For a typed constant value used in ordinary calculations, a const object is usually preferable precisely because it participates fully in the type system. Macros remain genuinely useful, though, for jobs a const object cannot do: conditional compilation, header guards, compile-time configuration, token generation, stringification, and compiler- or platform-specific feature selection.

#define MAX_SIZE 100

const int max_size = 100;

Macro vs enum

For a small set of related integer constants, an enum is often a cleaner alternative to a run of individual #define lines.

The enum version gives MAX_RETRIES a real place in the C language as an enumeration constant, rather than as raw preprocessor replacement text.

The preprocessor is still needed when a value must participate directly in a preprocessing condition, such as #if MAX_RETRIES > 2 -- whether an enum constant can be used there depends on how and where it's defined, so a macro is the more dependable choice specifically for values that conditional compilation itself needs to see.

#define MAX_RETRIES 3

/* versus: */
enum
{
    MAX_RETRIES = 3
};

Macro vs Inline Function

For an operation like squaring a number, a static inline function is often preferable to a function-like macro, because it gives you real function semantics while still offering the compiler an opportunity to avoid call overhead.

The inline function gives you real type checking on its argument, normal C function-call semantics, easier debugging (a debugger can step into it like any other function), arguments evaluated exactly once according to ordinary function-call rules, and therefore no risk of the accidental multiple-evaluation problem SQUARE(i++) runs into.

The inline keyword is only a suggestion to the compiler -- whether the call is actually inlined (avoiding function-call overhead entirely) is ultimately the compiler's own decision, based on optimization settings and the function's complexity.

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

/* versus: */
static inline int square(int x)
{
    return x * x;
}

#error

If DEVICE_TYPE has not been defined by this point (for example, through a build-system flag), preprocessing stops immediately with the message "DEVICE_TYPE must be defined" instead of letting the rest of the file compile against a missing configuration value.

#ifndef DEVICE_TYPE
#error "DEVICE_TYPE must be defined"
#endif

#warning

Some compilers support #warning, which emits a diagnostic message without stopping preprocessing -- useful for flagging something worth attention (an experimental feature, a deprecated code path) without failing the build outright.

Unlike #error, #warning is not standardized in the same universal way across C implementations -- treat it as a widely-supported but not guaranteed extension, and consider portability before relying on it in code meant to build everywhere.

#warning "This feature is experimental"

#line

From this point forward, the compiler behaves as though the current line is line 100 of a file named "generated.c", for the purposes of any diagnostics it reports.

#line 100 "generated.c"

A Complete Worked Example

Putting the pieces together, here is a single small program that uses an object-like macro, a debug-conditional LOG macro built on the do { } while (0) pattern, and a value-based #if check, all in one place.

The preprocessor decides what MAX_VALUE expands to (100) wherever it appears.

The preprocessor decides whether DEBUG is defined, which selects which LOG(x) definition is active for the rest of the file.

The preprocessor evaluates #if MAX_VALUE >= 100 and keeps only the matching branch's printf line.

Only after all of that is the resulting plain C source -- with no # directives left in it -- handed to the compiler.

Everything that decided which lines of code exist happened before compilation started; the compiler that finally runs only ever sees the one selected variant of this program.

#include <stdio.h>

#define MAX_VALUE 100

#ifdef DEBUG
#define LOG(x) \
    do { \
        printf("DEBUG: %s\n", (x)); \
    } while (0)
#else
#define LOG(x) \
    do { \
    } while (0)
#endif

int main(void)
{
    int value = MAX_VALUE;

    LOG("Program started");

#if MAX_VALUE >= 100
    printf("Maximum value is at least 100\n");
#else
    printf("Maximum value is less than 100\n");
#endif

    printf("Value = %d\n", value);

    return 0;
}

Preprocessor vs Compiler

It's worth drawing a firm line between what belongs to the preprocessor and what belongs to the compiler, since confusing the two is a common source of misunderstanding.

The key point: the preprocessor does not perform normal C type checking at all. It is entirely possible for it to produce output that is syntactically nonsensical or type-incorrect C -- it will do so without complaint, because checking types simply isn't its job.

The preprocessor's substitution step succeeds without any complaint -- it is purely textual. The type mismatch between int and a string literal is a C-language problem, and only the compiler's semantic analysis catches it.

Interview framing: does the preprocessor understand C types? Not in the normal compiler sense -- all macro processing happens before normal C semantic and type analysis, so a macro can produce text that is completely wrong from a type perspective, and the preprocessor itself will never notice.

Preprocessor
------------------------
#include
#define
#undef
#if / #ifdef / #ifndef
#elif / #else / #endif
#  (stringify)
## (token paste)

Compiler
------------------------
C syntax
Types
Expressions
Functions
Control flow
Optimization
Code generation
#define VALUE "hello"

int x = VALUE;

/* the preprocessor happily produces: */
int x = "hello";
/* only the COMPILER catches that this assigns a string literal to an int */

The Mental Model: Source Transformation Before Compilation

It helps to hold one single mental model of the whole preprocessor in mind: it is a compile-time source transformation stage, sitting entirely in front of the compiler, whose only job is to turn one piece of C-shaped text into another piece of C-shaped text.

This is exactly why preprocessing always happens before normal C compilation -- the compiler is never asked to reason about a #define or a #ifdef at all, because none of them exist anymore by the time it starts. All it ever sees is the one, fully-resolved piece of ordinary C source that survived the transformation.

PREPROCESSOR
                      |
        +-------------+--------------+
        |             |              |
     #include       #define      #if/#ifdef
        |             |              |
        v             v              v
     Include       Expand        Select/
     headers       macros        remove code
        |             |              |
        +-------------+--------------+
                      v
             Preprocessed source
                      |
                      v
                   Compiler

Best Practices

A short set of habits keeps macros from becoming a source of bugs rather than a convenience.

Prefer ordinary C constructs -- a const object or a static inline function -- whenever they offer clearer, type-checked semantics that a macro cannot provide.

Reach for a macro specifically when preprocessing itself is what you actually need: header guards, conditional compilation for feature selection or platform differences, and genuine compile-time configuration are all good, legitimate uses of #define, #ifdef/#endif, and #ifndef/#define/#endif.

Always parenthesize both the macro parameters and the overall replacement expression.

Be cautious with any macro whose parameter might appear more than once in its body -- SQUARE(x) is the classic example -- and never pass an argument with a side effect such as i++ to one. When a macro becomes difficult to read or reason about, that is usually a sign it should be rewritten as an ordinary function or a static inline function instead.

const int max_size = 100;

static inline int square(int x)
{
    return x * x;
}
#define ADD(a, b) ((a) + (b))   /* prefer this */
#define ADD(a, b) a + b        /* over this */

Quick Reference

A compact summary of the directives and operators covered on this page, for quick lookup.

What is the fundamental difference between what the preprocessor does to source code and what the compiler does?

The preprocessor performs a purely textual transformation -- expanding includes and macros and selecting conditional branches -- entirely before compilation, with no awareness of C types or syntax correctness. The compiler operates on the resulting plain C source and performs all normal type checking, parsing, and code generation.

Why does #define SQUARE(x) x * x behave incorrectly for SQUARE(2 + 3), and how does adding parentheses fix it?

Without parentheses, the caller's + and the macro's * combine according to ordinary operator precedence on the raw substituted text, so SQUARE(2 + 3) expands to 2 + 3 * 2 + 3 rather than treating 2 + 3 as one value. Wrapping every occurrence of the parameter, and the whole expression, in parentheses -- ((x) * (x)) -- forces the substituted argument to be evaluated as a single unit no matter what surrounds the macro call.

Why can passing an expression like i++ to a macro that evaluates its parameter more than once lead to undefined behavior?

If a macro's parameter appears more than once in its replacement text, the argument's text -- including any side effect it contains -- is duplicated along with it. An expression like i++ then modifies i more than once with no defined sequencing between those modifications, which is undefined behavior in C.

What problem do header guards (#ifndef/#define/#endif) solve, and why is #pragma once not a full ISO C substitute for them?

Header guards prevent the same header's contents from being processed more than once within a single translation unit when it's reached through multiple include paths, which would otherwise cause redefinition errors. #pragma once achieves the same practical effect and is widely supported, but it is a compiler extension rather than part of the ISO C standard, so the traditional guard pattern remains the more portable choice.

What's the practical difference between defining a constant with #define MAX_SIZE 100 versus const int max_size = 100, and when would you prefer one over the other?

The macro is pure preprocessing replacement text with no C type, no scope, and no storage -- nothing is actually created by defining it. The const version is a genuine typed C object with real scope and storage duration, fully subject to the language's type-checking rules. Prefer const for an ordinary typed constant used in calculations, and reserve macros for jobs that need actual preprocessing, such as conditional compilation or header guards.

Write a C program that defines an object-like macro for the value of PI and uses it to compute and print the area of a circle for a given radius.

PI is an object-like macro, so every occurrence of PI is replaced with the literal text 3.14159265359 before compilation. The area calculation then compiles as an ordinary expression using that literal value, with no macro-related overhead or type of its own.

Write a C program that defines a properly parenthesized SQUARE function-like macro and demonstrates that SQUARE(2 + 3) correctly evaluates to 25.

Because both the parameter x and the whole expression are wrapped in parentheses, SQUARE(2 + 3) expands to ((2 + 3) * (2 + 3)), forcing 2 + 3 to be evaluated as a single unit before the multiplication happens, giving the correct result of 25 instead of the wrong answer an unparenthesized macro would produce.

Write a C program that shows the multiple-evaluation danger of macros by comparing the result of calling a SQUARE macro with i++ against calling an equivalent square() function with i++.

The function call square(j++) evaluates j++ exactly once, following normal C function-call rules, so j predictably increases by one and func_result is well-defined. Calling SQUARE(i++) instead would expand to ((i++) * (i++)), evaluating the increment twice with no defined order between them, which is undefined behavior -- so the program deliberately avoids ever making that call and explains why in a comment.

Write a C program that uses #ifdef/#else conditional compilation to give a LOG macro different behavior depending on whether DEBUG is defined.

Because DEBUG is defined at the top of the file, the preprocessor selects the first LOG(msg) definition, which expands into an actual printf call, so the debug message is printed. If the #define DEBUG line were removed, the #else branch's empty LOG(msg) would be selected instead, and the LOG("Starting application") call site would expand to nothing at all.

Write a C program that uses the stringification operator # to print both the name of a variable and its value.

The # operator turns the argument count into the string literal "count" without evaluating it, so SHOW_NAME(count) expands to printf("count" " = %d\n", count);. The two adjacent string literals are concatenated by the C language itself, and the program prints "count = 10".

Write a C program that uses the token-pasting operator ## to generate a small set of numbered variable names and prints their values.

Each DECLARE_NUMBERED(n, val) call pastes the digit n directly onto the identifier prefix value using ##, producing real, distinct identifiers value1, value2, and value3 that the compiler treats exactly as if they had been typed by hand, rather than as three separate uses of one name.

Write a C program that defines a SWAP macro using the do { } while (0) pattern and demonstrates that it behaves safely as a single statement inside an if/else.

Wrapping the macro's three statements in do { ... } while (0) makes SWAP(x, y); behave, syntactically, as exactly one statement, so it attaches correctly to the if and leaves the else free to pair with the same if -- without the wrapper, the extra statements and the trailing semicolon could break the if/else structure or accidentally attach the else to the wrong branch.

Write a C program that prints diagnostic information using the predefined macros __FILE__, __LINE__, and __func__.

__FILE__ and __LINE__ are predefined macros that expand to the current source filename and line number at the point they appear, while __func__ is a predefined identifier that automatically holds the name of the enclosing function -- here, report -- making all three useful for building diagnostic or logging output without hardcoding any of that information.

Write a C program that uses #ifndef combined with #error to enforce that a required configuration macro is defined before compiling, and prints a message using that macro once it is.

Because DEVICE_TYPE is defined before the #ifndef check runs, the #error branch is skipped and the program compiles normally, printing the configured device name. If the #define DEVICE_TYPE line were removed, preprocessing would stop immediately with the diagnostic message "DEVICE_TYPE must be defined" instead of letting compilation continue against a missing configuration value.

Directive / Feature   Purpose
--------------------  ------------------------------------------------
#include              Include a header
#define               Define a macro
#undef                Remove a macro definition
#ifdef                Test whether a macro is defined
#ifndef               Test whether a macro is not defined
#if                   Evaluate a preprocessing condition
#elif                 Additional preprocessing condition
#else                 Alternative preprocessing branch
#endif                End conditional block
#error                Produce a preprocessing diagnostic
#pragma                Implementation-specific compiler instructions
#                     Stringify a macro argument
##                    Paste preprocessing tokens together
__FILE__              Current source filename
__LINE__              Current source line number
__DATE__              Translation date
__TIME__              Translation time
__func__              Current function name identifier