Macros
In C++, you have constexpr for compile-time constants, templates for generic code, and inline functions to avoid call overhead. C has no templates, its constexpr only arrived in C23 (in a limited form), and inline (added in C99) is little more than a hint. Instead, C leans heavily on the preprocessor — the #define macro system that rewrites your source code before the compiler sees it.
Macros are pure textual substitution. The preprocessor does not know about types, scope, or expressions — it just replaces text. This makes macros powerful and flexible, but also a source of subtle bugs if you are not careful.
Object-Like Macros
The simplest macros define named constants:
#define MAX_BUF 1024
#define PI 3.14159265
#define GREETING "Hola, amigo"Everywhere the preprocessor sees MAX_BUF, it replaces it with 1024. No semicolons — a common mistake is writing #define MAX_BUF 1024;, which would paste 1024; everywhere, breaking expressions like malloc(MAX_BUF * sizeof(int)).
Trap: Do not put a semicolon at the end of a #define. The semicolon becomes part of the replacement text and will cause surprising errors.
Conditional Compilation
Macros also control which code the compiler sees:
#define DEBUG
#ifdef DEBUG
printf("x = %d\n", x);
#endif #ifdef checks whether a macro is defined (regardless of its value). Its complement #ifndef checks that a macro is not defined. You can also use #if, #elif, and #else for more complex conditions:
#if VERBOSE_LEVEL >= 2
printf("Detailed trace...\n");
#elif VERBOSE_LEVEL == 1
printf("Basic trace...\n");
#else
/* no tracing */
#endifInclude Guards
The most common use of #ifndef is protecting header files from being included more than once:
/* myheader.h */
#ifndef MYHEADER_H
#define MYHEADER_H
struct point {
int x, y;
};
void draw_point(struct point p);
#endif /* MYHEADER_H */The first time myheader.h is included, MYHEADER_H is not defined, so the contents are processed and MYHEADER_H gets defined. Any subsequent include finds MYHEADER_H already defined and skips the entire file.
Tip: Many compilers support #pragma once as a non-standard alternative to include guards. It is simpler to write but not portable to all compilers. When in doubt, use the #ifndef guard — it works everywhere.
Function-Like Macros
Macros can take parameters, making them look like functions:
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define ABS(x) ((x) < 0 ? -(x) : (x))But they are not functions — they are text substitution with parameter placeholders. This distinction matters.
The Parenthesization Rules
Always parenthesize every parameter use and the entire macro body:
/* Wrong: */
#define SQUARE(x) x * x
/* SQUARE(1 + 2) expands to: 1 + 2 * 1 + 2 = 5 (not 9!) */
/* Right: */
#define SQUARE(x) ((x) * (x))
/* SQUARE(1 + 2) expands to: ((1 + 2) * (1 + 2)) = 9 */Without parentheses, operator precedence in the surrounding expression can silently rearrange the computation.
The Double-Evaluation Trap
Since macros substitute text, each parameter reference evaluates the argument again:
#define SQUARE(x) ((x) * (x))
int i = 3;
int result = SQUARE(i++);
/* Expands to: ((i++) * (i++)) — i is incremented TWICE */
/* Undefined behavior: two unsequenced modifications of i */A real function evaluates its argument once. A macro evaluates it once per appearance in the replacement text. This is the most important difference between macros and functions.
Trap: Never pass expressions with side effects (like i++, f(), or assignment) to function-like macros. The expression will be evaluated multiple times, producing unexpected results or undefined behavior.
Multi-Statement Macros: do { ... } while (0)
If a macro needs to execute multiple statements, wrap them in do { ... } while (0):
#define SWAP(a, b) do { \
int tmp = (a); \
(a) = (b); \
(b) = tmp; \
} while (0)Why not just use braces? Consider:
if (x > y)
SWAP(x, y);
else
printf("Already sorted\n");If SWAP expanded to a bare { ... }, the semicolon after SWAP(x, y) would terminate the if statement, and the else would become a syntax error. The do { ... } while (0) idiom creates a single statement that works correctly with semicolons and control flow.
Tip: The do { ... } while (0) pattern is everywhere in C codebases. It looks odd at first, but it is the standard way to make multi-statement macros behave like ordinary statements.
Stringification and Token Pasting
The preprocessor has two special operators for macro arguments.
Stringification: #
The # operator turns a macro argument into a string literal:
#define PRINT_VAR(x) printf(#x " = %d\n", x)
int score = 42;
PRINT_VAR(score);
/* Expands to: printf("score" " = %d\n", score); */
/* Adjacent string literals are concatenated: "score = %d\n" */
/* Output: score = 42 */This is useful for debug macros where you want to print both the variable name and its value.
Token Pasting: ##
The ## operator joins two tokens into one:
#define DECLARE_PAIR(type) \
type type##_first; \
type type##_second;
DECLARE_PAIR(int)
/* Expands to:
int int_first;
int int_second;
*/Token pasting is commonly used to generate families of related variables or functions from a single macro.
Variadic Macros
Macros can accept a variable number of arguments using ... and __VA_ARGS__:
#define LOG(fmt, ...) fprintf(stderr, "[LOG] " fmt "\n", __VA_ARGS__)
LOG("score is %d", 42);
/* Expands to: fprintf(stderr, "[LOG] " "score is %d" "\n", 42); */This is commonly used to wrap printf-style functions with extra decoration like timestamps or log levels.
Tip: When __VA_ARGS__ is empty, the trailing comma before it can cause a compilation error. GNU C provides ##__VA_ARGS__, which swallows the comma when the argument list is empty:
#define LOG(fmt, ...) fprintf(stderr, "[LOG] " fmt "\n", ##__VA_ARGS__)
LOG("started"); /* No extra args — comma is removed */This is a GCC/Clang extension — ##__VA_ARGS__ itself was never standardized. C23 solves the same problem portably with __VA_OPT__ (e.g. fmt "\n" __VA_OPT__(,) __VA_ARGS__) and also allows passing zero variadic arguments.
Multi-Level Expansion
Macros can expand to other macros, and the preprocessor rescans the result to expand again. But the # and ## operators are special — they operate on the raw argument text before any expansion happens.
#define MAX_BUF 1024
#define STRINGIFY(x) #x
#define XSTRINGIFY(x) STRINGIFY(x)
printf("%s\n", STRINGIFY(MAX_BUF));
/* # operates before expansion: prints "MAX_BUF" */
printf("%s\n", XSTRINGIFY(MAX_BUF));
/* First pass: XSTRINGIFY(MAX_BUF) → STRINGIFY(1024) */
/* Rescan: STRINGIFY(1024) → "1024" */
/* Prints "1024" */STRINGIFY(MAX_BUF) gives "MAX_BUF" because # stringifies its argument before expansion. XSTRINGIFY(MAX_BUF) first expands MAX_BUF to 1024 (since the outer macro does not use # directly), then passes 1024 to STRINGIFY, producing "1024".
This two-level indirect pattern is used whenever you need the expanded value of a macro as a string.
Tip: Whenever you need a macro’s expanded value as a string, use the two-level indirect pattern. It comes up often when embedding version numbers or configuration values in strings.
X-Macros
X-macros are a technique for defining a list of items once and expanding it in multiple ways. The idea: define the list as a macro that calls an unspecified “action” macro on each item, then define that action differently for each use.
Here is a concrete example that generates both an enum and a string table from a single list of log levels:
#include <stdio.h>
/* Define the list once */
#define LOG_LEVELS(X) \
X(LOG_DEBUG) \
X(LOG_INFO) \
X(LOG_WARN) \
X(LOG_ERROR) \
X(LOG_FATAL)
/* Generate the enum */
#define AS_ENUM(name) name,
enum log_level { LOG_LEVELS(AS_ENUM) LOG_COUNT };
/* Generate the string table */
#define AS_STRING(name) #name,
const char *log_level_names[] = { LOG_LEVELS(AS_STRING) };
int main(void) {
for (int i = 0; i < LOG_COUNT; i++) {
printf("%d = %s\n", i, log_level_names[i]);
}
return 0;
}Output:
0 = LOG_DEBUG
1 = LOG_INFO
2 = LOG_WARN
3 = LOG_ERROR
4 = LOG_FATALAdd a new log level? Add one line to LOG_LEVELS and the enum and string table stay in sync automatically. Without X-macros, you would need to update both the enum and the string array separately — and hope you never forget one.
Tip: X-macros are one of the preprocessor’s most powerful patterns. You will see them in real codebases for error codes, command tables, and state machines. The key advantage: a single source of truth for a list of items.
Try It: Macro Starter
#include <stdio.h>
// Object-like macros
#define MAX_TRACKS 10
#define LABEL "Sire Records"
// Function-like macro with proper parenthesization
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
// Stringification: print variable name and value
#define PRINT_INT(var) printf(#var " = %d\n", var)
// Multi-statement macro using do { ... } while (0)
#define SWAP(a, b) do { \
int tmp = (a); \
(a) = (b); \
(b) = tmp; \
} while (0)
// Variadic macro
#define LOG(fmt, ...) fprintf(stderr, "[LOG] " fmt "\n", ##__VA_ARGS__)
int main(void) {
// Object-like
printf("Label: %s, Max tracks: %d\n", LABEL, MAX_TRACKS);
// Function-like
printf("SQUARE(5) = %d\n", SQUARE(5));
printf("SQUARE(1+2) = %d\n", SQUARE(1 + 2));
printf("MAX(3, 7) = %d\n", MAX(3, 7));
// Stringification
int year = 1984;
PRINT_INT(year);
// SWAP
int a = 10, b = 20;
printf("Before swap: a=%d, b=%d\n", a, b);
SWAP(a, b);
printf("After swap: a=%d, b=%d\n", a, b);
// Conditional compilation
#ifdef DEBUG
printf("Debug mode is on\n");
#else
printf("Debug mode is off\n");
#endif
// Variadic macro
LOG("started");
LOG("year is %d", 1985);
return 0;
}Key Points
- Macros are textual substitution performed before compilation. They are not functions and do not respect scope or type rules.
- Object-like macros define constants and feature flags. Never end a
#definewith a semicolon. - Function-like macros must have every parameter use and the entire body parenthesized to avoid precedence bugs.
- Macro arguments are evaluated each time they appear — do not pass expressions with side effects.
- Use
do { ... } while (0)for multi-statement macros so they work correctly withif/elseand semicolons. - The
#operator stringifies a macro argument;##pastes tokens together. #and##prevent argument expansion. Use the two-level indirect pattern (e.g.,XSTRINGIFY/STRINGIFY) when you need the expanded value.__VA_ARGS__enables variadic macros for wrappingprintf-style functions.- X-macros define a list once and expand it multiple ways, keeping enums and string tables in sync.
Exercises
Think about it: C++ uses
constexprandinlinefunctions to replace many uses of macros. What specific problems do macros have that these C++ features solve? Why does C still rely on macros despite these problems?What does this produce?
#define DOUBLE(x) ((x) + (x)) int i = 5; printf("%d\n", DOUBLE(i++));Calculation: Given the macro
#define BUFSIZE 256, how many bytes doeschar buf[BUFSIZE + 1]allocate? Why is the+ 1a common pattern?Where is the bug?
#define MUL(a, b) a * b int result = MUL(2 + 3, 4 + 5); printf("%d\n", result);What does this produce?
#define STRINGIFY(x) #x #define XSTRINGIFY(x) STRINGIFY(x) #define VERSION 3 printf("[%s] [%s]\n", STRINGIFY(VERSION), XSTRINGIFY(VERSION));Where is the bug?
#define LOG_IF(cond, msg) \ if (cond) \ printf("[WARN] %s\n", msg); if (x > 100) LOG_IF(x > 200, "very high"); else printf("normal\n");Write a program that defines an X-macro list of at least four colors, then uses it to generate both an
enumand a function that returns the string name for a given enum value. Print each color’s enum value and name.