Macros in C

Macros are handled by the preprocessor, a text-substitution pass that runs before the compiler ever sees your code. Understanding that one fact - it's text substitution, not a function call - explains almost every macro bug you'll ever hit.


0. Why macros exist, why industry leans on them, and why your syllabus skipped them

Why they're needed at all

C compiles fast and gives you almost no built-in tools for two very common problems:

That's the whole reason the preprocessor exists: it solves problems at the text level, one step earlier than the compiler can help you.

Why real projects lean on them so heavily

Once you look at production C - the Linux kernel, SQLite, Redis, game engines, embedded firmware - macros stop looking optional:

In short: industry C leans on macros not because they're elegant, but because C the language is deliberately minimal, and the preprocessor is the pressure valve that lets real-world engineering constraints (platform differences, performance, avoiding duplication) get solved anyway.

Why your syllabus (probably) didn't go here

This isn't a gap unique to you - it's structural:

And yeah - it is one of the more fun corners of C once you see what it can do. Generating code from a single list of names, computing struct offsets to walk backward from a member to its owner, watching a debug build silently vanish into nothing in release mode - that's the kind of thing that makes low-level programming feel like you're bending the rules a little. It's just usually left for you to discover on your own, in a codebase, rather than in a classroom.


1. Macro Declaration and Usage

The simplest macro: a name that expands to some fixed text.

#define MAX_USERS 100
#define PI 3.14159
#define GREETING "Hello, world!"

int main(void) {
    int users[MAX_USERS];
    double area = PI * 5 * 5;
    printf(GREETING "\n");
    return 0;
}

The preprocessor literally replaces MAX_USERS with 100 wherever it appears, before compilation starts. There's no type, no scope, no const - it's find-and-replace.


2. Function-like macros

Macros can take arguments, making them look like functions:

#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))

int main(void) {
    int result = SQUARE(5);       // expands to ((5) * (5))
    int bigger = MAX(3, 7);       // expands to ((3) > (7) ? (3) : (7))
    printf("%d %d\n", result, bigger);
    return 0;
}

Unlike real functions, there's no type checking, no stack frame, and the "call" is inlined as raw text at compile time.


3. Why every parameter needs parentheses

If you drop the parentheses around macro parameters, operator precedence from the call site can silently break the expansion.

#define BAD_SQUARE(x) x * x
#define GOOD_SQUARE(x) ((x) * (x))

int main(void) {
    int a = BAD_SQUARE(1 + 2);   // expands to 1 + 2 * 1 + 2 = 5, not 9!
    int b = GOOD_SQUARE(1 + 2);  // expands to ((1 + 2) * (1 + 2)) = 9
    printf("%d %d\n", a, b);
    return 0;
}

Rule of thumb: wrap every parameter in parentheses, and wrap the whole expansion in parentheses too.


4. Macros don't evaluate arguments once - side effects get duplicated

Because a macro is textual substitution, an argument that appears more than once in the macro body gets evaluated more than once at the call site.

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

int main(void) {
    int i = 5;
    int result = SQUARE(i++);   // expands to ((i++) * (i++))
    // i is incremented TWICE, and the result is undefined behavior
    printf("%d %d\n", result, i);
    return 0;
}

A real function int square(int x) { return x * x; } would only evaluate i++ once. This is one of the biggest reasons to prefer inline functions over macros when you can.


5. Multi-line macros need \ continuation

A macro body can span multiple lines if each line (except the last) ends with a backslash.

#define LOG_ERROR(msg) \
    do { \
        fprintf(stderr, "[ERROR] %s (%s:%d)\n", msg, __FILE__, __LINE__); \
        exit(1); \
    } while (0)

int main(void) {
    if (!open_file()) {
        LOG_ERROR("could not open file");
    }
    return 0;
}

The do { ... } while (0) wrapper is a well-known idiom (see section 9) that makes a multi-statement macro behave like a single statement.


6. Macros are text substitution - they don't respect string literals

This is the exact bug from clipboard_history.c. The preprocessor expands a macro name wherever it appears as its own token, but it will not reach inside an existing string literal to find and replace text.

#define N_BLANK_LINES "\n\n\n"

