The Preprocessor & Macros
Before the compiler sees a single line of your C code, another program runs over it: the preprocessor. It handles every line that starts with #. It pastes header files into your code, replaces macro names with their text, and can leave out whole sections depending on settings you choose.
In this lesson you will see the preprocessor's output, write macros that do not break, turn debugging code on and off with a compiler option, use the predefined macros, and meet the preprocessor features added in C23.
What the preprocessor does
The preprocessor works on text, not on C meaning. It knows nothing about types or variables. You can see its output with gcc -E, which stops after preprocessing.
// area.c
#define PI 3.14159
#define CIRCLE_AREA(r) (PI * (r) * (r))
double garden = CIRCLE_AREA(7.0);gcc -E area.cAfter a few marker lines that record file names and line numbers, the output ends with:
double garden = (3.14159 * (7.0) * (7.0));
The macros have vanished, replaced by their text. This is what the compiler actually compiles. When a macro misbehaves, gcc -E is the quickest way to see why.
#include
#include pastes the entire contents of another file at that point. There are two forms:
#include <stdio.h>with angle brackets searches the system's standard header folders. Use it for the standard library and installed libraries.#include "marks.h"with quotes searches the current file's folder first, then the system folders. Use it for your own headers, as in Header Files & Multi-file Programs.
Object-like and function-like macros
An object-like macro replaces a name with text: #define MAX_STUDENTS 60. A function-like macro takes arguments: #define SQUARE(x) ((x) * (x)). There must be no space between the macro name and its opening bracket. Because the replacement is plain text, brackets are essential:
#include <stdio.h>
#define SQUARE_BAD(x) x * x
#define SQUARE(x) ((x) * (x))
int main(void)
{
printf("SQUARE_BAD(2 + 3) = %d\n", SQUARE_BAD(2 + 3));
printf("SQUARE(2 + 3) = %d\n", SQUARE(2 + 3));
printf("100 / SQUARE(5) = %d\n", 100 / SQUARE(5));
return 0;
}SQUARE_BAD(2 + 3) = 11 SQUARE(2 + 3) = 25 100 / SQUARE(5) = 4
SQUARE_BAD(2 + 3) becomes 2 + 3 * 2 + 3, which is 11. The rule: put brackets around every parameter and around the whole replacement. Even then, 100 / SQUARE_BAD(5) would become 100 / 5 * 5, which is 100, not 4.
Prefer functions when you can
Brackets cannot fix one problem: a macro argument may be evaluated more than once.
#include <stdio.h>
#define MAX(a, b) ((a) > (b) ? (a) : (b))
static inline int max_int(int a, int b)
{
return a > b ? a : b;
}
int main(void)
{
int x = 5, y = 3;
int m = MAX(x++, y); // x++ appears twice after expansion
printf("MAX: m = %d, x = %d\n", m, x);
x = 5;
m = max_int(x++, y); // x++ runs exactly once
printf("max_int: m = %d, x = %d\n", m, x);
return 0;
}MAX: m = 6, x = 7 max_int: m = 5, x = 6
The macro expanded to ((x++) > (y) ? (x++) : (y)), so x was incremented twice and the "maximum" was 6, a value that was never passed in. A static inline function evaluates each argument once, checks types, and shows up in the debugger. Modern compilers make small functions just as fast as macros, so use functions for calculations and constexpr for constants, and keep macros for jobs only the preprocessor can do, like the ones below.
Stringising and token pasting
Inside a macro, #param turns an argument into a string literal, and a##b glues two pieces of text into one name.
#include <stdio.h>
#define SHOW(expr) printf(#expr " = %d\n", (expr))
#define MAKE_GETTER(field) int get_##field(void) { return field; }
int price = 1299;
int stock = 42;
MAKE_GETTER(price)
MAKE_GETTER(stock)
int main(void)
{
SHOW(7 * 6);
SHOW(price / 100);
printf("get_stock() = %d\n", get_stock());
return 0;
}7 * 6 = 42 price / 100 = 12 get_stock() = 42
SHOW(7 * 6) becomes printf("7 * 6" " = %d\n", (7 * 6)), and the two adjacent strings join. This is handy for debugging output. MAKE_GETTER(stock) builds a function named get_stock. Use ## sparingly: generated names are hard to search for.
Conditional compilation
#if, #ifdef, #ifndef, #elif, #else and #endif keep or remove code before compilation. A common use is debug logging that costs nothing in the final program. The option -DNAME defines a macro from the command line.
#include <stdio.h>
#ifdef DEBUG
#define LOG(fmt, ...) fprintf(stderr, "[%s:%d] " fmt "\n", __FILE__, __LINE__, __VA_ARGS__)
#else
#define LOG(fmt, ...) ((void) 0)
#endif
int main(void)
{
int total = 0;
for (int i = 1; i <= 3; i++) {
total += i * 10;
LOG("i = %d, total = %d", i, total);
}
printf("Total: %d\n", total);
return 0;
}gcc -std=c23 -Wall -Wextra -DDEBUG log.c -o log
./log[log.c:14] i = 1, total = 10 [log.c:14] i = 2, total = 30 [log.c:14] i = 3, total = 60 Total: 60
Built without -DDEBUG, the same program prints only Total: 60, because every LOG becomes ((void) 0), which does nothing. ... and __VA_ARGS__ make a variadic macro that accepts any number of extra arguments. __FILE__ and __LINE__ are filled in where the macro is used, so each message says exactly where it came from.
Predefined macros and the standard version
The compiler predefines several macros. __FILE__ is the current file name, __LINE__ the current line, and __DATE__ and __TIME__ the moment of compilation. __func__ is not a macro but a predefined variable holding the current function's name. __STDC_VERSION__ tells you which C standard is being used:
#include <stdio.h>
int main(void)
{
#if __STDC_VERSION__ >= 202311L
puts("Compiled as C23 or newer");
#elif __STDC_VERSION__ >= 201710L
puts("Compiled as C17");
#else
puts("Compiled as an older standard");
#endif
printf("__STDC_VERSION__ = %ld\n", __STDC_VERSION__);
printf("This is line %d of %s, in function %s\n", __LINE__, __FILE__, __func__);
return 0;
}Compiled as version.c with GCC 15 and -std=c23:
Compiled as C23 or newer __STDC_VERSION__ = 202311 This is line 13 of version.c, in function main
The values are 201112L for C11, 201710L for C17 and 202311L for C23. GCC 13 and 14 used a provisional value, 202000L, in C23 mode, so on those versions this program prints "Compiled as C17" even with -std=c23.
Common mistakes
- Missing brackets in a macro. Bracket every parameter and the whole replacement.
- A semicolon at the end of a
#define. It is pasted everywhere the macro is used. - A space before the bracket.
#define SQUARE (x) ...defines an object-like macro whose text starts with(x). - Arguments with side effects.
MAX(i++, j)may incrementitwice. Use an inline function. - Multi-statement macros. Two statements in a macro break inside an
ifwithout braces. Wrap them asdo { ... } while (0), or better, write a function.
Key points
- The preprocessor handles
#lines before compilation, working on plain text;gcc -Eshows its output. #include <...>is for system headers and#include "..."for your own.- Bracket every macro parameter and the whole replacement; prefer inline functions and
constexprwhere possible. #turns a macro argument into a string and##pastes tokens together.#ifdefwith-DNAMEswitches code such as debug logging on and off at compile time.__FILE__,__LINE__,__func__and__STDC_VERSION__describe the code being compiled; C23 adds#embed,#elifdefand#warning.
Exercise
Write a macro ARRAY_LEN(a) that gives the number of elements in an array, and a macro CHECK(cond) that prints Check failed: followed by the condition's text and the line number when the condition is false. Use them on an array of five fees to print its length and to check that the first fee is below 1000 and the last is below 500.
Show solution
#include <stdio.h>
#define ARRAY_LEN(a) (sizeof (a) / sizeof (a)[0])
#define CHECK(cond) \
do { \
if (!(cond)) { \
printf("Check failed: %s (line %d)\n", #cond, __LINE__); \
} \
} while (0)
int main(void)
{
int fees[] = {750, 1200, 300, 450, 600};
printf("Number of fees: %zu\n", ARRAY_LEN(fees));
CHECK(fees[0] < 1000);
CHECK(fees[4] < 500);
return 0;
}Number of fees: 5 Check failed: fees[4] < 500 (line 17)
A backslash at the end of a line continues the macro on the next line. #cond turns the condition into text, and do { ... } while (0) makes the macro behave like a single statement, so it is safe even inside an if without braces. __LINE__ gives the line where CHECK was used.
Quick Quiz
1. What does gcc -E file.c show?
-E stops after the preprocessor, so you see exactly what the compiler will compile.
2. With #define DOUBLE(x) x + x, what is DOUBLE(3) * 2?
The expansion is 3 + 3 * 2, which is 9; brackets around the replacement would give 12.
3. What does #expr do inside a macro?
The # operator stringises the argument, so SHOW(a + b) can print the text "a + b".
4. What value does __STDC_VERSION__ have for C23 in GCC 15?
C23 defines __STDC_VERSION__ as 202311L; older GCC versions used a provisional value.
Learn C with a mentor
Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
