Topic 91 of 100 Preprocessors and Macros Interactive Tutorial

Macros in C: Object-Like, Function-Like, and Other Macro Forms

13 mins read Last updated: 22 Aug 2026 25,900 views
C Compiler 0.00%

What is Macros in C? Core Concept & Direct Summary

Core Objective & Problem Statement

What is the difference between object-like macros, function-like macros, stringification (`#`), and token-pasting (`##`) operators?

Direct Answer Summary

A macro is a name for a piece of text, and using it replaces that name with that text before the compiler runs.

main.c
Run Online
#define MAX 100
#define SQUARE(x)((x)*(x))

That's the whole concept. The #define lines vanish, MAX becomes 100, and SQUARE(7) becomes ((7)*(7)) — before the compiler even starts.

Simple idea, enormous consequences. Because the preprocessor works on text and knows nothing about types, scope, or syntax, macros are simultaneously the most powerful and the most dangerous feature in C. They can do things functions cannot — and they can silently produce wrong answers in code that looks perfectly correct.

This page covers the full family of macro forms, then goes into the part most explanations skip: exactly how the expansion algorithm works, which is what lets you predict what a macro will do instead of guessing.

(Suggested image: the hero graphic — SQUARE(2+3) on one side, an arrow labeled "preprocessor", and ((2+3)*(2+3)) = 25 on the other)

How Does Macros in C Work? In-Depth Mechanics & Architecture

The Full Family of Macro Forms

"Object-like" and "function-like" are the two you'll write most, but they aren't the only forms. Here's the complete set — the rest of the page takes each in turn.

FormExampleExpands toTypical use
Object-like#define MAX 100100Constants, configuration values
Function-like#define SQ(x) ((x)*(x))an expressionInline operations
Variadic#define LOG(f,...) ...a callWrapping printf-style functions
Parameterless function-like#define PLATFORM()textCatching "used like a function" mistakes
Empty#define DEBUGnothingFlag for #ifdef
Statement#define SWAP(a,b) do{...}while(0)a statementMulti-step operations
Do-nothing#define DBG(...) ((void)0)nothingCompiling features out entirely
Type-generic_Generic-baseda dispatchOne name, many types
Dispatch / arityCOUNT_ARGS(...)an integerOverloading on argument count
List (X-macro)repeated includes of one listgenerated codeKeeping parallel tables in sync
Predefined__LINE__, __FILE__a valueDiagnostics
Guard / marker#define LIB_H(nothing used)Include guards
Alias#define u32 unsigned intthe typeLegacy type shorthands

Prerequisite note: the preprocessor itself — #include, #ifdef, conditional compilation, guards — is covered in the preprocessor guide. This page is specifically about macros.


Object-Like Macros

A name that expands to a fixed token sequence. No parameters, no parentheses.

main.c
Run Online
#define PI 3.141592653589793
#define MAX_ITEMS 100
#define APP_NAME "PreprocDemo"
#define PATH_SEP '/'
#define TAB '\t'
#define FOREVER for (;;)

Used anywhere afterward, the name is replaced:

main.c
Run Online
int buffer[MAX_ITEMS];
/* → int buffer[100];
*/printf("%s\n", APP_NAME);
/* → printf("%s\n", "PreprocDemo");
*/

Properties worth knowing

A macro has no type, no address, and no scope.MAX_ITEMS is not a variable — it's a name for the text 100. So &MAX_ITEMS is an error and MAX_ITEMS++ is nonsense.

Macros are not substituted inside string literals. With #define VERSION 3, the literal "version VERSION" stays exactly as written — the preprocessor treats the whole quoted string as one token.

The effect runs from the definition to the end of the file — through function boundaries, past the closing brace of whatever function it appeared in, and into any file that includes this one afterward. There's no block scoping.

A #define inside a function still has file scope from that point. It reads as local and isn't:

main.c
Run Online
void f(void) {
    #define LOCAL 5 printf("%d\n", LOCAL);
}
/* LOCAL is now defined for the REST OF THE FILE, including code below f() */

