#define MAX_TASKS 256 /*
1. give a name to a constant */
#define SQUARE(x)((x)*(x)) /*
2. give a name to a piece of code */The #define Directive in C: Constants, Macros, and Examples
What is The #define Directive in C? Core Concept & Direct Summary
How does `#define` create symbolic constants, multi-statement macros with `do-while(0)`, and configuration flags?
#define is the directive you'll use more than any other preprocessor feature. It does exactly two jobs:
Both are the same operation — naming a piece of text — and everything else about #define follows from that. It creates names the preprocessor substitutes before your code is compiled. The names have no type, no address, and no scope in the language's sense; they exist only between the #define line and the end of the file.
This page is about the directive itself: its exact syntax rules, how to use it well for constants, how it relates to const and enum, and what #undef and redefinition actually do. (For the expansion algorithm — pre-expansion, #, ##, variadic forms, argument counting — see the macros guide.)
(Suggested image: the hero graphic — one #define line splitting into two labeled paths: "names a value" and "names code")
How Does The #define Directive in C Work? In-Depth Mechanics & Architecture
The Directive's Syntax Rules
#define is a preprocessor directive, and directives have their own grammar — not C's. These rules are worth knowing precisely because they're where the "why won't this compile" moments come from.
It's line-oriented: one logical line per directive
A directive runs from # to the end of the line — there is no closing delimiter and no semicolon.
That second one is a genuine trap. MAX2 now expands to 100;, so:
The error points at the array declaration, but the mistake is the semicolon on the #define line. Never put a semicolon in a #define.
Continuation with a backslash
To span multiple physical lines, end each line with a backslash as the very last character. Verified — six physical lines splice into a single logical line:
The gotcha: the backslash must be the last character — a trailing space after it breaks the continuation and leaves you with a fragment of a macro and an error on some unrelated later line. Many editors show trailing whitespace precisely for this reason; turn that on if you write multi-line macros.
Comments are removed before the directive is processed
Comments are handled in an earlier translation phase than #define, replaced by a single space. Verified:
Two consequences worth noting: a comment cannot be used inside a macro body without introducing a space (x/*n*/+1 becomes x +1, not x+1), and a comment can never be used to hide part of a directive from the preprocessor.
Leading whitespace and a space after # are both allowed
All four are equivalent. That flexibility is why directives look like they're floating in your source — they are, syntactically.
The two forms
And two special cases:
The rule that decides which form you get: whether the ( is adjacent to the name.
One space changes the entire meaning of the directive. This is the single most confusing #define syntax quirk, and it exists nowhere else in C.
(Suggested image: four #define spellings with indicators on the #, whitespace, name, and (, showing which parts are flexible and which are not)
Using #define for Constants
This is the most common use by a wide margin. Four categories, each with its own considerations.
1. Literal constants
2. Character and string constants
A string constant has a real advantage over a repeated literal: the text lives in one place, so APP_NAME cannot drift out of sync across a program. And adjacent string literals concatenate, which makes this work naturally:
3. Computed constants
Macros can be defined in terms of other macros, which keeps a set of related values self-consistent:
Verified values:
Note the parentheses around each one. #define ONE_MB ONE_KB * 1024 would break the moment it's composed — the same body-parenthesization rule from the macros guide, and it applies just as much to constants as to expressions.
4. The type problem — and the overflow it causes
A macro has no type.#define ONE_GB 1073741824 is just digits. Whether it's an int, unsigned, or long long depends on where the value lands — which means a computed constant can silently overflow.
Verified — the 32-bit boundary:
ONE_GB fits a 32-bit int — but only just. Doubling it doesn't, and a terabyte (ONE_TB) overflows both int and unsigned. The arithmetic happens in whatever type the use site provides; the macro itself offers no protection and no warning.
The fix: put the type in the constant, with a literal suffix.
Literal suffixes are the only type information a macro constant can carry, and they're essential for anything that might exceed 32 bits: u/U for unsigned, l/L for long, ul/UL, ll/LL for long long, f/F for float.
5. Integer division in computed constants
A macro that computes a ratio in integer arithmetic truncates — verified:
66 instead of 66.67 is a correctness bug in anything measuring a rate, and it comes from the arithmetic — not from the macro mechanism. Introduce a floating-point literal (100.0, 0.5f) whenever you want a fractional result.
6. The naming convention
Macros are conventionally ALL_CAPS — MAX_TASKS, ONE_GB, APP_NAME. This isn't decoration: it's the only signal in C that a name is a macro, because a macro can shadow a variable or function of the same name and the code will still compile:
ALL_CAPS names can't collide with the ordinary identifiers you write, so they're a naming hygiene measure that prevents a whole class of baffling errors. It also makes them greppable — you can find every use of a macro by searching its name.
(Suggested image: the four constant categories as cards — literal, string, computed, typed-with-suffix — with the overflow boundary highlighted on the computed card)
#define vs const vs enum: Which Should a Constant Be?
If you need a named constant, you have three options, and the choice is not cosmetic. Each one produces genuinely different behaviour.
The C-specific gotcha that surprises C++ programmers
In C++, a const int is a constant expression and works anywhere a constant is needed. In C, it is not — const means "read-only", not "compile-time known". Two visible consequences:
So in C, const cannot replace a macro where the constant must appear in a case label, an array bound at file scope, or a bit-field width. That's a legitimate reason to keep a macro.
The recommendation, in order
1. Use enum for related integer constants.
You get names, proper scope, an integer type, type checking at use sites, and debugger visibility — with no code generated. For a set of related values (states, error codes, limits), enum is strictly the best option.
2. Use const (or static const) for typed, scoped, addressable values.
A real object: typed, scoped, visible in the debugger, and it has an address if something needs one. Ideal for values of type double, strings, and structs — none of which enum can express.
3. Use #define only when you need what the others structurally cannot provide:
- The constant must appear in
#if/#elif. Only macros exist at that stage — this alone justifies plenty of macros. - It must work as a
caselabel, a file-scope array bound, or a bit-field width — in C. - It must be visible before any compilation, or overridable from the build system (
-D), which is how build configuration and feature flags work. - It must be usable for both a value and as part of token generation (stringizing, pasting, or an X-macro list).
- It must work generically across types textually.
And it's worth saying plainly: the C89-era habit of #define-ing everything is not a best practice, it's an inheritance. Modern C has better tools for most constants, and using them buys you type checking, scoping, and a debugger that can show you the value.
(Suggested image: the three-way comparison as a decision card — "a set? → enum. typed/addressable? → const. needs #if or literal contexts? → #define")
#undef: Ending a Macro's Life
After the #undef, the name is an ordinary identifier again — not defined, no longer substituted. #undef on a name that isn't defined is legal and does nothing, which makes it safe to write unconditionally.
Four real uses:
1. Replacing a macro deliberately.
Without the #undef, this is a redefinition (see below) and may warn or fail.
2. Overriding something a library defined.
Legitimate, but be sure you mean it — you've just broken the library's assumption for everything that follows.
3. Temporary macros — the X-macro pattern. This is the important one. A generic name like X is defined, used, and undefined immediately, so it doesn't leak into the rest of the file:
4. Cleaning up after a diagnostic on the build system.gcc -UMAX removes a macro for a compile, mirroring #undef.
And the counterpart test — is a name defined? Use #ifdef / #ifndef, or #if defined(NAME) when combining conditions:
(Suggested image: a macro's lifetime bar — defined at the #define line, active to the end of the file, truncated early by #undef)
Where #define Can Appear, and How Far It Reaches
This trips people up more than the syntax. A macro's reach is purely positional — from its #define to the end of the file (or the next #undef).
It can appear anywhere — including inside a function
It looks local and isn't. There's no block scope for macros. If you want one confined, #undef it:
Order matters — before or after an #include changes everything
Headers that behave differently depending on macros defined before them are configuration headers, and this ordering mechanism is how compile-time configuration usually works. It's also a genuine source of confusion: the same header included at two different points can produce two different translation units.
The convention that keeps this manageable: put your configuration macros in a single dedicated header, and ensure it's included first in every translation unit that needs it.
Where to put #defines in a project
(Suggested image: two source fragments — one defining a config macro before the include, one after — with the resulting translation units differing)
Redefinition Rules, Exactly
This is specified precisely, and knowing the rule prevents a lot of confusion.
Redefining a macro is legal if the new definition is identical to the old one. "Identical" means: the same tokens in the same order, with whitespace differences ignored. And — the part people don't expect — parameter names may differ.Verified:
Normalizing parameter names makes the point clear: MAX(a,b) and MAX(x,y) with the same body are the same macro as far as the standard is concerned, so it's legal — and it's common in practice when two headers both define the same helper.
A different definition is undefined behavior. In practice compilers warn (-Wmacro-redefined on GCC) and keep the first or last definition depending on the compiler — which is exactly the kind of silent divergence that makes a build work on your machine and fail on someone else's.
The habit that avoids all of this:
That's a defensive guard for a library macro you intend to own. It's verbose, but it makes the intent explicit and portable.
(Suggested image: two identical definitions being accepted, and a differing one raising a diagnostic, with the parameter names normalized to show why)
Defining Macros From the Build Command Line
#define has a command-line equivalent, and it's how build configuration usually arrives:
Details that matter:
-DNAMEwith no=valuedefines it as1. That's why#if DEBUGworks with-DDEBUGbut not with a bare#define DEBUG(see the trap below).- No spaces.
gcc -DMAX = 200definesMAXas 1 and then tries to compile=and200as filenames. - Quoting a string macro is shell-dependent and awkward — prefer putting string constants in a header and using
-Donly for numbers and flags. -Uundefines, which is how you override a macro a header defines unconditionally.
This is exactly how NDEBUG disables assert, how DEBUG enables logging, and how feature flags reach the compiler. A macro defined on the command line is available to every source file in that compile — the only mechanism in C for influencing code before it's read.
(Suggested image: a build command with -D flags on the left and the equivalent #define lines on the right, marked "same result")
Practical Examples
Example 1: A configuration header
Example 2: Using it
Output:
Why the VERSION_NUM trick is worth learning.2.1.0 is a string — you can't compare it numerically. Encoding the components as major*10000 + minor*100 + patch gives 20100, which is comparable:
So #if VERSION_NUM >= 20100 compiles a feature only into 2.1.0 and later — decided at compile time, with no runtime check and no code emitted when the condition is false.
The limitation, stated honestly: each component must stay under 100. A minor version of 1.100.0 would produce 20000, colliding with 2.0.0. Systems that need more room use 8-bit fields in a 32-bit integer instead:
That gives each component 0–255 with no collisions.
Example 3: Feature flags that remove code entirely
With FEATURE_LOGGING at 0, every LOG("...") call compiles to nothing — not a call that returns early, but literally no code. That's a capability no function or const can provide, and it's the strongest argument for keeping #define in modern C.
Example 4: Why #define is wrong for types
A common mistake: using #define to make a type shorthand. Verified by expansion:
That's int *aandint b — one pointer and one plain int, because * binds to the declarator, not the type. Now the correct version:
For types, always use typedef, never #define. A typedef creates a real type name that the compiler understands as a unit; a macro is text, and text doesn't survive C's declarator syntax. (The same reasoning applies to #define u32 unsigned int — it works for simple uses and breaks for pointer and array declarations.)
(Suggested image: the PTR a, b; expansion showing a as int and b as int, next to the typedef version where both are pointers)*
Debugging #define
-Wundef deserves a special mention. Undefined identifiers inside #if evaluate to 0silently — so #if SEMVER > 2 with a typo'd name is simply false, with no diagnostic at all. This is one of the few places where C fails silently by design, and -Wundef is the antidote. Turn it on in any project that uses conditional compilation.
(Suggested image: a compile error inside a macro expansion shown before and after -ftrack-macro-expansion, with the improved pointing highlighted)
Common Mistakes
- Putting a semicolon in a
#define—#define MAX 100;makesMAXexpand to100;, breakingint arr[MAX];intoint arr[100;];. No semicolons in directives. - Trailing whitespace after a
\continuation — breaks the splice and produces an error on an unrelated line. - A space before
(in a function-like macro —#define F (x)is an object-like macro whose body starts with(x). Completely different meaning. - Assuming a macro obeys scope — a
#defineinside a function applies for the rest of the file. Use#undefto confine it. - Forgetting parentheses around computed constants —
#define ONE_MB ONE_KB * 1024breaks when composed with anything. - Ignoring the type of a large constant — verified:
ONE_GB(1073741824) fits a 32-bitintbarely; doubling it overflows, and a terabyte overflows bothintandunsigned. Use suffixes (UL,ULL). - Expecting fractional results from integer constant math — verified: a percent macro gives
66, not66.67. Use a floating-point literal. - Using
#definefor a type — verified:#define PTR int*thenPTR a, b;gives a pointer and an int. Usetypedef. - Lowercase macro names — they can silently collide with variables and functions. Use ALL_CAPS.
- Redefining a macro differently — undefined behavior/diagnostic; it may work on one compiler and not another.
#undeffirst if intentional. - A bare
#define DEBUGand then testing#if DEBUG— an empty macro expands to nothing, so#if DEBUGbecomes#ifwith no expression: a compile error (#if with no expression). Use#define DEBUG 1for#if, or#ifdef DEBUG. This is the single most common#definetrap in real code. - Assuming
constcan replace a macro everywhere — in C aconstobject is not a constant expression, so it can't be acaselabel or a file-scope array bound. - Forgetting
#undefafter a temporary macro — especially in X-macro patterns, where a generic name likeXleaks into everything below. - Placing a
#defineafter the#includethat needs it — the header was preprocessed without it. Include the config header first. - Expecting a macro name to expand inside a string literal — quoted strings are opaque;
"MAX"stays"MAX". - Macro names that collide with system or library names — the macro wins and breaks the header it was meant to complement. Prefix your macros (
APP_MAXrather thanMAX). - Doing arithmetic in a macro argument with side effects — still true for constant-style macros used as function-like macros: the parameter may be evaluated more than once.
- Assuming
#defineallocates storage — it never does;&NAMEis never valid.
(Suggested image: "18 #define Mistakes" checklist — with #1, #11, and #8 highlighted as the three that most often confuse beginners)
Final Thoughts
#define is one directive with one effect — naming text — and it does it in two shapes: a name for a value, and a name for code. The directive-level rules are few but unforgiving: line-oriented, no semicolon, backslash to continue, comments already removed, and that one space before ( changing everything.
For constants, the useful discipline is a simple order of preference:
enumfor related integer constants — names, scope, type checking, zero costconstfor typed, scoped, addressable values — especiallydouble, strings, and structs#defineonly when it's structurally required —#if,caselabels and file-scope array bounds in C, build-time configuration via-D, and code that must vanish entirely when a feature is off
Two habits prevent the majority of #define bugs: put the type in the constant with a literal suffix (1024UL, not 1024), and parenthesize computed constants. A third prevents the rest: use ALL_CAPS, so a macro can never be mistaken for — or collide with — an ordinary name.
And remember that a macro has no type, no scope, and no address, and its reach is purely positional. Those aren't limitations to work around; they're the shape of the tool. Know them and #define becomes predictable; forget them and it becomes the source of your most confusing compile errors.
Runnable C Code Example & Practical Implementation
#include <stdio.h>
#define LOG_ERROR(msg) do {
\
fprintf(stderr, "[ERROR] %s:%d: %s\
", __FILE__, __LINE__, msg);
\
} while (0)
int main() {
int status = -1;
if (status < 0)
LOG_ERROR("Failed to initialize network subsystem");
else
printf("Initialization OK\
");
return 0;
}Key Takeaways, Best Practices & Memory Safety Rules
do { ... } while(0) blocks.Frequently Asked Questions & Technical Interview Insights
Technical Interview & Viva Questions
Frequently asked C programming interview questionsQ: Why is `do { ... } while(0)` necessary for multi-statement macros?
Q: What does `#undef` do?
Related Tools, Tutorials & Authoritative References
Recommended Interactive Tools
- Online C Compiler & Runner — Interactive online GCC sandbox to execute C code in real time.
- Online C Code Editor & IDE — Multi-file C programming environment with code formatting.
- C++ Programming Tutorial — Transition from procedural C to Object-Oriented C++ with STL.
- JavaScript Programming Tutorial — Master modern JavaScript fundamentals, DOM APIs, and async patterns.
Authoritative Standards & Citations
- ISO/IEC 9899 (C Language Standard) — Official international C specification by ISO/IEC JTC 1/SC 22/WG 14.
- GNU GCC Documentation — Official reference manual for the GNU Compiler Collection and C libraries.
- The C Programming Language (K&R) — Definitive classic work by Brian Kernighan & Dennis Ritchie at Bell Labs.
- C Reference Documentation (cppreference.com) — Verified reference covering C89 through C23 language features.