Topic 92 of 100 Preprocessors and Macros Interactive Tutorial

The #define Directive in C: Constants, Macros, and Examples

12 mins read Last updated: 22 Aug 2026 21,800 views
C Compiler 0.00%

What is The #define Directive in C? Core Concept & Direct Summary

Core Objective & Problem Statement

How does `#define` create symbolic constants, multi-statement macros with `do-while(0)`, and configuration flags?

Direct Answer Summary

#define is the directive you'll use more than any other preprocessor feature. It does exactly two jobs:

main.c
Run Online
#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 */

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.

main.c
Run Online
#define MAX 100 /* ✓ */
#define MAX2 100;
/* ⚠ the ';' becomes part of the replacement text! */

That second one is a genuine trap. MAX2 now expands to 100;, so:

main.c
Run Online
int arr[MAX2];
/* → int arr[100;];   ← syntax error */

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:

main.c
Run Online
physical lines: 6logical lines after splicing: 1the whole macro is ONE logical line(directives are line-oriented):
#define ASSERT_POSITIVE(v) do {
    if ((v) <= 0) {
        report(#v);
    }
} while (0)

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:

main.c
Run Online
before:
#define SIDE(4) /* width in cm */ +(3)after :
#define SIDE(4) +(3)
before:
#define F(x) x/*n*/+1after :
#define F(x) x +1

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

main.c
Run Online
#define MAX 100 /* ✓ any amount of whitespace around the name */
#define MAX 100 /* ✓ indented is fine */
# define MAX 100 /* ✓ a space between # and define is legal */

All four are equivalent. That flexibility is why directives look like they're floating in your source — they are, syntactically.

The two forms

FormSyntaxExpands when written as
Object-like#define NAME replacementNAME
Function-like#define NAME(p1, p2) replacementNAME(a, b)

And two special cases:

FormSyntaxNote
Empty#define NAMEExpands to nothing — used purely as a flag
Parameterless function-like#define NAME() replacementExpands only when written NAME()

