Macros
A function-like macro takes arguments and pastes them into its body before the compiler sees them. It has no type checking because there is no function: the arguments are text. The parenthesis and the placement of every parameter decide whether the macro behaves like a function or quietly changes the meaning of an expression.
Function-like macros
The body of MAX parenthesises each parameter and the whole expression. Without the outer pair, a caller's MAX(a, b) * 2 would multiply only one branch. With them the macro behaves like a function call for every expression that has no side effect.
#include <stdio.h>
#define MAX(a, b) (((a) > (b)) ? (a) : (b))
int main(void)
{
printf("%d\n", MAX(3, 7));
printf("%d\n", MAX(-2, -9));
printf("%d\n", MAX(2 + 3, 4 * 1));
return 0;
}
gcc -Wall -Wextra max.c -o max
./max
7
-2
5
MAX(-2, -9) is -2 because the comparison is between the two values, not between their absolute sizes. The third call shows the parentheses working: the addition and the multiplication stay inside their parameters.
Multi-statement macros
A macro that expands to several statements must be a single statement from the caller's point of view, or an if without braces will attach to the wrong part. The do { } while (0) wrapper is the standard answer: the semicolon after the call completes the loop, and the block runs exactly once.
#include <stdio.h>
#define LOG(value) do { \
printf("value: %d\n", (value)); \
} while (0)
int main(void)
{
int flag = 1;
if (flag)
LOG(10);
else
LOG(20);
return 0;
}
gcc -Wall -Wextra dowhile.c -o dowhile
./dowhile
value: 10
The else belongs to the if because the expansion is one statement. A plain { ... } wrapper would compile but leaves a semicolon after the block, which is a trap in an if/else chain.
Stringify and paste
Two operators work only inside macros. A single # before a parameter turns its text into a string literal, and ## joins two tokens into one. Both happen before compilation, so the result can be a name the source never spells out.
#include <stdio.h>
#define STRINGIFY(x) #x
#define CONCAT(a, b) a##b
int main(void)
{
int CONCAT(total, 1) = 42;
printf("%s\n", STRINGIFY(hello));
printf("%s\n", STRINGIFY(2 + 3));
printf("total1: %d\n", total1);
return 0;
}
gcc -Wall -Wextra stringify.c -o stringify
./stringify
hello
2 + 3
total1: 42
The stringification does not evaluate the argument: it keeps the spaces and the signs exactly as written, which is why STRINGIFY(2 + 3) prints the expression. Token pasting happens before the name is looked up, so total1 exists as a normal variable in the line after the macro.
Variadic macros
... in the parameter list collects the remaining arguments in __VA_ARGS__. When a call passes none, the comma before the group would remain, so C23 added __VA_OPT__(,), which expands to a comma only when arguments are present.
#include <stdio.h>
#define LOGF(format, ...) printf("[log] " format "\n" __VA_OPT__(,) __VA_ARGS__)
int main(void)
{
LOGF("started");
LOGF("value %d at %s", 7, "noon");
return 0;
}
gcc -Wall -Wextra variadic.c -o variadic
./variadic
[log] started
[log] value 7 at noon
The format is concatenated with the prefix at compile time, so the compiler still checks it: a wrong conversion or argument type in LOGF produces the same warning as in printf. That is the advantage over a hand-written variadic function, which the next lesson covers.
Double evaluation and missing parentheses
A macro argument is pasted as text, so an argument with a side effect runs once per use. MAX(i++, 0) increments i twice, and the result depends on the comparison branch:
#include <stdio.h>
#define MAX(a, b) (((a) > (b)) ? (a) : (b))
#define DOUBLE(x) x * 2
int main(void)
{
int i = 1;
int big = MAX(i++, 0);
printf("big: %d, i: %d\n", big, i);
printf("double: %d\n", DOUBLE(1 + 2));
return 0;
}
gcc -Wall -Wextra sideeffect.c -o sideeffect
./sideeffect
big: 2, i: 3
double: 5
The first line shows two increments, the second shows the missing parentheses: DOUBLE(1 + 2) becomes 1 + 2 * 2, which is 5. A function does neither, which is why macros are for constants, logging and generic code, not for arithmetic with side effects.
Exercises
- Write a MIN3 macro for three values and test it with negative numbers and expressions.
- Write a CHECK(condition) macro that prints the file and line when the condition is false, using __FILE__ and __LINE__.
- Add a prefix parameter to LOGF so the same macro can log with two different tags, then call it with and without extra arguments.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
Why does
MAXparenthesise every parameter and the whole body?Answer
So a surrounding operator cannot split the expression
Without the parentheses,
MAX(a, b) * 2would attach the multiplication to one branch of the conditional. -
Why does a multi-statement macro use
do { } while (0)?Answer
It makes the expansion a single statement
The wrapper lets the trailing semicolon complete one statement, so an
ifwithout braces cannot capture the wrong part. -
What does the
#operator do inside a macro?Answer
It turns a parameter into a string literal
Stringification keeps the argument text exactly as written, including spaces and operators.
-
What does
__VA_OPT__(,)provide in a variadic macro?Answer
A comma only when extra arguments exist
C23 uses
__VA_OPT__to avoid a stray comma when the call passes no variadic arguments. -
What does this program print?
#include <stdio.h> #define MAX(a,b) (((a)>(b))?(a):(b)) int main(void){ printf("%d %d %d\n", MAX(3,7), MAX(-2,-9), MAX(2+3,4*1)); return 0; }Answer
7 -2 5
The macro compares the actual values, so -2 beats -9, and the parentheses keep the expressions grouped.
-
What does this program print?
#include <stdio.h> #define LOG(v) do { printf("value: %d\n", (v)); } while (0) int main(void){ int flag=1; if(flag) LOG(10); else LOG(20); return 0; }Answer
value: 10
The
do-whileexpansion is one statement, so theelsestays with theifand only the true branch runs. -
What does
STRINGIFY(2 + 3)produce?Answer
The text
2 + 3, not the value 5Stringification does not evaluate the argument; it keeps the tokens and spaces exactly as written.
-
What does this program print?
#include <stdio.h> #define MAX(a,b) (((a)>(b))?(a):(b)) #define DOUBLE(x) x*2 int main(void){ int i=1; int big=MAX(i++,0); printf("big: %d, i: %d\n", big, i); printf("double: %d\n", DOUBLE(1+2)); return 0; }Answer
big: 2, i: 3 / double: 5
MAX(i++,0)evaluatesi++twice, andDOUBLE(1+2)expands to1 + 2*2, which is 5. -
What is the danger of
MAX(i++, 0)?Answer
The argument with a side effect runs twice
A macro pastes its argument as text, so an increment appears in both branches of the conditional and runs twice.
-
What goes wrong when a multi-statement macro is a plain block in an
if?Answer
The semicolon ends the if and the else breaks
A
{ ... }expansion followed by a semicolon leaves theelseunattached, which is why thedo-whilewrapper exists.
The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev