Menu

Conditional Compilation in C: #ifdef, #ifndef, #if, and -D

Compile different code for different builds with #ifdef, #ifndef, #if, #elif and #else - debug switches, platform branches, defining macros from the command line with -D, and using #if 0 to disable code.

This page includes runnable editors - edit, run, and see output instantly.

Conditional compilation lets one source file become several different programs. The preprocessor decides, before the compiler runs, which blocks of text survive - so code inside a failing branch is not merely skipped at runtime, it is deleted. It never gets parsed, so it contributes nothing to the executable and can even contain constructs the current compiler would reject.

That is the difference from an ordinary if. Both choose between paths; only one of them chooses before the program exists.

#ifdef and #ifndef

The simplest question is whether a macro is defined at all:

Delete the #define DEBUG line and every debug print vanishes from the build - not disabled, gone. Run it once as written, then remove that line and run it again.

#ifndef is the negation: keep the block only if the macro is not defined. Its most common use is the include guard covered in header files, and it is also how you supply a default that a caller can override:

Every #ifdef and #ifndef must be closed by #endif. Annotating the closer in long files - #endif /* DEBUG */ - saves real time later.

#if, #elif, #else

#if takes a constant integer expression and keeps the block when it is nonzero. That makes version and level comparisons possible:

Change LOG_LEVEL to 0 or 3 and re-run to see the program itself change shape.

The expression is evaluated by the preprocessor, which means it can use only integer constants, arithmetic and comparison operators, and macros that expand to those. It cannot see sizeof, enum values, variables, or anything that needs the compiler. An undefined macro inside an #if evaluates to 0 rather than being an error, which is convenient and occasionally surprising:

#if FEATURE_X        /* FEATURE_X is never defined anywhere -> 0 -> block dropped */

defined(NAME) turns the #ifdef question into something usable inside an #if, so you can combine tests:

#if defined(DEBUG) && !defined(NDEBUG)
    /* debug build, and assertions are on */
#endif

#if defined(LINUX) || defined(BSD)
    /* either unix-like system */
#endif

#if defined(X) and #ifdef X mean the same thing; reach for the first when you need &&, ||, or !.

Defining Macros from the Command Line

The real power of these switches is that the source need not change at all. gcc -D defines a macro for the whole compilation:

gcc -DDEBUG program.c -o program        # DEBUG defined as 1
gcc -DLOG_LEVEL=3 program.c -o program  # specific value
gcc -DDEBUG -DBUFFER_SIZE=512 a.c b.c -o app

-DNAME alone is equivalent to #define NAME 1. -U NAME undefines one, which matters when a header defines something you want off.

So the usual arrangement is: the source contains #ifndef-guarded defaults, and the build command selects a configuration.

/* config.h */
#ifndef LOG_LEVEL
#define LOG_LEVEL 1        /* quiet by default */
#endif

#ifndef MAX_CONNECTIONS
#define MAX_CONNECTIONS 64
#endif
# development build
gcc -DDEBUG -DLOG_LEVEL=3 -Wall -Wextra -g src/*.c -o app-dev

# release build
gcc -DNDEBUG -O2 src/*.c -o app

NDEBUG is standardized: defining it disables every assert() in the program, because <assert.h> is itself written with conditional compilation. That is the pattern in miniature - a header that compiles to different things depending on what the build defined.

Platform Switches

Compilers predefine macros identifying the target system, so one source file can call the right API on each:

#if defined(_WIN32)
    #include <windows.h>
    #define CLEAR_SCREEN "cls"
#elif defined(__APPLE__)
    #include <unistd.h>
    #define CLEAR_SCREEN "clear"
#elif defined(__linux__)
    #include <unistd.h>
    #define CLEAR_SCREEN "clear"
#else
    #error "Unsupported platform"
#endif

The common ones: _WIN32 (defined on both 32- and 64-bit Windows), _WIN64, __linux__, __APPLE__, __unix__, __ANDROID__. Compiler identity has its own set - __GNUC__, __clang__, _MSC_VER - and architecture too - __x86_64__, __aarch64__.

A runnable version that reports what it was built on:

#error is worth knowing on its own: it stops compilation with your message. Ending a platform chain with #else / #error "Unsupported platform" turns a silent misbuild into a clear failure at the top of the build log.

To list every macro your compiler predefines:

gcc -dM -E - < /dev/null

That output is the authoritative answer to "what can I test for on this machine".

#if 0 to Disable Code

Commenting out a block with /* ... */ fails the moment the block contains a comment of its own, because C comments do not nest - the first */ inside ends the outer comment and everything after it becomes stray code. #if 0 has no such problem:

#if 0
    /* This whole region is removed, comments and all. */
    legacy_init();
    int n = old_calculation(42);  /* even this comment is fine */
    report(n);
#endif

Flip it to #if 1 to bring the code back. Editors keep highlighting it as C, and you can nest an #if 0 inside another #if.

It is a debugging tool, not a storage system. Code sitting in #if 0 is never compiled, so it rots quietly - it will not build by the time anyone flips the switch. Use it while bisecting a problem, then delete the block and let version control remember it.

Where Conditional Compilation Goes Wrong

Three failure modes account for most of the pain.

Code that is never compiled is never checked. A typo inside an inactive #ifdef branch is invisible until someone builds that configuration - possibly months later, possibly in CI on a platform you do not have. If a project has interesting platform branches, build all of them regularly.

Interleaving #ifdef with control flow gets unreadable fast. This kind of thing is hard to reason about:

if (ready) {
#ifdef FAST_PATH
    fast_send(buf);
} else {
#endif
    slow_send(buf);
}

Braces opened in one branch and closed in another are legal and awful. Prefer conditionals that wrap whole functions, and select between them:

#ifdef FAST_PATH
static void send_data(const char *buf) { fast_send(buf); }
#else
static void send_data(const char *buf) { slow_send(buf); }
#endif

A runtime if is often better. If both branches would compile everywhere, an ordinary if (debug_enabled) keeps both paths type-checked, testable, and switchable without a rebuild. Reserve the preprocessor for what only it can do: code that genuinely cannot compile on the other platform.

For the pattern that combines all of this - a header of defaults, a per-build -D, and platform branches wrapped in include guards - see header files; for #define itself, macros.

Frequently Asked Questions

What is conditional compilation in C?

Using preprocessor directives to keep or discard blocks of source code before the compiler runs. Code inside a failing #ifdef is deleted from the text entirely - it is never parsed, never compiled, and never appears in the executable. It is how one source file supports several platforms or build types.

What is the difference between #ifdef and #if?

#ifdef NAME asks only whether the macro exists, regardless of its value. #if expression evaluates a constant integer expression, so it can compare values: #if VERSION >= 3. Use #ifdef for flags that are merely present or absent, #if when the value matters. #if defined(NAME) combines both and can be joined with && and ||.

How do I define a macro from the command line with gcc?

Use -D: gcc -DDEBUG program.c defines DEBUG as if the file began with #define DEBUG 1, and -DMAX=50 gives it a specific value. This is how build systems switch features on without editing the source, and -U NAME undefines one.

Why use #if 0 instead of commenting code out?

Because /* ... */ comments do not nest - the first */ inside the region ends the comment early and the rest becomes broken code. #if 0 ... #endif removes any amount of code containing any number of comments, keeps syntax highlighting alive, and is trivially flipped back to #if 1.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED