Macros in C Explained: Expansion, Worked Output Traces and Exam Traps

Learn a reliable expand-first method for C macros. Follow exact traces for nested macros, unsafe SQUARE calls, repeated evaluation, token operators and header guards.

KnowledgeGate Team

Exam prep & CS education

Updated 12 Sep 20266 min read

A macro call can look like a function call, but it behaves differently: the preprocessor substitutes tokens before C expression rules are applied. That is why a harmless-looking SQUARE(3 + 2) can produce 11 instead of 25, and why an increment passed to a macro can run twice. Macro analysis starts with token expansion, followed by grouping and sequencing, and ends with calculation. Object-like and function-like macros, #, ##, conditional compilation, header visibility and exam-style traps all arise from preprocessing behavior.

Macros in C: what the preprocessor changes before compilation

A macro is a named preprocessing replacement created with #define. An object-like macro has no parameter list:

#define CAPACITY 8
int slots[CAPACITY];

The first substitution makes the declaration int slots[8];. A function-like macro accepts argument tokens:

#define DOUBLE(x) (2 * (x))

Neither kind creates a runtime object, stack frame or parameter variable. Macro invocations expand before the resulting C tokens are compiled. The compiler type-checks only the C tokens left after expansion. A macro parameter has no type of its own and is not type-checked at the invocation.

For ordinary parameter uses, expand nested macros in each argument, substitute those tokens, rescan the replacement, then apply C grouping, sequencing and evaluation. The # and ## operators follow special rules.

Object-like, function-like and nested macros

Consider three definitions:

#define BASE 4
#define STEP (BASE + 1)
#define TWICE(x) (2 * (x))
int value = TWICE(STEP);

Because x is not used with # or ##, STEP expands before substitution:

Preprocessing:
STEP -> (BASE + 1) -> (4 + 1)
TWICE(STEP) -> (2 * ((4 + 1)))
C evaluation:
(2 * ((4 + 1))) -> (2 * 5) -> 10

This separates preprocessing from C evaluation. STEP is replaced until the argument is fully expanded; only the compiler evaluates 4 + 1.

Compare this with static inline int twice(int x) { return 2 * x; }. The function has an int parameter and evaluates its argument once for that parameter. A macro may use an argument zero, one or several times. The compiler decides whether either form becomes a call, so speed is not an inherent macro advantage.

Macro parentheses trap: why SQUARE(3 + 2) becomes 11

Start with an unsafe definition:

#define SQUARE(x) x * x

SQUARE(3 + 2) expands to 3 + 2 * 3 + 2. Multiplication comes first: 2 * 3 = 6, followed by 3 + 6 + 2 = 11. The macro did not square 5; it inserted argument tokens without grouping.

Correct it as #define SQUARE(x) ((x) * (x)). The call now expands to ((3 + 2) * (3 + 2)). Each grouped sum is 5, so the result is 5 * 5 = 25. Parenthesise every parameter use and the entire replacement expression.

Also, with the unsafe definition, 100 / SQUARE(5) becomes 100 / 5 * 5. Division and multiplication associate left to right, so (100 / 5) * 5 = 20 * 5 = 100. With the corrected macro, 100 / ((5) * (5)) = 100 / 25 = 4.

Expansion trace of SQUARE(3 + 2): the unsafe x * x form gives 11, while the parenthesised form gives 25.

Macro side effects: a complete MAX(i++, j++) trace

Parentheses do not prevent repeated evaluation:

#define MAX(a, b) ((a) > (b) ? (a) : (b))
int i = 4, j = 7;
int m = MAX(i++, j++);

Expand completely before calculating:

int m = ((i++) > (j++) ? (i++) : (j++));

The comparison reads the old values, so 4 > 7 is false. Those postfix operations leave i = 5 and j = 8. Only the false branch is selected. Its j++ yields the old value 8 for m, then changes j to 9. The final state is m = 8, i = 5, j = 9.

Both argument spellings occur twice, and the selected one is evaluated again. Better parentheses cannot make this macro single-evaluation. Pass side-effect-free expressions, or first store i++ and j++ in separate temporary values and compare those.

State trace of m = MAX(i++, j++) with i = 4, j = 7: the false comparison ends at m = 8, i = 5, j = 9.