The rule that decides which form you get: whether the ( is adjacent to the name.

main.c
Run Online
#define F(x)((x)*2) /* ✓ function-like — one parameter */
#define F(x)((x)*2) /* ⚠ SPACE: object-like named F, body starts "(x) ((x)*2)" */

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

main.c
Run Online
#define MAX_TASKS 256
#define DEFAULT_TIMEOUT 30
#define PI 3.141592653589793
#define ENABLED 1
#define DISABLED 0

2. Character and string constants

main.c
Run Online
#define PATH_SEP '/'
#define FIELD_SEP '|'
#define LINE_END '\n'
#define EMPTY_STRING ""
#define APP_NAME "Tasks"
#define APP_VERSION "2.1.0"

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:

main.c
Run Online
printf(APP_NAME " version " APP_VERSION "\n");
/* ← all one string to the compiler */

3. Computed constants

Macros can be defined in terms of other macros, which keeps a set of related values self-consistent:

main.c
Run Online
#define SECONDS_PER_MINUTE(60)
#define SECONDS_PER_HOUR(SECONDS_PER_MINUTE * 60)
#define SECONDS_PER_DAY(SECONDS_PER_HOUR * 24)
#define ONE_KB(1024u)
#define ONE_MB(ONE_KB * 1024u)
#define ONE_GB(ONE_MB * 1024u)

Verified values:

main.c
Run Online
#define SECONDS_PER_DAY(24 * 60 * 60) = 86400
#define SECONDS_PER_YEAR(365 * 24 * 60 * 60) = 31536000
#define KB 1024 = 1024
#define MB(KB * 1024) = 1048576
#define GB(MB * 1024) = 1073741824
#define TB(GB * 1024) = 1099511627776

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:

main.c
Run Online
INT_MAX(32-bit int) = 2147483647UINT_MAX(32-bit uint) = 4294967295GB fits in int? True(1073741824 < 2147483647)GB*2 fits in int? False(2147483648 > 2147483647) -> OVERFLOWTB fits in uint? False(1099511627776 > 4294967295) -> OVERFLOW

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.

main.c
Run Online
#define ONE_KB(1024UL) /* unsigned long  */
#define ONE_MB(ONE_KB * 1024UL)
#define ONE_GB(ONE_MB * 1024UL)
#define ONE_TB(ONE_GB * 1024UL) /* safe: 64-bit unsigned long */
#define TINY 0.5f /* float,  not double */
#define EXACT 1.0 /* double */
#define BIG 1000000000000LL /* long long */

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:

main.c
Run Online
#define PERCENT(p,total)((p) * 100 /(total))2 out of 3 -> 66(not 66.67 - integer division truncates)1 out of 2 with((a)+(b))/2 for a=1,b=2 -> 1(not 1.5)fix with a floating-point literal:((p) * 100.0 /(total)) -> 66.67

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:

main.c
Run Online
#define count 10
void f(void) {
    int count = 5;
    /* ⚠ actually: int 10 = 5;   ← syntax error */
}

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.

#define MAX 100const int max = 100;enum { MAX = 100 };
Has a typeNoint (or whatever you write)An integer type
Can take its addressNoYesNo
Obeys scopeNo — file from that pointYesYes
Visible in a debuggerNoYesUsually
Usable in #ifYesNoNo
Usable as an array sizeYesNot at file scope in CYes
Usable as a case labelYesNo in CYes
Type-checked at use sitesNoYesYes
Multiple copies in binaryNoPossiblyNo
Occupies storageNoPossiblyNo
Substitutable by the compilerAlready substitutedYes, when it canYes

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:

main.c
Run Online
const int n = 10;
int arr[n];
/* ⚠ In C this is a VLA (C99+) at block scope, or an ERROR at file scope.                      In C++ it is a normal fixed-size array. */
switch (x) {
    case n: /* ✗ ERROR in C — a const object is not a constant expression */
}

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.

main.c
Run Online
typedef enum {
    STATUS_OK = 0, STATUS_WARN = 1, STATUS_ERROR = 2, STATUS_FATAL = 3
}
Status;
enum {
    MAX_TASKS = 256, DEFAULT_TIMEOUT = 30
}
;

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.

main.c
Run Online
static const double PI = 3.141592653589793;
static const char *APP_NAME = "Tasks";

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 case label, 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.

main.c
Run Online
#include <limits.h>
#undef INT_MAX
#define INT_MAX 32767 /* deliberately restricting for a target */

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:

main.c
Run Online
#define X(name, value) COLOR_
#
#name,typedef enum {
    COLORS
}
Color;
#undef X /* ← without this, X leaks everywhere after */

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:

main.c
Run Online
#ifdef DEBUG /* ... */
#endif
#if defined(_WIN32) && defined(_MSC_VER) /* both must be true */
#endif