int main(void) {
    // WRONG: "N_BLANK_LINES" here is just characters inside a string
    // literal. The preprocessor does not expand macros embedded inside
    // quotes, so this prints the literal text "N_BLANK_LINES".
    printf("%s\nN_BLANK_LINES", "some text");

    // RIGHT: adjacent string literals are concatenated by the compiler,
    // so placing the macro OUTSIDE the quotes lets it expand normally,
    // then the two literals glue together into one string.
    printf("%s\n" N_BLANK_LINES, "some text");

    // ALSO RIGHT: pass it as its own %s argument instead.
    printf("%s\n%s", "some text", N_BLANK_LINES);

    return 0;
}

Takeaway: macros only expand where they appear as a standalone identifier token in the code - never when their name happens to occur inside a string or a comment.


7. Stringification with #

The # operator turns a macro argument into a string literal, as written at the call site.

#define STRINGIFY(x) #x
#define PRINT_VAR(x) printf(#x " = %d\n", x)

int main(void) {
    printf("%s\n", STRINGIFY(hello));   // prints: hello
    printf("%s\n", STRINGIFY(1 + 2));   // prints: 1 + 2 (as text, not 3)

    int count = 42;
    PRINT_VAR(count);   // prints: count = 42
    return 0;
}

This is how assertion macros produce messages like Assertion failed: x > 0, using the source text of the condition itself.


8. Token pasting with ##

The ## operator glues two tokens together into a single new token, useful for generating names.

#define MAKE_GETTER(field) int get_##field(void) { return field; }

static int width = 10;
static int height = 20;

MAKE_GETTER(width)    // expands to: int get_width(void) { return width; }
MAKE_GETTER(height)   // expands to: int get_height(void) { return height; }

int main(void) {
    printf("%d %d\n", get_width(), get_height());
    return 0;
}

## is commonly used to generate families of related function or variable names without hand-writing each one.


9. The do { ... } while (0) idiom

Multi-statement macros need this wrapper so they behave correctly when used after an if without braces - otherwise a stray semicolon or an else can bind incorrectly.

// WITHOUT the wrapper: this breaks in an if/else
#define SWAP_BAD(a, b) { int t = a; a = b; b = t; }

// WITH the wrapper: this is safe everywhere
#define SWAP(a, b) do { int t = (a); (a) = (b); (b) = t; } while (0)

int main(void) {
    int x = 1, y = 2;

    if (x > y)
        SWAP_BAD(x, y);   // the extra ';' after this creates a dangling
                          // empty statement, which silently breaks
                          // any following 'else'
    else
        printf("no swap needed\n");  // won't even compile with SWAP_BAD

    if (x > y)
        SWAP(x, y);       // works correctly with or without an else
    else
        printf("no swap needed\n");

    return 0;
}

10. Variadic macros

Macros can accept a variable number of arguments using ... and __VA_ARGS__.

#define LOG(fmt, ...) printf("[LOG] " fmt "\n", __VA_ARGS__)

int main(void) {
    LOG("starting up");                      // no extra args - see note below
    LOG("user %s logged in with id %d", "ana", 42);
    return 0;
}

Note: the call with zero extra arguments (LOG("starting up")) leaves a trailing comma before an empty __VA_ARGS__ in standard C, which some compilers reject. GCC/Clang support the ##__VA_ARGS__ extension (or C23's __VA_OPT__) to swallow that comma cleanly:

#define LOG(fmt, ...) printf("[LOG] " fmt "\n", ##__VA_ARGS__)

11. Conditional compilation

Macros drive #if/#ifdef to include or exclude code at compile time - useful for platform-specific code, debug builds, and feature flags.

#define DEBUG 1

int main(void) {
#if DEBUG
    printf("debug mode is on\n");
#else
    printf("release build\n");
#endif

#ifdef _WIN32
    printf("compiling on Windows\n");
#elif defined(__linux__)
    printf("compiling on Linux\n");
#else
    printf("unknown platform\n");
#endif

    return 0;
}

This is exactly the mechanism used in clipboard_history.c to require a minimum Windows API version:

#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0600
#endif

12. Include guards

Every header file that might get #included more than once (directly or indirectly) needs a guard to prevent duplicate declarations.

/* myheader.h */
#ifndef MYHEADER_H
#define MYHEADER_H

void do_something(void);
typedef struct { int x, y; } Point;

#endif /* MYHEADER_H */

Most modern compilers also support the simpler, non-standard but widely supported #pragma once as a shorthand for this pattern.


