Skip to content

Intermediate · The preprocessor, multi-file programs and make · topic 36 of 60 · 25 min

The preprocessor and macros

Explanation

Before compiling, the preprocessor handles lines that start with #: it pastes in #include files and replaces #define macros as plain text. Object-like macros name constants; function-like macros look like functions but are text substitution, so always put brackets around parameters and the whole body, and beware arguments with side effects. Prefer const variables and static inline functions where possible.

What the preprocessor does

The preprocessor is the first stage of compilation (module 1). It knows nothing about C types or syntax; it works on text. It removes comments, pastes in the contents of #include files, replaces macros, and keeps or drops code based on #if conditions. You can see its output with gcc -E file.c.

Object-like macros

c
#define GST_RATE 18
#define MAX_STUDENTS 60
#define APP_NAME "Fee Manager"

int marks[MAX_STUDENTS];               /* becomes: int marks[60]; */
printf("%s v1\n", APP_NAME);

By convention macro names are UPPER_CASE so readers know they are not variables. A macro has no type and no scope: it is replaced everywhere after its definition. A const variable (const double GST_RATE = 18.0;) or an enum constant is often better because it has a type and shows up in the debugger; macros remain common for array sizes in C and for conditional compilation.

Function-like macros and their traps

c
#define SQUARE_BAD(x) x * x
SQUARE_BAD(1 + 2)          /* becomes 1 + 2 * 1 + 2 = 5, not 9 */
10 / SQUARE_BAD(5)         /* becomes 10 / 5 * 5 = 10, not 0 */

#define SQUARE(x) ((x) * (x))     /* bracket every parameter AND the whole body */
SQUARE(1 + 2)              /* ((1 + 2) * (1 + 2)) = 9 */

int i = 3;
SQUARE(i++)                /* ((i++) * (i++)): i incremented twice → undefined behaviour */

#define MAX(a, b) ((a) > (b) ? (a) : (b))   /* the larger argument is evaluated twice */
Careful: Macro arguments are pasted as text, possibly several times, so arguments with side effects (i++, a function call that reads input, a slow calculation) misbehave. A static inline function evaluates each argument once, checks types and is just as fast with optimisation: static inline int square(int x) { return x * x; }.

Multi-statement macros

c
#define LOG_ERROR(msg) do { fprintf(stderr, "[%s:%d] %s\n", __FILE__, __LINE__, msg); } while (0)

if (failed) LOG_ERROR("could not open config");   /* works safely even without braces */
else puts("ok");
The do { … } while (0) wrapper makes the macro behave like a single statement.

# and ##

c
#define STR(x) #x                 /* "stringify": STR(price) → "price" */
#define FIELD(name) int field_##name /* token pasting: FIELD(qty) → int field_qty */

Predefined macros

c
printf("%s:%d in %s, built %s %s\n", __FILE__, __LINE__, __func__, __DATE__, __TIME__);
/* __func__ (C99) is the current function's name; __STDC_VERSION__ tells you the C standard in use */
Think of it like this: The preprocessor is like a mail-merge before printing letters: it replaces every {NAME} placeholder with text, blindly, without understanding the sentence. If a placeholder is replaced by "1 + 2" in the middle of a calculation, the sentence can change meaning, which is why brackets matter.
Tip: When a macro behaves strangely, run gcc -E on the file and look at the expanded line. The bug is usually obvious once you see the text the compiler actually received.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

The preprocessor and macros · 1/6

The preprocessor

In this lesson you will learn what the preprocessor does with lines that start with a hash.

Prefer a video by a teacher? Find free videos on “c preprocessor macros define pitfalls inline functions” on YouTube ↗

In real life: A logging macro with file and line

Embedded teams often use a logging macro that automatically records the file and line, so field reports from devices point to the exact source line without the developer typing it. A static inline helper would not know the caller's __LINE__, which is one place where macros are genuinely needed.

c
#define LOG(level, fmt, ...) \
    fprintf(stderr, "%s %s:%d: " fmt "\n", level, __FILE__, __LINE__, __VA_ARGS__)

LOG("WARN", "battery at %d%%", 12);   /* WARN meter.c:88: battery at 12% */
__VA_ARGS__ (C99) passes the remaining arguments through.

Practice (0/3 done)

Task 1 · Question

#define DOUBLE(x) x + x What is 3 * DOUBLE(4)?

Task 2 · Question

#define MAX(a, b) ((a) > (b) ? (a) : (b)) int i = 5, j = 3; int m = MAX(i++, j); What are m and i?

Task 3 · Practice

Write a header with: an object-like macro for a buffer size, a correctly bracketed MIN macro, the same logic as a static inline function, and a do-while(0) LOG macro using __FILE__ and __LINE__. Show with gcc -E what MIN(a + 1, b * 2) expands to, and demonstrate the double-evaluation problem with MIN(x++, y).

Quick quiz

Question 1

1.When does macro replacement happen?

Question 2

2.Why wrap multi-statement macros in do { … } while (0)?

Question 3

3.What does #x do in a macro body?

Question 4

4.Which is usually better than a function-like macro for simple calculations?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

When does macro replacement happen?

Answer: Before compilation, in the preprocessor

It is a text step.

Why wrap multi-statement macros in do { … } while (0)?

Answer: So the macro acts as one statement, even in if/else without braces

Avoids broken if/else.

What does #x do in a macro body?

Answer: Turns the argument into a string literal

Stringification.

Which is usually better than a function-like macro for simple calculations?

Answer: A static inline function

Type-checked and evaluates arguments once.

Which macro is predefined when compiling for Windows?Related

Answer: _WIN32

_WIN32 is defined for 32- and 64-bit Windows targets.

From Conditional compilation and include guards

Can unit tests call private functions of their module?Related

Answer: Yes, because the tests module is a child module

Child modules can see private items of their parent.

From Unit tests

What does a function return if it has no return statement?Related

Answer: undefined

Every JavaScript function without a return value gives undefined.

From Functions: declarations, expressions and return

Which include style is used for your own header file?Related

Answer: #include "myfile.h"

Quotes search your project folder first; angle brackets are for standard and system headers.

From Functions: declare, define, call and return

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 4 more topics in “The preprocessor, multi-file programs and make” before its exam: Conditional compilation and include guards, Splitting a program into files, Building with make, Libraries: static and shared.
  2. Module 8 exam: Preprocessor, multi-file programs and make · 12 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Bits, undefined behaviour and safety. 60 topics left in the course before the final exam.

← Previous: Binary files and records