🎩 The Preprocessor
#define: object-like macros & text substitution
▶ Open the interactive lesson — free, no signupIn a game where the number 64 means "max players" in ten different places, updating nine of them means you've shipped a bug. #define lets you name that number once and change it everywhere with a single edit, which is why real C code is full of ALL_CAPS names like MAX_USERS. You'll also see how this find-and-replace can quietly betray you, and how one pair of parentheses saves the day.
#define teaches the preprocessor a new word. From that line on, every time the word appears on its own as a name in your code, it's replaced by the text you gave — before compilation, with zero understanding of C. That last clause is where all the power and all the danger live.
#include <stdio.h>
#define MAX_USERS 64
#define GREETING "hello, "
#define PI 3.14159265358979
int main(void) {
int slots[MAX_USERS]; /* becomes: int slots[64]; */
printf(GREETING "world\n"); /* string literals concat! */
printf("circumference: %f\n", 2 * PI * 10);
printf("slots: %zu\n", sizeof slots / sizeof slots[0]);
return 0;
}$ gcc defines.c -o defines && ./defines hello, world circumference: 62.831853 slots: 64
A macro like this — just a name and its replacement text, no parentheses after the name — is called an object-like macro, and it's the classic way to name a constant. Convention: macro names are SCREAMING_SNAKE_CASE — the all-caps shout warns readers "this is not a variable, it's a substitution."
It really is just text
The preprocessor doesn't compute 10+10 into 20. It stores the five characters 1 0 + 1 0 and pastes them wherever SIZE appears. Predict this one before peeking:
#include <stdio.h>
#define SIZE 10+10 /* looks like 20... */
int main(void) {
printf("%d\n", SIZE); /* line A */
printf("%d\n", SIZE * 2); /* line B */
return 0;
}🤔 What do lines A and B print?
Think first
Line A prints 20 — but line B prints 30, not 40!
The substitution is textual: SIZE * 2 becomes 10+10 * 2, and multiplication binds tighter than addition, so it computes 10 + (10*2) = 30. The preprocessor never saw "20"; it only ever saw five characters of text. The fix: #define SIZE (10+10).
Rule zero of macros: if the replacement text is an expression, wrap it in parentheses: #define SIZE (10+10). You cannot control what precedence-sensitive context your macro gets pasted into.
🧠 Checkpoint: With #define N 2+3, what does N * N evaluate to?
- 25
- 11
- 10
- 13
Show answer
11 — Textually: 2+3 * 2+3 → 2 + (3×2) + 3 = 11. Parenthesize the definition — (2+3) — and you would get 25 as intended.
Fine print on substitution
- Macros are not replaced inside string literals:
printf("SIZE")prints the word SIZE. - Nor inside other identifiers:
SIZESormy_SIZEare untouched — replacement works on whole tokens, not substrings. - Macro definitions last until the end of the file — scope means nothing to the preprocessor. A
#defineinside a function still applies to every later function. - To retire a macro early, use
#undef SIZE. It's rare, but handy when a library macro shadows something you need (e.g. the standard library is allowed to define function-like macros for its functions —#undef getcgets you the real function).
Macros the compiler defines for you
The implementation predefines a set of magic macros, refreshed at each use site:
| macro | expands to | example |
|---|---|---|
__FILE__ | current file name (string) | "main.c" |
__LINE__ | current line number (int) | 42 |
__DATE__ / __TIME__ | compilation date / time (strings) | "Aug 1 2026" |
__STDC__ | 1 on a conforming compiler | 1 |
__STDC_VERSION__ | the C standard in use (long) | see below |
__func__ | enclosing function's name | "main" |
Pedantic gem: __func__ is technically not a macro but a predefined identifier — it behaves like a local static const char[], because the preprocessor has no idea what function it's in. Everyone lumps it in with these anyway.
__STDC_VERSION__ is how code detects the standard version:
| standard | __STDC_VERSION__ |
|---|---|
| C89/C90 | not defined (only __STDC__) |
| C95 | 199409L |
| C99 | 199901L |
| C11 | 201112L |
| C17 | 201710L |
| C23 | 202311L |
#include <stdio.h>
int main(void) {
printf("file: %s, line: %d\n", __FILE__, __LINE__);
printf("func: %s\n", __func__);
printf("built: %s %s\n", __DATE__, __TIME__);
printf("standard: %ld\n", __STDC_VERSION__);
return 0;
}$ gcc whoami.c -o whoami && ./whoami file: whoami.c, line: 4 func: main built: Aug 1 2026 14:03:22 standard: 201710 # gcc's current default is C17 (201710L); try -std=c23
🧠 Checkpoint: Your code compiled with -std=c11 checks __STDC_VERSION__. What value does it see?
- 199901L
- 201112L
- 201710L
- 11L
Show answer
201112L — The value encodes year and month of the standard: 2011-12 for C11. C99 is 199901L, C17 is 201710L, C23 is 202311L.
#define vs const vs enum
C gives you three ways to name a constant, and they are genuinely different:
#define MAX 100 | const int max = 100; | enum { MAX = 100 }; | |
|---|---|---|---|
| has a type? | no — raw text | yes | yes (int) |
| obeys scope? | no | yes | yes |
| visible in debugger? | usually not | yes | yes |
| usable as case label / array size? | yes | no (in C it's not a constant expression!) | yes (integers only) |
That third row surprises people coming from C++: in C, a const int is merely a read-only variable, so int arr[max]; is a VLA and case max: is an error. For integer constants, enum gives you type + scope + constant-expression status — often the best of all worlds. (C23 finally adds a true constexpr; that story continues in Part 5.)
🧠 Checkpoint: Why does const int max = 100; ... case max: fail to compile in C?
- case labels must be literals only
- In C a const variable is not a constant expression
- const variables cannot be read in a switch
- It compiles fine
Show answer
In C a const variable is not a constant expression — Unlike C++, C treats a const-qualified variable as a read-only object, not a compile-time constant. Case labels need integer constant expressions — use a macro, an enum constant, or (C23) constexpr.
Try it
▶ This spot has an interactive editor widget — open the interactive lesson to play with it.
Object-like macros substitute text; give them parameters and they substitute parameterized text — welcome to function-like macros, where the real footguns are stored.
▶ Practice this lesson interactively (with live gcc)