13. Predefined macros

The compiler defines several macros for you automatically, handy for logging and debugging.

int main(void) {
    printf("File: %s\n", __FILE__);
    printf("Line: %d\n", __LINE__);
    printf("Compiled on: %s at %s\n", __DATE__, __TIME__);
    printf("Function: %s\n", __func__);   // technically not a macro, but used the same way
    return 0;
}

14. #undef - removing a macro definition

You can undefine a macro, which is useful when a name from one header conflicts with something you need to define yourself.

#define VERSION 1

#undef VERSION
#define VERSION 2

int main(void) {
    printf("Version: %d\n", VERSION);   // prints 2
    return 0;
}

15. Macros vs. const / inline - when to prefer each

Macros have no type safety and no scoping, so modern C generally prefers alternatives where possible:

// Macro: no type checking, no debugger visibility, textual substitution
#define MAX_SIZE 100

// Preferred where possible: typed, scoped, visible to a debugger
static const int MAX_SIZE_CONST = 100;

// Macro function: duplicates side effects, no type checking
#define SQUARE(x) ((x) * (x))

// Preferred where possible: type-checked, single evaluation of x
static inline int square(int x) { return x * x; }

Macros still earn their keep for things types and functions can't do: conditional compilation, stringification, token pasting, and header guards.


16. X-Macros - generating repetitive code from one source list

An X-Macro is a technique where you define your data once in a list, then "replay" that list through different macro definitions to generate an enum, a string table, a switch statement, etc. - all guaranteed to stay in sync.

/* Define the data once. */
#define COLOR_LIST \
    X(RED)   \
    X(GREEN) \
    X(BLUE)

/* Generate an enum from it. */
typedef enum {
#define X(name) COLOR_##name,
    COLOR_LIST
#undef X
} Color;

/* Generate a matching string table from the SAME list. */
static const char *color_names[] = {
#define X(name) #name,
    COLOR_LIST
#undef X
};

int main(void) {
    Color c = COLOR_GREEN;
    printf("%s\n", color_names[c]);   // prints: GREEN
    return 0;
}

If you add X(YELLOW) to COLOR_LIST, both the enum and the string table update automatically - no risk of them drifting out of sync.


17. __COUNTER__ - a compiler-provided unique integer

__COUNTER__ is a widely supported extension (GCC, Clang, MSVC) that expands to 0, then 1, then 2, etc., incrementing every time it's used. Combined with ##, it's the standard trick for generating unique identifiers, e.g. so a macro can be used more than once in the same scope.

#define CONCAT_INNER(a, b) a##b
#define CONCAT(a, b) CONCAT_INNER(a, b)
#define UNIQUE_NAME(base) CONCAT(base, __COUNTER__)

int main(void) {
    int UNIQUE_NAME(temp) = 1;   // becomes: int temp0 = 1;
    int UNIQUE_NAME(temp) = 2;   // becomes: int temp1 = 2;
    printf("%d %d\n", temp0, temp1);
    return 0;
}

Note the two-layer CONCAT/CONCAT_INNER indirection - this is required (see section 21) so that __COUNTER__ is expanded to its numeric value before ## pastes it, rather than pasting the literal text __COUNTER__.


18. _Pragma, and saving/restoring a macro with push_macro/pop_macro

_Pragma("...") is the operator form of #pragma, usable inside a macro body (#pragma itself cannot appear in a macro expansion). push_macro/pop_macro let you temporarily override a macro and restore it afterward - handy in headers that must not permanently clobber a user's definition.

#define WARN_UNUSED _Pragma("GCC diagnostic push") \
                     _Pragma("GCC diagnostic ignored \"-Wunused-variable\"")

#pragma push_macro("MAX")
#undef MAX
#define MAX(a, b) ((a) > (b) ? (a) : (b))   // safe to use locally

/* ... use this file's own MAX ... */

#pragma pop_macro("MAX")   // restores whatever MAX meant before this file

19. #line - overriding reported file/line info

#line changes what __LINE__ and __FILE__ report from that point on. It's mainly used by code generators (e.g. a parser generator emitting C from a .y grammar file) so error messages point at the original source file instead of the generated one.

#line 100 "original_source.y"
/* From here, __LINE__ reports 100, __FILE__ reports "original_source.y",
   even though this is really line 2 of the generated .c file. */