Stringification, token pasting and conditional compilation

The # and ## operators have preprocessing roles:

#define TEXT(x) #x
#define JOIN(a, b) a##b
int mark_2 = 84;
printf("%s %d", TEXT(GATE CS), JOIN(mark_, 2));

TEXT(GATE CS) becomes the string literal "GATE CS". JOIN(mark_, 2) pastes the tokens into the identifier mark_2. Output: GATE CS 84. Operands next to # or ## are not macro-expanded first. These are not ordinary runtime C operators.

Conditional compilation selects source during preprocessing:

#define DEBUG 1
#if DEBUG
puts("trace");
#endif

Here DEBUG becomes 1, so the guarded line remains and prints trace when executed. If DEBUG is defined as 0, the line is excluded from that translation. This is build-time selection, not a runtime if.

Macro visibility, headers and statement-macro traps

A macro name means whatever its current definition says at the line where it is used. With #define SIZE 8, int first[SIZE]; becomes int first[8];. After #undef SIZE and #define SIZE 16, int second[SIZE]; becomes int second[16];. Braces do not create macro scope. A definition lasts until #undef or the end of that preprocessing translation unit.

A header guard uses that visibility within one translation unit:

#ifndef MATRIX_H
#define MATRIX_H
#define ROWS 2
void clear_matrix(int matrix[ROWS][ROWS]);
#endif

On a second inclusion, MATRIX_H is defined, so the body is skipped. Separately compiled source files have separate preprocessing translation units.

The same few mistakes repeatedly break macro expansion, and each has a direct fix worth recognizing.

Mistake

What expansion does

Fix

Missing parameter or whole-expression parentheses

Changes grouping around substituted tokens

Write ((x) * (x))

Trailing semicolon in #define INC(x) ((x) + 1);

Can insert a statement-ending ; inside a larger expression

Leave the semicolon to the caller

Several bare statements after an if

Only the first statement is controlled reliably

Use a do { ... } while (0) wrapper

Side-effecting argument such as i++

Repeats the side effect when the parameter is repeated

Pass a temporary or use a function

For a statement-like macro, the standard wrapper keeps it single:

#define SWAP_INT(a, b) do { int temp = (a); (a) = (b); (b) = temp; } while (0)
int x = 2, y = 9;
SWAP_INT(x, y);

The result is x = 9, y = 2. The wrapper fixes statement structure, but arguments must still be suitable assignable expressions without troublesome side effects.

Macro exam questions: expand first, then classify the trap

Under exam pressure, it is easy to compute a macro mentally and miss the trap, or to get the value without knowing what the question is testing. Never evaluate a macro in your head. Expand it fully in writing first, then read the result for missing parentheses, repeated argument evaluation, stringify or paste behavior, or conditional compilation.

Use a five-pass answer method:

  1. Write the macro definition exactly as given.

  2. Expand nested macro names in ordinary arguments before substitution.

  3. Substitute, rescan the replacement, and handle # or ## by their special rules.

  4. Mark grouping and sequencing in the resulting C expression.

  5. Compute the value, or reject an unsafe or non-portable shortcut.

Apply the five passes to the unsafe SQUARE(3 + 2). Substitution produces 3 + 2 * 3 + 2, which evaluates to 11. Pass 4 exposes the missing parentheses, while the safe form produces ((3 + 2) * (3 + 2)) and gives 25. Next, run the same five passes on MAX(i++, j++) with i = 4 and j = 7. The trace finishes at m = 8, i = 5 and j = 9, making the repeated-evaluation trap visible. For the wider route, continue with GATE CS Exam Preparation.

Macros in C: the short version and next step

Remember five rules: macros replace tokens before compilation; nested names are rescanned; each parameter and the whole expression need parentheses; repeated arguments must not receive side effects; and #, ##, conditional compilation and header guards are preprocessing tools. The numerical anchors are TWICE(STEP) = 10, corrected SQUARE(3 + 2) = 25 instead of 11, and m = 8, i = 5, j = 9 after the side-effect trace. Continue with the C Language Course for structured C concepts and practice, or GATE Guidance by Sanchit Sir for the wider GATE CS sequence.