Skip to content

Intermediate · The preprocessor, multi-file programs and make · topic 37 of 60 · 20 min

Conditional compilation and include guards

Explanation

#if, #ifdef, #ifndef, #else and #endif keep or remove code before compilation, so one source can build debug and release versions or support Linux and Windows. Define flags in code or on the command line with gcc -DDEBUG. Every header needs an include guard (#ifndef NAME_H / #define NAME_H / #endif, or #pragma once) so including it twice does not cause errors.

Keeping or removing code

c
#define DEBUG 1

#if DEBUG
    printf("x = %d\n", x);          /* compiled only when DEBUG is non-zero */
#endif

#ifdef USE_SMS                      /* true if USE_SMS is defined at all */
    send_sms(phone, text);
#else
    send_email(email, text);
#endif

Defining flags from the command line

bash
gcc -DDEBUG prog.c -o prog          # like #define DEBUG 1
gcc -DMAX_USERS=500 prog.c -o prog  # like #define MAX_USERS 500
gcc -DNDEBUG -O2 prog.c -o prog     # NDEBUG turns off assert() (module 11)
Build systems pass different -D flags for debug and release builds.

Platform checks

c
#if defined(_WIN32)
    #include <windows.h>
    #define PATH_SEP '\\'
#elif defined(__linux__) || defined(__APPLE__)
    #include <unistd.h>
    #define PATH_SEP '/'
#else
    #error "Unsupported platform"     /* stops compilation with a message */
#endif

Compilers predefine macros that describe the target: _WIN32, __linux__, __APPLE__, __x86_64__, __arm__, and __STDC_VERSION__ (for example 201710L for C17). Use them sparingly; keep platform-specific code in small, separate files where possible.

Checking the C standard version

c
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L
    /* C23: bool, true and false are keywords */
#else
    #include <stdbool.h>
#endif

_Static_assert(sizeof(int) >= 4, "this code assumes at least 32-bit int");   /* C11 compile-time check */
Version checks let one header work with old and new compilers; static assertions stop the build when an assumption is false.

Include guards

If two headers both include "money.h", and a .c file includes both, money.h would be pasted twice, and its struct definitions would be duplicated, which is a compile error ("redefinition of struct Money"). An include guard makes the second inclusion empty.

c
/* money.h */
#ifndef MONEY_H
#define MONEY_H

typedef struct { long paise; } Money;
Money money_add(Money a, Money b);

#endif /* MONEY_H */
c
/* the shorter, non-standard but widely supported alternative */
#pragma once
typedef struct { long paise; } Money;
#pragma once is supported by gcc, clang and MSVC, but it is not part of the C standard.
Careful: Using #if to comment out code (#if 0 … #endif) is fine for a quick experiment, but large blocks of disabled code rot: they stop compiling and confuse readers. Delete unused code and rely on version control (Git) to bring it back. Also avoid scattering #ifdef through business logic; it makes code hard to test because some branches are never compiled.
Think of it like this: Conditional compilation is like a tailor's pattern with notes: "for the kids' size, cut along the dotted line; for adults, the solid line". One pattern, different garments, chosen before cutting begins.
Tip: Name guards after the file path to keep them unique: PROJECT_BILLING_TAX_H. Clashing guard names make one header silently disappear.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Conditional compilation and include guards · 1/6

Conditional compilation

In this lesson you will include or exclude code before compilation.

Prefer a video by a teacher? Find free videos on “c conditional compilation ifdef include guards pragma once” on YouTube ↗

In real life: Debug logging that costs nothing in production

Firmware for a medical weighing scale prints detailed sensor values during development but must be as small and fast as possible in the shipped device. A DEBUG_LOG macro expands to a printf in debug builds and to nothing at all in release builds.

c
#ifdef DEBUG
  #define DEBUG_LOG(...) fprintf(stderr, __VA_ARGS__)
#else
  #define DEBUG_LOG(...) ((void) 0)
#endif

DEBUG_LOG("raw=%d filtered=%d\n", raw, filtered);   /* gone entirely unless built with -DDEBUG */

Practice (0/3 done)

Task 1 · Question

What does this print when compiled WITHOUT -DPRO? #ifdef PRO puts("Pro"); #else puts("Free"); #endif

Task 2 · Question

Why do headers need include guards?

Task 3 · Practice

Create config.h with an include guard and a #ifdef DEBUG section that defines a LOG macro (printing) or an empty LOG macro otherwise. Include config.h twice from main.c (directly and through another header) to prove the guard works. Build once with -DDEBUG and once without, and compare the output.

Quick quiz

Question 1

1.What does gcc -DVERBOSE do?

Question 2

2.What does #error do?

Question 3

3.Is #pragma once part of the C standard?

Question 4

4.Which macro is predefined when compiling for Windows?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What does gcc -DVERBOSE do?

Answer: Defines the macro VERBOSE (as 1)

Like #define VERBOSE 1.

What does #error do?

Answer: Stops compilation with a message

Used for unsupported configurations.

Is #pragma once part of the C standard?

Answer: No, but most compilers support it

Include guards are the standard way.

Which macro is predefined when compiling for Windows?

Answer: _WIN32

_WIN32 is defined for 32- and 64-bit Windows targets.

What is the difference between #include "x.h" and #include <x.h>?Related

Answer: "" searches the project's folders first; <> searches the system include folders

Use "" for your own headers.

From Splitting a program into files

When does macro replacement happen?Related

Answer: Before compilation, in the preprocessor

It is a text step.

From The preprocessor and macros

Which stage expands #include and #define?Related

Answer: Preprocessing

gcc -E shows the preprocessor output.

From Compiling with gcc

What does #pragma once at the top of a header do?Related

Answer: Stops the header from being included more than once in the same .cpp file

It is an include guard: repeated #includes of that header are skipped.

From Functions: declare, define, call and return

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 3 more topics in “The preprocessor, multi-file programs and make” before its exam: Splitting a program into files, Building with make, Libraries: static and shared.
  2. Module 8 exam: Preprocessor, multi-file programs and make · 12 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Bits, undefined behaviour and safety. 60 topics left in the course before the final exam.

← Previous: The preprocessor and macros