Redefinition rules

Redefining a macro with an identical definition is legal and does nothing. Redefining it with a different body is undefined behavior — and in practice, a diagnostic or a silent use of the first definition depending on the compiler:

main.c
Run Online
#define SIZE 10
#define SIZE 20 /* ⚠ redefinition — GCC: warning, MSVC: warning, both may keep 10 */

To deliberately replace a macro, #undef it first:

main.c
Run Online
#undef SIZE
#define SIZE 20 /* ✓ clean */

#undef is also essential for temporary macros — the pattern behind X-macros, where a generic name like X is defined, used, and undefined repeatedly.

Object-like macros vs const and enum

Here's a design question people rarely ask: if you just need a constant, should it be a macro?

Macro (#define)const variableenum constant
Has a typeNoYesYes (an integer type)
Has an addressNoYesNo
Visible in a debuggerNoYesSometimes
Usable in #ifYesNoNo
Respects scopeNoYesYes
Usable as array sizeYesOnly in C99 VLAs/constantsYes
Participates in type checkingNoYesYes

The guidance:

  • Use enum for related integer constants — flags, states, error codes, sizes. You get names, scope, type checking, and debugger visibility.
  • Use const for typed constants — const double PI = 3.14159; gives you a real, typed, scoped object the compiler can check and the debugger can show.
  • Use a macro only when you need what they can't do — participation in #if, use as a case label or array bound in every context, type-generic behaviour, or operation on the caller's expression.

The old habit of using macros for everything prefixed with #define is a C89 inheritance. Modern C has better tools for most of it.

(Suggested image: a three-column comparison card — macro vs const vs enum — with the "use macro only when" row highlighted)


Function-Like Macros

Put parentheses immediately after the name and it takes parameters:

main.c
Run Online
#define SQUARE(x)((x)*(x))
#define MAX(a,b)((a)>(b)?(a):(b))
#define MIN(a,b)((a)<(b)?(a):(b))
#define CLAMP(v,lo,hi)((v)<(lo)?(lo):((v)>(hi)?(hi):(v)))

The space that changes everything

The ( must be adjacent — no space, no tab, no comment:

main.c
Run Online
#define SQUARE(x)((x)*(x)) /* ✓ function-like, one parameter      */
#define SQUARE(x)((x)*(x)) /* ⚠ NO — a space makes the name SQUARE */ /*   an OBJECT-like macro whose body    */ /*   begins with the tokens  (x) ((x)...*/

Now SQUARE(9) expands to (x) ((x)*(x))(9) — a spectacular, baffling error. This quirk exists nowhere else in the language and it catches nearly everyone once. Verify with gcc -E when a macro misbehaves mysteriously.

The parenthesis rule — both halves

#define SQUARE(x) x*x is the canonical trap. Verified expansion:

main.c
Run Online
SQUARE(x) = x*x SQUARE(2+3) -> 2+3*2+3 = 11 <-- WRONGSQUARE(x) =((x)*(x)) SQUARE(2+3) ->((2+3)*(2+3)) = 25 <-- CORRECT

The argument's text is dropped in without parentheses, so 2+3 becomes part of a bigger expression and normal precedence does the rest. 2 + 3*2 + 3 = 11, not 25.

And the body needs its own outer parentheses — verified:

main.c
Run Online
ADDPAIR body is(x) +(x): no OUTER parentheses2 * ADDPAIR(3) expands to -> 2 *(3) +(3) = 9(expected 12)

Each parameter was parenthesized; the body wasn't, so composing the macro inside a larger expression broke it. 2 * (3) + (3) evaluates as 6 + 3, not 2 * 6.

Wrap every parameter in parentheses. Wrap the entire body in parentheses too. No exceptions.

Arguments with side effects

Parentheses fix precedence. They cannot fix this:

main.c
Run Online
#define MAX(a,b)((a)>(b)?(a):(b))int m = MAX(i++, j++);

a appears twice in the body, so i++ is potentially evaluated twice — and b likewise. Verified, with i = 1, j = 2:

main.c
Run Online
result m = 3i is now 2(incremented 1x)j is now 4(incremented 2x) <-- TWICE, not once!total side effects: 3 increments for ONE apparent "argument each"

Two ++ operators produced three increments. That's undefined behavior, and the call site looks entirely innocent.

The rule: never pass an argument that has a side effect to a macro that uses it more than once — and since you can't tell that from the call site, the practical version is stronger: never pass a side-effecting expression to a macro at all.

Parameterless function-like macros

This looks pointless next to #define PLATFORM "linux", but the difference is real: PLATFORM without parentheses does not expand. That's occasionally useful when a name must be an identifier in one place and expand in another.

(Suggested image: the SQUARE comparison with the expansions and their values 11 vs 25 marked wrong/correct)


How Macro Expansion Actually Works

This is the part that turns guesswork into prediction. The C standard specifies the algorithm; here it is in five steps.

Step 1 — Collect the arguments. When a function-like macro name is followed by (, the preprocessor scans forward, tracking parenthesis nesting, and splits the argument list on commas at nesting depth 1.

Step 2 — Pre-expand each argument. Unless the parameter is used with # or ## in the body, the argument's text is fully macro-expanded first.

Step 3 — Substitute. Replace each parameter in the body with its (pre-expanded, or raw, per step 2) argument text.

Step 4 — Rescan. The result is scanned again for more macro names.

Step 5 — Don't recurse. A macro currently being expanded is marked (informally "painted blue") and is not expanded again during its own expansion.

Every surprising macro behavior traces back to one of those five steps.

Step 4 in action: chains expand

Verified:A expands to 42. A → B, then rescanning finds B → 42. Macros compose through as many layers as you define, and rescanning is why nested macros work at all.

Step 5 in action: recursion is impossible

Verified:foo expands to foo and stops. No infinite loop — the macro is blue-painted during its own expansion, so the second foo is left alone.

This is why you can't write a self-recursive macro. Attempts to get iteration out of the preprocessor (the DEFER/OBSTRUCT/"recursive macros" techniques) work by defeating this rule through indirection — clever, well documented, and generally a sign you want a code generator instead.

Step 2 in action: # and ## switch pre-expansion off

This is the deepest and most useful rule, and most tutorials state it without explaining why it matters.

Verified:

main.c
Run Online
STR(VERSION) expands to -> "VERSION" <- NOT expanded(uses #)XSTR(VERSION) expands to -> "3" <- expanded: STR(x) applies
#, not XSTRCAT(VERSION,X) expands to -> VERSIONX <- pasted UNEXPANDED by
#
#

Given #define VERSION 3:

  • #define STR(x) #x → STR(VERSION) gives the literal text"VERSION", because the argument is used with # and therefore never pre-expanded.
  • #define XSTR(x) STR(x) → XSTR(VERSION) gives "3", because XSTR uses xplainly, so the argument is expanded to 3before being handed to STR.

That's the whole reason the two-level XSTR idiom exists — and it has a practical consequence beyond stringizing:

main.c
Run Online
#define CAT_(a,b) a
#
#b /* pastes RAW tokens        */
#define CAT(a,b) CAT_(a,b) /* expands args FIRST, then pastes */
CAT(static_assert_, __LINE__) /* → static_assert_42, using the real line number */CAT_(static_assert_, __LINE__) /* → static_assert___LINE__  — WRONG, __LINE__ never expanded */

Because ## suppresses pre-expansion, you need a plain-parameter wrapper to force __LINE__ to expand before pasting. Every "generate a unique name" macro in existence uses this two-layer form, and now you know why.

Step 1 in action: the comma problem

Argument splitting is by commas — and the preprocessor doesn't know that a comma inside a ternary or a template-like construct belongs to one argument. Verified:

main.c
Run Online
MAX(a ? b, c : d, e) supplies 3 arguments: a ? b | c : d | eMAX declares 2 parameters -> compiler error: error: macro "MAX" passed 3 arguments, but takes just 2
parenthesized: MAX((a ? b : c), e) supplies 2:(a ? b : c) | e

The ternary's comma was read as an argument separator. Wrapping the whole expression in parentheses raises the nesting depth and protects the comma. That's why macros with comma-containing arguments — compound literals, braced initializers, comma operators — must be parenthesized at the call site.

And string literals are opaque

Macro names inside a string literal are not expanded. Verified: stringizing a b c produced "a b c" — and critically, the identifiers a and b inside that resulting literal were never expanded (even though macros named A and B existed), because the preprocessor treats a quoted string as a single token.

That's a feature: it means string content can never be accidentally rewritten by a macro.

(Suggested image: the five-step algorithm as a flow — collect, pre-expand, substitute, rescan, blue-paint — with "the five behaviors" annotated on the relevant steps)


The # Operator: Stringize

Inside a macro body, # before a parameter turns that argument's text into a string literal:

main.c
Run Online
#define STR(x)
#x
#define XSTR(x) STR(x)
STR(hello) /* → "hello"          */STR(1 + 2) /* → "1 + 2"          (not 3 — it's text) */STR(VERSION) /* → "VERSION"        (verified: not expanded) */XSTR(VERSION) /* → "3"              (verified: expanded first) */

Whitespace is normalized.Verified:STR(a b c) produces "a b c" — runs of whitespace collapse to a single space, and leading/trailing whitespace is removed. That's specified behaviour, so STR is safe on code fragments.

What # cannot do:

  • It can't stringize __VA_ARGS__ directly in C99/C11 — #__VA_ARGS__ is ill-formed. You need a variadic helper or the C23 approach.
  • It can't stringize a non-parameter.# applies only to a parameter name.
  • It doesn't produce a runtime string from a runtime value. It operates on the text of the argument at preprocessing time. STR(i) gives "i", not "42".

(Suggested image: the argument text a b c being wrapped in quotes with whitespace runs collapsed to single spaces)


The ## Operator: Token Paste

## glues two tokens into one:

main.c
Run Online
#define CAT(a,b) a
#
#b
CAT(var, 1) /* → var1         */CAT(int_, 32) /* → int_32       */CAT(my, Func) /* → myFunc       */

It is how you generate identifiers — the mechanism behind X-macros, dispatch tables, generated accessors, and the static_assert_##__LINE__ uniqueness trick above.

Rules and traps

Arguments used with ## are not pre-expanded (verified above with CAT(VERSION, X) → VERSIONX, not 3X). Use the two-level wrapper when you need expansion first.

The paste must form a valid token. Gluing things that don't combine into one token is undefined behavior:

main.c
Run Online
CAT(+, -) /* → +-   — not a single valid token: UB   */CAT(3, .) /* →
3.  — incomplete float literal: UB    */

## combines tokens, not text. CAT(a, b) where a is x and b is y yields the single identifier xy, not the two tokens x y.

## cannot be first or last in a body.#define X ##a and #define X a## are both errors — there'd be nothing to paste with.

A comma can be pasted in: the GNU ,##__VA_ARGS__ idiom works precisely because ## may delete a token when the paste partner is empty.

(Suggested image: two tokens var and 1 with ## between them fusing into var1)


Variadic Macros and __VA_ARGS__

C99 added macros that accept a trailing run of arguments:

main.c
Run Online
#define LOG(fmt, ...) fprintf(stderr, fmt, __VA_ARGS__)

__VA_ARGS__ stands for everything after the named parameters, commas included. Its main purpose is wrapping functions that are themselves variadic — printf, fprintf, snprintf — which is exactly what logging needs:

main.c
Run Online
LOG("count = %d\n", count);
/* → fprintf(stderr, "count = %d\n", count) */

The empty-argument problem

In C99 and C11, if __VA_ARGS__ is empty you get a dangling comma. Verified:

main.c
Run Online
#define LOG(fmt, ...) fprintf(stderr, fmt, __VA_ARGS__)LOG("done") expands to -> fprintf(stderr, "done",) ^ trailing comma, nothing after it -> error
LOG("count = %d", count) expands to -> fprintf(stderr, "count = %d", count) ^ fine: __VA_ARGS__ is non-empty

That's a genuine gap in the C99 design, and there are three responses:

1. Require at least one variable argument. Simplest and fully portable — document it, and callers pass a trailing "" or 0 if they have nothing:

main.c
Run Online
LOG("done", 0);
/* awkward but portable */

2. The GNU extension ,##__VA_ARGS__ — a ## before __VA_ARGS__ deletes the preceding comma when the arguments are empty. Widely supported (GCC, Clang, and others), used everywhere in practice:

main.c
Run Online
#define LOG(fmt, ...) fprintf(stderr, fmt, ##__VA_ARGS__)LOG("done") /* → fprintf(stderr, "done")   ✓ verified */

It's an extension, not standard C before C23 — so it's a portability decision, not a free win.

3. C23's __VA_OPT__( ) — the standardized solution: the content inside __VA_OPT__ is emitted only when __VA_ARGS__ is non-empty.

main.c
Run Online
#define LOG(fmt, ...) fprintf(stderr, fmt __VA_OPT__(,) __VA_ARGS__)LOG("done") /* → fprintf(stderr, "done")           */LOG("%d\n", n) /* → fprintf(stderr, "%d\n", n)        */

Recommendation: use __VA_OPT__ if you can require C23; use ,##__VA_ARGS__ if your compilers support it and you accept the portability note; otherwise require a trailing argument.

(Suggested image: the three fixes side by side — required argument, GNU ##, and C23 VA_OPT — with the resulting expansion under each)


Counting Arguments: The NARG Idiom

Because __VA_ARGS__ can be a list, the preprocessor can count the list. The trick: append a descending sequence and let positional binding pick out the right value.

main.c
Run Online
#define COUNT_ARGS(...) COUNT_ARGS_(__VA_ARGS__, 5, 4, 3, 2, 1, 0)
#define COUNT_ARGS_(a, b, c, d, e, N, ...) N

Verified binding:

main.c
Run Online
COUNT_ARGS(x) -> COUNT_ARGS_(x, 5, 4, 3, 2, 1, 0) -> N = 1COUNT_ARGS(x, y) -> COUNT_ARGS_(x, y, 5, 4, 3, 2, 1, 0) -> N = 2COUNT_ARGS(x, y, z) -> COUNT_ARGS_(x, y, z, 5, 4, 3, 2, 1, 0) -> N = 3COUNT_ARGS(x, y, z, w) -> COUNT_ARGS_(x, y, z, w, 5, 4, 3, 2, 1, 0) -> N = 4COUNT_ARGS(x, y, z, w, v) -> COUNT_ARGS_(x, y, z, w, v, 5, 4, 3, 2, 1, 0) -> N = 5

How it works: the real arguments are substituted first (step 2), pushing the tail sequence to the right. Then the helper's parameters a through e consume the surplus and N lands on the correct count. Adding ... at the end of the helper absorbs anything beyond.

What it's for — overloading on argument count:

main.c
Run Online
#define LOG_1(msg) logWrite(msg)
#define LOG_2(fmt, a) logWriteF(fmt, a)
#define LOG_CHOOSER(_1,_2,NAME,...) NAME
#define LOG(...) LOG_CHOOSER(__VA_ARGS__, LOG_2, LOG_1)(__VA_ARGS__)
LOG("hello");
/* → LOG_1("hello")        */LOG("%d", 42);
/* → LOG_2("%d", 42)       */

The honest assessment: this is a genuinely impressive technique and it appears in real libraries. It's also the point where macros stop being a convenience and start being a sublanguage — with error messages that are notoriously unreadable, and no debugger support. If you find yourself needing argument-count dispatch, reconsider: a function with a fewer-argument wrapper, an array-and-count parameter, or C11 _Generic is usually clearer.

(Suggested image: the NARG binding visualized — the argument list with the 5,4,3,2,1,0 tail appended and the N slot landing on the count)


Type-Generic Macros With _Generic

C11's _Generic selects an expression based on a type — which, wrapped in a macro, gives you one name that works across types:

main.c
Run Online
#include <math.h>
#define ABS(x) _Generic((x), \ int: abs, \ long: labs, \ long long: llabs, \ float: fabsf, \ double: fabs \)(x)
main.c
Run Online
int i = -5;
double d = -3.7;
printf("%d\n", ABS(i));
/* → abs(i)   — integer version  */printf("%.2f\n", ABS(d));
/* → fabs(d)  — double version   */

The macro picks the right function by type and immediately calls it. This is the modern alternative to the old "one macro per type" approach (ABS_INT, ABS_DOUBLE, ...), and it's type-checked: a type not in the list is a compile error, not a silent wrong answer.

Points to know:

  • _Generic is a keyword, not a macro — it's a C11 language feature. It's used inside macros to make them type-generic.
  • The type must match exactly. An unsigned int argument won't select int; add explicit entries or a default: branch.
  • All branches must be valid expressions, but only the selected one is evaluated. That's what makes it work like an overload.
  • A default: branch is optional but wise — without it, an unlisted type is a compile error (which is often exactly what you want).

(Suggested image: one ABS name fanning out to five typed implementations, with the selected one highlighted per call)


A Macro Idiom Library

Patterns worth having in hand — each with its caveats.

main.c
Run Online
/* --- comparison --- */
#define MIN(a,b)((a) <(b) ?(a) :(b))
#define MAX(a,b)((a) >(b) ?(a) :(b))
#define CLAMP(v,lo,hi)((v) <(lo) ?(lo) :((v) >(hi) ?(hi) :(v)))/* ⚠ each argument may be evaluated twice — never pass i++ */
/* --- array length (the one thing only a macro can do) --- */
#define ARRAY_LEN(a)(sizeof(a) / sizeof((a)[0]))/* ⚠ only correct for a real array in the same scope; on a pointer it lies */
/* --- bit manipulation --- */
#define BIT(n)(1UL <<(n))
#define SET_BIT(v,n)((v) |= BIT(n))
#define CLEAR_BIT(v,n)((v) &= ~BIT(n))
#define TEST_BIT(v,n)(((v) & BIT(n)) != 0)
/* --- name and text generation --- */
#define STRINGIFY_(x)
#x
#define STRINGIFY(x) STRINGIFY_(x) /* two-level: expands first */
#define CAT_(a,b) a
#
#b
#define CAT(a,b) CAT_(a,b) /* two-level: expands first */
/* --- compile-time assertion (C89-compatible) --- */
#define STATIC_ASSERT(cond, tag) \ typedef char CAT(static_assert_, tag)[(cond) ? 1 : -1]/* STATIC_ASSERT(sizeof(int) == 4, int_size); */
/* --- structure member offset --- */
#define OFFSETOF(type, member)((size_t)&(((type *)0)->member))/* <stddef.h> provides a proper offsetof(); this shows the idea */
/* --- silences an unused-parameter warning --- */
#define UNUSED(x)((void)(x))
/* --- a do-nothing macro for features compiled out --- */
#define NOTHING(...)((void)0)

Note the two-level STRINGIFY and CAT in that list. They exist precisely because of the pre-expansion rule verified earlier — the one-level versions would refuse to expand __LINE__, VERSION, or any macro argument. Every serious codebase has these two-layer forms in a macros.h, and now you know the reason rather than copying them blindly.

(Suggested image: the idiom library rendered as a reference card — a strong candidate for a shareable/downloadable graphic)


Macros vs Functions: The Decision Table

ConsiderationMacroFunction (static inline in a header)
Type checkingNoneFull
Argument evaluationMay run more than onceExactly once
Side effects in argumentsDangerousPerfectly safe
DebuggableNo — invisible at runtimeYes
ScopeIgnoredProper
Code sizeOne copy per use siteOne copy (inlined or not)
Call overheadNoneNegligible when inlined
Can take a type as an argumentYesNo
Works on any type textuallyYesNo — one per type, or _Generic
sizeof on the caller's arrayYes (ARRAY_LEN)No — decays to a pointer
# / ## / __VA_ARGS__YesNo
Can appear in #ifYesNo
Error messages when misusedOften incomprehensibleClear

The rule of thumb:

Write a function. Reach for a macro only when you need something a function structurally cannot do — take a type, apply sizeof to the caller's object, generate identifiers, or participate in conditional compilation.

static inline in a header solves the "function call overhead" justification almost entirely: the compiler inlines it where beneficial and emits one out-of-line copy otherwise, with full type checking, single evaluation, and debuggability intact. It's strictly better than a function-like macro for anything that behaves like a function.

(Suggested image: the decision table as a two-column card, with the "reach for a macro only when..." line emphasized)


Debugging Macros

Macros are invisible in the compiler's output and the debugger, so the tooling matters more than usual.

ToolWhat it does
gcc -E file.cShow the translation unit — the fastest way to see exactly what a macro expanded to
gcc -E -P file.cSame, without #line markers — far easier to read
gcc -E -dD file.cKeep #define directives in the output too
gcc -dM -E - < /dev/nullDump every predefined macro for your compiler/target
-DNAME / -DNAME=valueDefine a macro from the command line — -DDEBUG, -DMAX=200
-UNAMEUndefine a macro from the command line
-Wmacro-redefinedWarn when a macro is redefined differently
-WundefWarn when #if tests an undefined identifier (which otherwise silently evaluates to 0)
-ftrack-macro-expansion=2 (GCC)Much better diagnostics pointing into macro expansions
-WshadowCatches macro locals shadowing outer variables

The workflow: when a macro does something inexplicable, preprocess the file and read the expansion. It almost always takes ten seconds. Trying to reason about the macro in your head is what takes an hour.

And a habit worth forming: compile with -Wall -Wextra -Wundef on any project with macros. Between -Wundef catching silent #if typos and -ftrack-macro-expansion improving the error messages, you'll catch a category of bugs that are otherwise very hard to find.

(Suggested image: a macro error message before and after -ftrack-macro-expansion, showing the improved pointing)


Common Mistakes

  1. Missing parentheses around parameters — SQUARE(2+3) gives 11 instead of 25 (verified). Wrap every parameter.
  2. Missing outer parentheses on the body — 2 * ADDPAIR(3) gives 9 instead of 12 (verified). Wrap the whole body.
  3. Passing side-effecting arguments — MAX(i++, j++) incremented jtwice and produced 3 increments total (verified). Undefined behavior, invisible at the call site.
  4. A space between the name and ( — #define F (x) is an object-like macro, not a function-like one. Completely different meaning.
  5. Multi-statement macros without do { } while (0) — breaks when used in a brace-less if, because the caller's ; becomes a null statement and orphans a following else.
  6. Expecting # to stringize an expanded value — STR(VERSION) gives "VERSION", not "3" (verified). Use the two-level XSTR.
  7. Using a one-level ## when you need expansion — CAT(a, __LINE__) pastes a__LINE__ literally (verified). Use the two-level CAT.
  8. Pasting tokens that don't form one valid token — CAT(+, -) is undefined behavior; ## fuses tokens, it doesn't concatenate text.
  9. Using ## at the start or end of a body — an error; there must be tokens on both sides.
  10. Empty __VA_ARGS__ with a plain comma — LOG("done") expands to a trailing comma and fails to compile (verified). Use __VA_OPT__ (C23), GNU ,##__VA_ARGS__, or require a trailing argument.
  11. #__VA_ARGS__ in C99/C11 — ill-formed; stringizing a variadic tail needs a helper.
  12. Commas inside macro arguments — MAX(a ? b, c : d, e) is read as 3 arguments (verified) and errors. Parenthesize the argument.
  13. Assuming ARRAY_LEN works on a pointer — it silently returns the pointer size divided by the element size. Only correct for a real array in the same scope.
  14. Relying on #define for scoping — a #define inside a function affects the rest of the file. #undef when you need to end it.
  15. Redefining a macro differently — undefined behavior/diagnostic. #undef first if the redefinition is intentional.
  16. Assuming a macro name expands inside a string literal — it doesn't; quoted strings are opaque tokens.
  17. Using #if VERSION > 2 with a typo'd name — undefined identifiers evaluate to 0 silently. Enable -Wundef.
  18. Macro names that collide with variables or functions — the macro wins, and the error can appear somewhere unrelated. Avoid short generic names (min, max, count, index).
  19. Reaching for a macro when a function would do — no type checking, double evaluation, and no debuggability are all avoided by static inline or a plain function.
  20. Nesting seven layers of argument-count dispatch — technically impressive, practically undebuggable. Use a function or _Generic.
  21. Forgetting #undef after a temporary macro — especially in X-macro patterns where a generic name like X is reused.
  22. Guard-macro style names for ordinary constants — accidental collisions with include-guard names silently disable whole headers. Keep guard names long and distinctive.

(Suggested image: "22 Macro Mistakes" checklist — with #1, #3, and #7 highlighted as the ones that compile clean and give wrong answers)


Final Thoughts

A macro is a name for text. Everything else — object-like, function-like, variadic, empty, statement, dispatch, list — is a variation on that single fact. And the reason to understand the expansion algorithm isn't academic: it's the only way to predict what a macro will do.

The five steps from this page cover every surprise you'll meet:

  • Arguments are collected by parenthesis nesting and split on top-level commas — which is why a comma inside a ternary breaks your call
  • Arguments are pre-expanded before substitution — which is why XSTR works and STR doesn't
  • # and ## suppress that pre-expansion — which is why the two-layer STRINGIFY and CAT exist
  • The result is rescanned — which is why macro chains expand
  • A macro is blocked during its own expansion — which is why recursion can't loop

Two rules prevent most macro bugs outright: parenthesize every parameter and the whole body, and never pass a side-effecting expression to a macro. A third prevents the rest: write a function unless the macro can do something a function structurally cannot.

Macros are the sharpest tool in C and the one most likely to cut you. Use them deliberately — for constants and conditional compilation, for sizeof-on-the-caller's-array idioms, for generating identifiers and dispatch tables — and reach for static inline, enum, _Generic, or a plain function everywhere else. When they misbehave, gcc -E and read the expansion. It's almost always immediate.

Runnable C Code Example & Practical Implementation

main.c
Run Online
#include <stdio.h>
#define SQUARE(x)((x) *(x))
#define TO_STR(s)
#s
#define CONCAT(a, b) a
#
#b
int main() {
    int value = 5;
    printf("Square of (value + 1): %d\
", SQUARE(value + 1));
    // (5+1)*(5+1) = 36
    printf("Stringified: %s\
", TO_STR(HelloC2026));
    int CONCAT(score, 1) = 98;
    // int score1 = 98;
    printf("Token pasted variable score1: %d\
", score1);
    return 0;
}

Key Takeaways, Best Practices & Memory Safety Rules

Best Practice & Key Takeaways
Wrap every macro parameter and the entire macro expression in parentheses ((x) * (y)) to prevent operator precedence bugs.

Frequently Asked Questions & Technical Interview Insights

Technical Interview & Viva Questions

Frequently asked C programming interview questions
Q

Q: Why can `SQUARE(x++)` cause bugs when `SQUARE` is a function-like macro?

A: Because `x++` is expanded twice: `((x++) * (x++))`, causing undefined behavior and incrementing `x` multiple times.
Q

Q: What standard macros are predefined by all ANSI C compilers?

A: `__FILE__` (source file name), `__LINE__` (current line number), `__DATE__` (compile date), and `__TIME__` (compile time).

Related Tools, Tutorials & Authoritative References

Recommended Interactive Tools

Authoritative Standards & Citations