#define MAX 100
#define SQUARE(x)((x)*(x))Macros in C: Object-Like, Function-Like, and Other Macro Forms
What is Macros in C? Core Concept & Direct Summary
What is the difference between object-like macros, function-like macros, stringification (`#`), and token-pasting (`##`) operators?
A macro is a name for a piece of text, and using it replaces that name with that text before the compiler runs.
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.
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.
Used anywhere afterward, the name is replaced:
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:
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:
To deliberately replace a macro, #undef it first:
#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?
The guidance:
- Use
enumfor related integer constants — flags, states, error codes, sizes. You get names, scope, type checking, and debugger visibility. - Use
constfor 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:
The space that changes everything
The ( must be adjacent — no space, no tab, no comment:
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:
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:
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:
a appears twice in the body, so i++ is potentially evaluated twice — and b likewise. Verified, with i = 1, j = 2:
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:
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", becauseXSTRusesxplainly, so the argument is expanded to3before being handed toSTR.
That's the whole reason the two-level XSTR idiom exists — and it has a practical consequence beyond stringizing:
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:
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:
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:
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:
## 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:
__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:
The empty-argument problem
In C99 and C11, if __VA_ARGS__ is empty you get a dangling comma. Verified:
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:
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:
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.
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.
Verified binding:
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:
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:
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:
_Genericis 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 intargument won't selectint; add explicit entries or adefault: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.
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
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
sizeofto 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.
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
- Missing parentheses around parameters —
SQUARE(2+3)gives 11 instead of 25 (verified). Wrap every parameter. - Missing outer parentheses on the body —
2 * ADDPAIR(3)gives 9 instead of 12 (verified). Wrap the whole body. - Passing side-effecting arguments —
MAX(i++, j++)incrementedjtwice and produced 3 increments total (verified). Undefined behavior, invisible at the call site. - A space between the name and
(—#define F (x)is an object-like macro, not a function-like one. Completely different meaning. - Multi-statement macros without
do { } while (0)— breaks when used in a brace-lessif, because the caller's;becomes a null statement and orphans a followingelse. - Expecting
#to stringize an expanded value —STR(VERSION)gives"VERSION", not"3"(verified). Use the two-levelXSTR. - Using a one-level
##when you need expansion —CAT(a, __LINE__)pastesa__LINE__literally (verified). Use the two-levelCAT. - Pasting tokens that don't form one valid token —
CAT(+, -)is undefined behavior;##fuses tokens, it doesn't concatenate text. - Using
##at the start or end of a body — an error; there must be tokens on both sides. - 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. #__VA_ARGS__in C99/C11 — ill-formed; stringizing a variadic tail needs a helper.- Commas inside macro arguments —
MAX(a ? b, c : d, e)is read as 3 arguments (verified) and errors. Parenthesize the argument. - Assuming
ARRAY_LENworks on a pointer — it silently returns the pointer size divided by the element size. Only correct for a real array in the same scope. - Relying on
#definefor scoping — a#defineinside a function affects the rest of the file.#undefwhen you need to end it. - Redefining a macro differently — undefined behavior/diagnostic.
#undeffirst if the redefinition is intentional. - Assuming a macro name expands inside a string literal — it doesn't; quoted strings are opaque tokens.
- Using
#if VERSION > 2with a typo'd name — undefined identifiers evaluate to0silently. Enable-Wundef. - 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). - Reaching for a macro when a function would do — no type checking, double evaluation, and no debuggability are all avoided by
static inlineor a plain function. - Nesting seven layers of argument-count dispatch — technically impressive, practically undebuggable. Use a function or
_Generic. - Forgetting
#undefafter a temporary macro — especially in X-macro patterns where a generic name likeXis reused. - 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
XSTRworks andSTRdoesn't #and##suppress that pre-expansion — which is why the two-layerSTRINGIFYandCATexist- 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
#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
((x) * (y)) to prevent operator precedence bugs.Frequently Asked Questions & Technical Interview Insights
Technical Interview & Viva Questions
Frequently asked C programming interview questionsQ: Why can `SQUARE(x++)` cause bugs when `SQUARE` is a function-like macro?
Q: What standard macros are predefined by all ANSI C compilers?
Related Tools, Tutorials & Authoritative References
Recommended Interactive Tools
- Online C Compiler & Runner — Interactive online GCC sandbox to execute C code in real time.
- Online C Code Editor & IDE — Multi-file C programming environment with code formatting.
- C++ Programming Tutorial — Transition from procedural C to Object-Oriented C++ with STL.
- JavaScript Programming Tutorial — Master modern JavaScript fundamentals, DOM APIs, and async patterns.
Authoritative Standards & Citations
- ISO/IEC 9899 (C Language Standard) — Official international C specification by ISO/IEC JTC 1/SC 22/WG 14.
- GNU GCC Documentation — Official reference manual for the GNU Compiler Collection and C libraries.
- The C Programming Language (K&R) — Definitive classic work by Brian Kernighan & Dennis Ritchie at Bell Labs.
- C Reference Documentation (cppreference.com) — Verified reference covering C89 through C23 language features.