Exercises — Macros vs inline functions
Two rules we will use over and over, so let us name them once:
Every trap below is just one of these two rules biting you.
Level 1 — Recognition
L1.1 — Who runs it?
For each line, say whether the preprocessor or the compiler is responsible.
#define PI 3.14159 // (a)
static inline int sq(int x){ return x*x; } // (b)
int y = sq(5); // (c) — who decides to inline?Recall Solution L1.1
- (a) Preprocessor.
#defineis a directive; it runs before real compilation and just remembers "replace the tokenPIwith the text3.14159." - (b) Compiler.
inlineis a real C keyword parsed and type-checked by the compiler. - (c) The compiler (its optimizer) decides whether to actually paste the body here.
inlineis only a hint.
L1.2 — Text or value?
#define DBL(x) (x)+(x). Someone writes DBL(3). What text does the preprocessor produce (not the number)?
Recall Solution L1.2
Pure text substitution: (3)+(3). The preprocessor never computes 6; it only rewrites text. The compiler later evaluates (3)+(3) = 6.
L1.3 — Which can take an address?
You want a pointer to reuse the "square" behaviour: int (*f)(int) = ???. Can this be a macro SQUARE? Can it be sq?
Recall Solution L1.3
Only the inline function sq has an address, so int (*f)(int) = sq; is legal. A macro is not an object — it vanishes after preprocessing — so &SQUARE is meaningless. This is one power inline functions have that macros never can.
Level 2 — Application
L2.1 — The precedence trap
#define SQ(x) x*x
int r = SQ(2 + 3);Hand-expand the text, then give the numeric value of r. What value did the author want?
Recall Solution L2.1
Blind paste: SQ(2 + 3) → 2 + 3*2 + 3. Now apply operator precedence — * binds tighter than +:
So r = 11. The author wanted . The missing parentheses let the outer + slice the argument apart.
L2.2 — Fix it
Rewrite SQ so SQ(2 + 3) gives 25, then re-expand to prove it.
Recall Solution L2.2
Parenthesize the whole body and every parameter:
#define SQ(x) ((x)*(x))Expand SQ(2 + 3) → ((2 + 3)*(2 + 3)) → . ✅ The inner parens keep 2+3 whole; the outer parens protect the result if the macro sits inside a bigger expression like 1 - SQ(2).
L2.3 — Outer-paren check
With the fixed #define SQ(x) ((x)*(x)), evaluate int r = 100 / SQ(2);. Then evaluate the same with a body that forgot the outer parens, #define BADSQ(x) (x)*(x).
Recall Solution L2.3
- Good:
100 / ((2)*(2))=100 / 4=25. - Bad (
BADSQ):100 / (2)*(2)→/and*are left-associative and equal precedence, so this is(100 / 2) * 2=50 * 2=100. The missing outer parens let the surrounding/steal into the macro. Answer:25vs100.
Level 3 — Analysis
L3.1 — Double evaluation, counted
#define SQ(x) ((x)*(x))
int i = 4;
int r = SQ(++i);Ignoring UB concerns for a moment and assuming left-to-right evaluation, how many times is i incremented, and what are the final i and r?
Recall Solution L3.1
Expand: SQ(++i) → ((++i)*(++i)). The text ++i appears twice, so i is incremented twice: 4 → 5 → 6, giving i = 6. Under naive left-to-right the product is 5 * 6 = 30.
Honest caveat: modifying i twice with no sequence point between is genuinely undefined behavior; a real compiler may print anything. The lesson: a macro duplicates the side effect. (The 30 here is just to expose the double increment, not a guaranteed value.)
L3.2 — Inline version, counted
static inline int sq(int x){ return x*x; }
int i = 4;
int r = sq(++i);Now how many times is i incremented, and what are the final i and r?
Recall Solution L3.2
By the evaluate-once rule, ++i is computed once into the parameter x before the body runs: i becomes 5, x holds 5. Body: x*x = 25. So i = 5, r = 25. Defined, predictable, type-checked. This is the headline reason to prefer inline functions.
L3.3 — The MAX re-evaluation
#define MAX(a,b) ((a) > (b) ? (a) : (b))
int p = 3, q = 5;
int m = MAX(p++, q++);Trace the expansion and, assuming left-to-right, give m, p, q.
Recall Solution L3.3
Expand: ((p++) > (q++) ? (p++) : (q++)).
- Compare: reads
p=3,q=5, then post-increments both → after comparep=4, q=6. Condition3 > 5is false. - So the
?:selects the third branch(q++): reads currentq=6as the result, then increments →q=7. Result:m = 6,p = 4,q = 7. The "winner"qwas evaluated twice — once in the compare, once when returned. (Formally UB again; shown to expose the double eval.)
Level 4 — Synthesis
L4.1 — The dangling-if bug
#define SWAP(a,b) int t=(a); (a)=(b); (b)=t;
if (cond) SWAP(x,y); else reset();Explain why this fails to compile / misbehaves, and rewrite SWAP correctly.
Recall Solution L4.1
Expand after the if:
if (cond) int t=(x); (x)=(y); (y)=t; else reset();The if body is only the first statement int t=(x);. The remaining two lines run unconditionally, and then a bare else appears with no matching if → compile error. Fix with the do{...}while(0) idiom, which packs many statements into one:
#define SWAP(a,b,T) do { T t=(a); (a)=(b); (b)=t; } while(0)
if (cond) SWAP(x,y,int); else reset(); // one statement, ; consumeddo{...}while(0) is a single statement that runs once, so the if/else pairing is preserved and the trailing ; closes it cleanly.
L4.2 — Choose the right tool
For each need, choose macro or inline function and justify in one line:
(a) MIN that works for int, double, and long in the same program;
(b) a helper is_even(n) used millions of times in a hot loop that must be debuggable;
(c) turning a token into a string literal, e.g. STR(hello) → "hello";
(d) a compile-time named constant BUFSIZE = 1024 you can also take a typed pointer to.
Recall Solution L4.2
- (a) Macro. C has no generics;
#define MIN(a,b) ((a)<(b)?(a):(b))is type-agnostic text. (Parenthesize!) - (b) Inline function. No call overhead when inlined, single evaluation, and you can set breakpoints. Debuggability rules out a macro.
- (c) Macro. Stringizing with
#xis a preprocessor-only power:#define STR(x) #x. No function can do this. - (d)
const/constexpr-style constant. Preferstatic const int BUFSIZE = 1024;(see const and constexpr) — it is typed and addressable; a#defineconstant has no address.
L4.3 — Rewrite a macro as a safe inline
Given #define CUBE(x) ((x)*(x)*(x)), write an equivalent int-typed inline function, and state one behaviour that changes for CUBE(i++).
Recall Solution L4.3
static inline int cube(int x){ return x*x*x; }With the macro, CUBE(i++) pastes i++ three times → i incremented three times (UB). With cube(i++), i is incremented once, x captures that single value, and x*x*x uses it three times safely. Given i = 2: cube(i++) returns 2*2*2 = 8 and leaves i = 3.
Level 5 — Mastery
L5.1 — Full trace with fixed macro and side effects
#define SQ(x) ((x)*(x))
int a = 2;
int b = SQ(a + 1);
int c = SQ(a++);Assuming left-to-right evaluation, give b, c, and final a. Mark which line is UB.
Recall Solution L5.1
b = SQ(a + 1)→((a + 1)*(a + 1))witha = 2→(3)*(3) = 9. No side effect, defined.b = 9.c = SQ(a++)→((a++)*(a++)).a++appears twice → naive result2 * 3 = 6,aends at4. This line is undefined behavior (two modifications ofawith no sequence point);c = 6,a = 4shown only to expose the double increment.
L5.2 — Predict the difference macro vs inline
Using the same inputs as L5.1 but with static inline int sq(int x){return x*x;}:
int a = 2;
int b = sq(a + 1);
int c = sq(a++);Give b, c, and final a, and state why every line is now well-defined.
Recall Solution L5.2
b = sq(a + 1): argumenta+1 = 3computed once →3*3 = 9.b = 9.c = sq(a++):a++evaluated once — parameterxgets2, thenabecomes3. Bodyx*x = 2*2 = 4.c = 4,a = 3.- Every line is defined because the evaluate-once rule gives each side effect a single, sequenced execution before the body runs. Compare with L5.1's UB.
L5.3 — The #define constant vs typed constant
You need SIZE = 10 used to declare int buf[SIZE]; and also passed to printf("%d", SIZE);.
(a) Show the macro version and what printf receives.
(b) Show the typed version and one advantage it gives.
Recall Solution L5.3
- (a)
#define SIZE 10.int buf[SIZE];→int buf[10];,printf("%d", SIZE);→printf("%d", 10);. Prints10. Purely textual, untyped. - (b)
static const int SIZE = 10;. Same array size (in C99+ this is a VLA-style or use anenum/macro for array bounds in strict C; the typed constant is fully addressable). Advantage: it has a type, an address (&SIZE), respects scope, and shows up in the debugger by name — none of which a#defineconstant offers.
L5.4 — Design challenge: safe generic MAX
The macro MAX(a,b) double-evaluates. Sketch (in words + code) a technique that keeps macro genericity but evaluates each argument once, using GNU statement expressions.
Recall Solution L5.4
Use a statement expression (a GCC/Clang extension) that stores each argument in a typed temporary using typeof, so each is evaluated once:
#define MAX(a,b) ({ __typeof__(a) _x=(a); __typeof__(b) _y=(b); \
_x > _y ? _x : _y; })_x=(a) and _y=(b) each evaluate their argument exactly once into a local, then the comparison uses the locals. MAX(p++, q++) now increments each of p, q once — the best of both worlds, at the cost of being non-portable (extension, not standard C). For portable code, prefer a typed inline function per type. Mnemonic: capture-once, compare-locals.
Score-yourself figure

The bar chart above shows, for the recurring SQ(a++) example with a = 2, how the macro and inline results diverge — the macro double-counts the increment (extra height in red), the inline stays clean (green).
Recall One-line summary of every level
Recognition (who runs it) → Application (parenthesize) → Analysis (count evaluations) → Synthesis (do{}while(0), right tool) → Mastery (typed constants + capture-once generics). Every trap = blind paste or duplicated side effect.
Connections
- C Preprocessor — the engine doing the blind text paste
- Operator Precedence in C — the L2 precedence traps
- Function Call Overhead — the cost both tools remove
- Undefined Behavior in C — the double-evaluation lines (L3, L5)
- const and constexpr — typed replacements for
#defineconstants - Compiler Optimization — why
inlineis only a hint