int x = 1 / 0;   // a compiler warning here will cite original_source.y:100

20. #error and #warning - compile-time diagnostics

Use these to fail the build (or just warn) when a required condition isn't met - e.g. an unsupported platform or a missing configuration macro.

#if !defined(__STDC_VERSION__) || __STDC_VERSION__ < 201112L
#error "This code requires C11 or later"
#endif

#ifndef APP_CONFIG_INCLUDED
#warning "APP_CONFIG_INCLUDED not set - using default configuration"
#define MAX_CONNECTIONS 10
#endif

#error stops compilation immediately with your message; #warning (widely supported, though not in the C standard until C23) just prints a warning and continues.


21. Why token pasting sometimes needs an extra layer of indirection

When a macro argument is itself another macro, # and ## use it unexpanded (as literal text) unless you add a wrapper layer that forces expansion first. This is the same reason UNIQUE_NAME in section 17 needed two macros instead of one.

#define VERSION_MAJOR 2
#define VERSION_MINOR 5

/* WRONG: pastes the literal text "VERSION_MAJOR" and "VERSION_MINOR" */
#define VERSION_BAD(a, b) a##_##b
// VERSION_BAD(VERSION_MAJOR, VERSION_MINOR) -> VERSION_MAJOR_VERSION_MINOR

/* RIGHT: the outer macro forces argument expansion before the inner
   macro performs the paste */
#define VERSION_CONCAT(a, b) a##_##b
#define VERSION_JOIN(a, b) VERSION_CONCAT(a, b)
// VERSION_JOIN(VERSION_MAJOR, VERSION_MINOR) -> 2_5

Same rule for #: if you want the value of a macro stringified rather than its name, add an indirection layer:

#define VALUE_TO_STRING(x) #x
#define STRINGIFY(x) VALUE_TO_STRING(x)

#define BUILD_NUMBER 42
printf("%s\n", STRINGIFY(BUILD_NUMBER));       // "42"
printf("%s\n", VALUE_TO_STRING(BUILD_NUMBER)); // "BUILD_NUMBER" (wrong!)

22. __has_include - checking whether a header exists

A widely supported feature (standardized in C23, available as an extension in GCC/Clang/MSVC long before that) for conditionally including a header only if it's present - useful for optional dependencies or platform shims.

#if defined(__has_include)
#  if __has_include(<stdatomic.h>)
#    include <stdatomic.h>
#    define HAVE_ATOMICS 1
#  else
#    define HAVE_ATOMICS 0
#  endif
#endif

int main(void) {
#if HAVE_ATOMICS
    printf("atomics available\n");
#else
    printf("no atomics - using fallback locking\n");
#endif
    return 0;
}

23. offsetof and the container_of pattern

offsetof (from <stddef.h>) is a standard macro that computes a struct member's byte offset. container_of builds on it to recover a pointer to the whole struct given a pointer to one of its members - a pattern used heavily in intrusive linked lists (e.g. the Linux kernel).

#include <stddef.h>

#define container_of(ptr, type, member) \
    ((type *)((char *)(ptr) - offsetof(type, member)))

typedef struct {
    int id;
    struct { int x, y; } position;  /* embedded member */
} Entity;

int main(void) {
    Entity e = { .id = 7, .position = { 3, 4 } };
    void *pos_ptr = &e.position;

    /* Recover the owning Entity* from just a pointer to its member. */
    Entity *owner = container_of(pos_ptr, Entity, position);
    printf("id = %d\n", owner->id);   // prints: 7
    return 0;
}

24. Type-generic macros with _Generic (C11)

_Generic isn't a macro itself, but it's almost always used through a macro, giving you a form of compile-time function overloading based on argument type.

#include <math.h>

#define abs_generic(x) _Generic((x), \
    int: abs,           \
    long: labs,         \
    float: fabsf,       \
    double: fabs        \
)(x)

int main(void) {
    printf("%d\n", abs_generic(-5));       // calls abs(int)
    printf("%f\n", abs_generic(-5.5));     // calls fabs(double)
    return 0;
}

This is exactly how the standard library implements generic-looking macros like tgmath.h's functions.


25. Macro-based assertions and mini test frameworks

Assertion and test macros combine several tricks from this guide - stringification, __FILE__/__LINE__, and the do {} while(0) wrapper - to produce useful failure messages while still behaving like a single statement.

