CConditional Compilation

Conditional Compilation

Conditional compilation lets you include or exclude blocks of source code before the compiler ever sees them, based on conditions the preprocessor can evaluate at build time. This is how the same codebase can compile differently for different platforms, build configurations, or feature sets.

The Directive Family

Directive

Meaning

#ifdef NAME

True if NAME has been #define-d (with any value, even empty)

#ifndef NAME

True if NAME has NOT been #define-d

#if expr

True if the constant integer expression expr evaluates to nonzero

#elif expr

Like else if, chained after #if/#ifdef

#else

Fallback branch if none of the above matched

#endif

Closes the conditional block

C
#include <stdio.h>

#define VERSION 2

int main(void)
{
#if VERSION == 1
    printf("Running version 1\n");
#elif VERSION == 2
    printf("Running version 2\n");
#else
    printf("Unknown version\n");
#endif
    return 0;
}
Common Uses
  • Platform-specific code — compile a different code path per operating system or compiler

  • Debug-only code — logging, assertions, or extra checks that should not exist in a release build

  • Feature flags — ship optional functionality that can be toggled on or off at compile time

C
#ifdef _WIN32
    #include <windows.h>
#elif defined(__linux__)
    #include <unistd.h>
#else
    #include <unknown_platform.h>
#endif
#if defined(X) vs #ifdef X

#ifdef X and #if defined(X) are equivalent — both test whether X has been defined. The #if defined(...) form has one practical advantage: because it is a normal expression, it composes with && and || to test several macros in a single condition, which plain #ifdef cannot do.

Note
`#ifdef DEBUG` can only test one name. `#if defined(DEBUG) && !defined(NDEBUG)` combines two conditions in one line — reach for `#if defined(...)` whenever you need more than a single check.

C
#if defined(DEBUG) && !defined(NDEBUG)
    #define LOG(msg) printf("[DEBUG] %s\n", msg)
#else
    #define LOG(msg)   /* expands to nothing in release builds */
#endif

int main(void)
{
    LOG("starting up");   /* real printf call in debug, no-op in release */
    return 0;
}

In a release build (where DEBUG is not defined), the LOG(...) macro expands to nothing at all — the call site simply disappears, with zero runtime cost. This pattern is extremely common for debug logging, assertions, and instrumentation that should never ship in production binaries.