The C Path — learn C, visually

🎩 The Preprocessor

#define: object-like macros & text substitution

⏱ 12 min · free interactive lesson · quizzes, visualizations & a real compiler

▶ Open the interactive lesson — free, no signup
Why you're learning this

In 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.

defines.c
#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;
}
terminal
$ 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:

trap.c — predict the output
#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 the compiler defines for you

The implementation predefines a set of magic macros, refreshed at each use site:

macroexpands toexample
__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 compiler1
__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/C90not defined (only __STDC__)
C95199409L
C99199901L
C11201112L
C17201710L
C23202311L
whoami.c
#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;
}
terminal
$ 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 100const int max = 100;enum { MAX = 100 };
has a type?no — raw textyesyes (int)
obeys scope?noyesyes
visible in debugger?usually notyesyes
usable as case label / array size?yesno (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)