#define ASSERT(cond) \
    do { \
        if (!(cond)) { \
            fprintf(stderr, "Assertion failed: %s (%s:%d)\n", \
                    #cond, __FILE__, __LINE__); \
            abort(); \
        } \
    } while (0)

#define TEST_EQ(actual, expected) \
    do { \
        if ((actual) != (expected)) { \
            fprintf(stderr, "FAIL %s:%d: expected %d, got %d\n", \
                    __FILE__, __LINE__, (int)(expected), (int)(actual)); \
            tests_failed++; \
        } else { \
            tests_passed++; \
        } \
    } while (0)

static int tests_passed = 0, tests_failed = 0;

int main(void) {
    ASSERT(1 + 1 == 2);
    TEST_EQ(2 + 2, 4);
    TEST_EQ(2 + 2, 5);   // will report a failure with file/line info

    printf("%d passed, %d failed\n", tests_passed, tests_failed);
    return 0;
}

This is essentially how assert() itself, and most lightweight C testing libraries, are built.


26. How macro expansion actually works (the rescanning algorithm)

Most macro confusion traces back to not knowing the real algorithm the preprocessor follows. In short, for a function-like macro invocation:

  1. Arguments are identified by matching parentheses (commas inside nested parentheses don't split arguments).
  2. Each argument is macro-expanded on its own first (unless it's an operand of # or ##, which use the raw, unexpanded argument text - this is exactly why section 21's extra indirection layer is needed).
  3. The macro body is expanded using those (possibly-expanded) arguments.
  4. The result is rescanned for more macro names to expand, including ones that were just pasted together by ##.
  5. A macro will not expand itself recursively - if its own name reappears during rescanning, that occurrence is left alone (this is informally called the "blue paint" rule, since the preprocessor effectively marks that token as "already expanded, don't touch again").
#define A B
#define B A

int main(void) {
    int A;   // expands: A -> B -> A, then stops (A is "painted", left as-is)
    /* declares a variable literally named A, NOT infinite recursion */
    return 0;
}

Knowing steps 2 and 5 in particular resolves the majority of "why didn't my macro expand the way I expected" questions.


27. Common pitfalls checklist

A fast reference for the mistakes that account for most real-world macro bugs (including the one that started this guide):


Quick reference

FeatureSyntaxPurpose
Object-like macro#define NAME valueNamed constant / text substitution
Function-like macro#define NAME(x) ...Inline "function" via text substitution
Stringify#xTurn argument into a string literal
Token pastea ## bGlue two tokens into one identifier
Line continuation\ at end of lineMulti-line macro body
Variadic macro..., __VA_ARGS__Accept a variable number of arguments
Conditional compile#if, #ifdef, #endifInclude/exclude code at compile time
Include guard#ifndef/#define/#endifPrevent duplicate header inclusion
Undefine#undef NAMERemove a macro definition
X-MacroX(...) list + replayGenerate synced enums/tables/code from one list
Unique counter__COUNTER__Compiler-generated incrementing integer
Pragma operator_Pragma("...")Use #pragma inside a macro body
Save/restore macro#pragma push_macro/pop_macroTemporarily override a macro definition
Override line/file info#line N "file"Used by code generators for diagnostics
Compile-time error#error "msg"Fail the build with a message
Compile-time warning#warning "msg"Warn without failing the build
Header existence check__has_include(<hdr>)Conditionally include an optional header
Struct offsetoffsetof(type, member)Byte offset of a struct member
Owning struct recoverycontainer_of(ptr, type, member)Get struct pointer from member pointer
Type-based dispatch_Generic((x), type: fn, ...)Compile-time "overloading" by argument type
Indirection layerwrapper macro calling inner #/## macroForce argument expansion before stringify/paste

Coverage note

This guide covers the C preprocessor as specified through C17, plus widely supported GCC/Clang/MSVC extensions (__COUNTER__, #warning, __has_include pre-C23) and the handful of C23 additions mentioned inline (__VA_OPT__, standardized __has_include, standardized #warning). Preprocessor behavior is otherwise compiler-independent by the standard, so everything here applies across GCC, Clang, and MSVC unless an extension is explicitly called out as such. If you've read all 27 sections, you have everything needed to read, write, and debug any macro you'll encounter in real-world C code.

Previous:
File Handling in C
Next:
Structs and Unions in C