(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

main.c
Run Online
void setup(void) {
    #define SCALE 3 printf("%d\n", SCALE * 2);
}
/* ⚠ SCALE is still defined for the REST OF THE FILE, including later functions */

It looks local and isn't. There's no block scope for macros. If you want one confined, #undef it:

main.c
Run Online
void setup(void) {
    #define SCALE 3 printf("%d\n", SCALE * 2);
    #undef SCALE
}

Order matters — before or after an #include changes everything

main.c
Run Online
#define FEATURE_X 1 /* ✓ visible to the header below */
#include "module.h"
#include "other.h" /* ✗ FEATURE_Y not defined here */
#define FEATURE_Y 1

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

Kind of macroBelongs in
Public constants and configurationA header (config.h, api.h)
Internal implementation macrosThe .c file, above its first use
Build-dependent flagsThe build system (-D), or a generated config header
Include guardsThe header itself, at the top
Temporary helper macrosImmediately around their use, with #undef after

(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:

main.c
Run Online
#define MAX(a,b)((a)>(b)?(a):(b)) normalized ->((P0)>(P1)?(P0):(P1))
#define MAX(x,y)((x)>(y)?(x):(y)) normalized ->((P0)>(P1)?(P0):(P1))identical? True -> LEGAL redefinition(no diagnostic)
#define MAX(a,b)((a)<(b)?(a):(b)) normalized ->((P0)<(P1)?(P0):(P1))identical to the first? False -> UNDEFINED BEHAVIOUR / diagnostic

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:

main.c
Run Online
#ifdef
MAX
#undef MAX
#endif
#define
MAX(a,b)((a)>(b)?(a):(b))

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:

main.c
Run Online
gcc -DDEBUG program.c
# equivalent to:
#define DEBUG 1gcc -DMAX=200 program.c
# equivalent to:
#define MAX 200gcc -DAPP_NAME='"Tasks"' program.c
# string macro — quoting is shell-dependentgcc -UDEBUG program.c
# equivalent to:
#undef DEBUG

Details that matter:

  • -DNAME with no =value defines it as 1. That's why #if DEBUG works with -DDEBUG but not with a bare #define DEBUG (see the trap below).
  • No spaces.gcc -DMAX = 200 defines MAX as 1 and then tries to compile = and 200 as filenames.
  • Quoting a string macro is shell-dependent and awkward — prefer putting string constants in a header and using -D only for numbers and flags.
  • -U undefines, 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

main.c
Run Online
/* config.h */
#ifndef CONFIG_H
#define CONFIG_H
/* ---- scalar constants ---- */
#define APP_NAME "Tasks"
#define APP_VERSION "2.1.0"
#define MAX_TASKS 256
#define DEFAULT_TIMEOUT 30
/* ---- character constants ---- */
#define PATH_SEP '/'
#define FIELD_SEP '|'
#define LINE_END '\n'
/* ---- computed constants (each parenthesised) ---- */
#define SECONDS_PER_MINUTE(60)
#define SECONDS_PER_HOUR(SECONDS_PER_MINUTE * 60)
#define SECONDS_PER_DAY(SECONDS_PER_HOUR * 24)
/* ---- sized constants: the suffix IS the type ---- */
#define ONE_KB(1024u)
#define ONE_MB(ONE_KB * 1024u)
#define ONE_GB(ONE_MB * 1024u)
#define ONE_TB(ONE_GB * 1024u)
/* ---- a version number that can be compared ---- */
#define VERSION_MAJOR 2
#define VERSION_MINOR 1
#define VERSION_PATCH 0
#define VERSION_NUM \(VERSION_MAJOR * 10000 + VERSION_MINOR * 100 + VERSION_PATCH)
#endif /* CONFIG_H */

Example 2: Using it

main.c
Run Online
#include "config.h"
#include <stdio.h>
int main(void) {
    printf("%s version %s\n", APP_NAME, APP_VERSION);
    printf("Buffer size : %d tasks\n", MAX_TASKS);
    printf("Timeout     : %d s\n", DEFAULT_TIMEOUT);
    printf("One day     : %d s\n", SECONDS_PER_DAY);
    printf("Version code: %d\n", VERSION_NUM);
    /* Version-gated feature */ if (VERSION_NUM >= 20100) {
        puts("Feature available since 2.1.0");
    }
    return 0;
}

Output:

main.c
Run Online
Tasks version 2.1.0Buffer size : 256 tasksTimeout : 30 sOne day : 86400 sVersion code: 20100Feature available since 2.1.0

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:

VersionVERSION_NUM
1.9.910909
2.0.020000
2.1.020100
2.10.021000

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:

main.c
Run Online
#define VERSION_NUM((VERSION_MAJOR << 16) |(VERSION_MINOR << 8) | VERSION_PATCH)

That gives each component 0–255 with no collisions.

Example 3: Feature flags that remove code entirely

main.c
Run Online
/* features.h */
#define FEATURE_LOGGING 1
#define FEATURE_CACHE 0
#if FEATURE_LOGGING
#define LOG(msg) fprintf(stderr, "[log] %s\n", msg)
#else
#define LOG(msg)((void)0)
#endif

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:

main.c
Run Online
#define PTR int* /* ⚠ DANGEROUS */PTR a, b;
/* expands to:  int* a, b;
*/

That's int *aandint b — one pointer and one plain int, because * binds to the declarator, not the type. Now the correct version:

main.c
Run Online
typedef int *Ptr;
/* ✓ */Ptr a, b;
/* both are pointers to int */

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

NeedCommand / technique
See exactly what a macro expanded togcc -E file.c then read the output
Readable preprocessed outputgcc -E -P file.c (omits #line markers)
Keep the #defines in the outputgcc -E -dD file.c
List every predefined macrogcc -dM -E - (reads from stdin)
Warn on differing redefinitions-Wmacro-redefined
Warn on undefined names in #if-Wundef (they otherwise silently evaluate to 0)
Better macro error locations-ftrack-macro-expansion=2 (GCC)

-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

  1. Putting a semicolon in a #define — #define MAX 100; makes MAX expand to 100;, breaking int arr[MAX]; into int arr[100;];. No semicolons in directives.
  2. Trailing whitespace after a \ continuation — breaks the splice and produces an error on an unrelated line.
  3. A space before ( in a function-like macro — #define F (x) is an object-like macro whose body starts with (x). Completely different meaning.
  4. Assuming a macro obeys scope — a #define inside a function applies for the rest of the file. Use #undef to confine it.
  5. Forgetting parentheses around computed constants — #define ONE_MB ONE_KB * 1024 breaks when composed with anything.
  6. Ignoring the type of a large constant — verified:ONE_GB (1073741824) fits a 32-bit int barely; doubling it overflows, and a terabyte overflows both int and unsigned. Use suffixes (UL, ULL).
  7. Expecting fractional results from integer constant math — verified: a percent macro gives 66, not 66.67. Use a floating-point literal.
  8. Using #define for a type — verified:#define PTR int* then PTR a, b; gives a pointer and an int. Use typedef.
  9. Lowercase macro names — they can silently collide with variables and functions. Use ALL_CAPS.
  10. Redefining a macro differently — undefined behavior/diagnostic; it may work on one compiler and not another. #undef first if intentional.
  11. A bare #define DEBUG and then testing #if DEBUG — an empty macro expands to nothing, so #if DEBUG becomes #if with no expression: a compile error (#if with no expression). Use #define DEBUG 1 for #if, or #ifdef DEBUG. This is the single most common #define trap in real code.
  12. Assuming const can replace a macro everywhere — in C a const object is not a constant expression, so it can't be a case label or a file-scope array bound.
  13. Forgetting #undef after a temporary macro — especially in X-macro patterns, where a generic name like X leaks into everything below.
  14. Placing a #define after the #include that needs it — the header was preprocessed without it. Include the config header first.
  15. Expecting a macro name to expand inside a string literal — quoted strings are opaque; "MAX" stays "MAX".
  16. 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_MAX rather than MAX).
  17. 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.
  18. Assuming #define allocates storage — it never does; &NAME is 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:

  • enum for related integer constants — names, scope, type checking, zero cost
  • const for typed, scoped, addressable values — especially double, strings, and structs
  • #define only when it's structurally required — #if, case labels 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

main.c
Run Online
#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

Best Practice & Key Takeaways
Always encapsulate multi-statement macros inside do { ... } while(0) blocks.

Frequently Asked Questions & Technical Interview Insights

Technical Interview & Viva Questions

Frequently asked C programming interview questions
Q

Q: Why is `do { ... } while(0)` necessary for multi-statement macros?

A: If defined simply as `{ stmt1; stmt2; }`, using it in `if (cond) MACRO(); else ...` expands with a trailing semicolon after the closing brace `{ ... }; else`, which is a syntax error that breaks the `else` branch.
Q

Q: What does `#undef` do?

A: It un-defines a previously defined macro name so it can be redefined or disabled.

Related Tools, Tutorials & Authoritative References

Recommended Interactive Tools

Authoritative Standards & Citations