C言語のソースコードを読んでいると#ifや#ifdefといった見慣れない記述が突然現れ、何をしているのか戸惑った経験はないでしょうか。
これらは条件付きコンパイルと呼ばれる仕組みで、OSやビルドモードごとに処理を切り替えたり、大規模プロジェクトの構成を柔軟に制御したりするために欠かせません。
本記事では、#if・#ifdef・#ifndefの違いから、実務での使い分け、注意点までを丁寧に解説します。
条件付きコンパイルとは
条件付きコンパイルとは何か

条件付きコンパイルとは、コンパイルの前処理段階で「この部分はコンパイルに含める」「この部分は捨てる」とソースコードを取捨選択する仕組みのことです。
C言語では、コンパイルの前にプリプロセッサ(前処理器)が動作し、#ifや#ifdefなどのディレクティブを解釈して、最終的にコンパイラに渡すソースコードを決定します。
このとき、条件が真(true)になった部分だけがコンパイルの対象となり、条件が偽(false)になった部分はコードとして存在しなかったことになります。
条件付きコンパイルが必要になる典型的な場面
条件付きコンパイルは、次のような場面で特に威力を発揮します。
1つ目に、OSやプラットフォームによって処理を切り替えたいときです。
たとえばWindowsとLinuxとでファイル操作APIが異なる場合、OSごとにソースコードを完全に分けてしまうと管理が大変になります。
そこで1つのソースファイルの中で、条件付きコンパイルによりOSごとに異なるコードだけを切り替えることができます。
2つ目に、デバッグ版とリリース版で機能を変えたいときです。
デバッグ時だけ詳細ログを出力したり、テスト用のコードを有効化したりして、本番ビルドではそれらを完全に取り除く、といった制御が簡単に行えます。
3つ目に、ライブラリの有無や機能フラグによる分岐です。
例えば、外部ライブラリがリンクされているときだけその機能を有効にし、未リンクなら代替処理を行う、といった柔軟な構成変更を実現できます。
条件付きコンパイルとプリプロセッサの関係

C言語のコンパイルは、以下のような段階で処理されます。
- プリプロセス(前処理)
- コンパイル
- アセンブル
- リンク
このうち条件付きコンパイルは1番目のプリプロセス段階で行われる処理です。
プリプロセッサは#includeや#defineなどのディレクティブも処理しますが、その中でもコードの一部を「そもそもコンパイル対象から外す」ことができるのが条件付きコンパイルです。
ここで重要なのは、条件付きコンパイルで除外されたコードはコンパイラからは「存在しなかったことになる」という点です。
そのため、未定義の関数呼び出しや構文エラーがあっても、条件が偽でコンパイル対象外になっていればエラーにはなりません。
条件付きコンパイル構文一覧
#ifと#elifの基本構文と評価ルール
#ifの基本構文
#ifは「整数定数式」を評価して真偽を判定するディレクティブです。
基本的な構文は次のようになります。
#if 条件式
/* 条件式が真(0以外)のときにコンパイルされるコード */
#else
/* 条件式が偽(0)のときにコンパイルされるコード */
#endif
条件式はコンパイル時に評価可能な整数定数式に限られます。
変数の値など、実行時に決まるものは使えません。
#elifによる多段分岐
#elifは「else if」に相当する追加条件です。
複数の条件を順番に評価し、最初に真となったブロックだけが有効になります。
#if MODE == 1
/* MODE が 1 のとき */
#elif MODE == 2
/* MODE が 2 のとき */
#else
/* 上記以外のとき */
#endif
ここでの評価ルールは、上から順に「まだ採用されていない最初の真の条件」が選ばれる、というものです。
複数の条件が真でも、先に現れたブロックだけが生き残る点に注意してください。
#ifのサンプルコード
#include <stdio.h>
// コンパイル時に MODE を指定することを想定
// 例: gcc -DMODE=1 sample.c -o sample
int main(void) {
#if MODE == 1
printf("MODE 1: デバッグモードです。\n");
#elif MODE == 2
printf("MODE 2: テストモードです。\n");
#else
printf("その他のモード: 通常実行です。\n");
#endif
return 0;
}
上記はコンパイルオプション-DMODE=1のように#define MODE 1を与えることで、ビルド時に動作を切り替える例です。
#ifdefと#ifndefの基本構文と使いどころ
#ifdefの基本構文
#ifdefは「指定したマクロが定義されているかどうか」を条件とするディレクティブです。
#ifdef マクロ名
/* マクロ名 が #define されている場合だけコンパイルされる */
#else
/* マクロ名 が 未定義の場合にコンパイルされる */
#endif
例えば、DEBUGというマクロが定義されているときだけデバッグ用ログを有効にする、といった用途によく使われます。
#ifndefの基本構文
#ifndefは「if not defined」の略で、指定したマクロが定義されていない場合にコードを有効にします。
#ifndef マクロ名
/* マクロ名 が 未定義の場合にコンパイルされる */
#endif
これはヘッダファイルの多重インクルード防止にほぼ必ず使われる構文です。
#ifndef MY_HEADER_H
#define MY_HEADER_H
/* ヘッダの内容 */
#endif /* MY_HEADER_H */
#ifdef / #ifndef の典型的な使いどころ
- 有無だけで十分なフラグ(例: DEBUG, USE_SSL など)
- ヘッダガード(多重インクルード防止)
- 外部ビルド設定との連携(ビルドシステムが定義したマクロのチェック)
値による分岐が不要で、存在するかしないかだけを見たいときは #ifdef / #ifndef を使うのが自然です。
#endifの役割とネスト時の注意点
#endifの役割
#endifは#if 系ディレクティブのブロックを終了するマーカーです。
#if、#ifdef、#ifndefなどを開いたら、必ず対応する#endifが必要です。
これが不足したり対応がずれたりすると、プリプロセス段階でエラーになります。
ネスト時の注意点
条件付きコンパイルは入れ子(ネスト)にして書くこともできます。
#if defined(OS_WINDOWS)
#if defined(USE_GUI)
/* Windows + GUI のとき */
#else
/* Windows + 非GUIのとき */
#endif
#else
/* 非Windows のとき */
#endif
ネストした場合、内側から順に閉じる必要があります。
また、可読性の観点から#endif にコメントを付けることが強く推奨されます。
#if defined(OS_WINDOWS)
/* ... */
#endif /* OS_WINDOWS */
このようにすることで、どの条件ブロックが閉じられているのかが一目で分かるようになります。
定数定義と#defineを使った条件分岐
#define による定数定義
条件付きコンパイルの多くは#defineと組み合わせて使います。
#defineはマクロ定義であり、名前付き定数としてもよく用いられます。
#define MODE 1
#define USE_FEATURE_X /* 値を持たないフラグ的マクロ */
#if MODE == 1
/* ... */
#endif
#ifdef USE_FEATURE_X
/* 機能Xを有効にしたコード */
#endif
値を持つマクロは#ifで、有無だけを表すマクロは #ifdef / #ifndefで使う、というパターンが多いです。
コンパイルオプションで定義する
ソースを変更せずにビルド構成を切り替えるには、コンパイラオプションでマクロを定義する方法が便利です。
例(GCCなどの場合):
gcc -DMODE=2 -DUSE_FEATURE_X main.c -o main
この場合、ソースコード内で
#if MODE == 2
/* ... */
#endif
#ifdef USE_FEATURE_X
/* ... */
#endif
といった条件が有効になります。
#if・#ifdef・#ifndefの使い分けガイド
#ifを使うべきケース
#ifは「値によって分岐したいとき」に適しています。
特に次のようなケースで有効です。
- 複数のモード番号やレベル値を切り替えるとき
- バージョン番号によって処理を変えるとき
- 数値の範囲比較をしたいとき
#define LOG_LEVEL 2 /* 0:なし, 1:エラーのみ, 2:詳細 */
#if LOG_LEVEL >= 2
#define LOG_DEBUG(msg) printf("[DEBUG] %s\n", msg)
#else
#define LOG_DEBUG(msg) /* 何もしない */
#endif
ここでは比較演算子や算術演算子を含む条件式が使えるため、柔軟な分岐が可能です。
#ifdefを使うべきケース
#ifdefは「そのマクロが定義されているかどうか」だけを見たいときに使います。
代表的なケースは次のとおりです。
- デバッグフラグの有無でログや検証コードを切り替える
- 外部ビルド設定(コンパイラやビルドシステムが定義)のチェック
- OSやライブラリの検出(例: _WIN32, __linux__ など)
#ifdef DEBUG
#define DBG_PRINT(msg) printf("[DEBUG] %s\n", msg)
#else
#define DBG_PRINT(msg) /* 何もしない */
#endif
このように「定義されているだけで十分な情報」であるときに有効です。
#ifndefを使うべきケース
#ifndefは主に次のケースで使います。
1つ目はヘッダガードです。
これは事実上の定番パターンです。
#ifndef MY_LIB_H
#define MY_LIB_H
/* ヘッダの中身 */
#endif /* MY_LIB_H */
2つ目はデフォルト値の設定です。
ビルドオプションや他のヘッダで定義されていない場合にだけ、標準の値を与える、といった使い方ができます。
#ifndef BUFFER_SIZE
#define BUFFER_SIZE 1024
#endif
このように書くことで、必要なら外部から上書き可能な「デフォルト設定」を定義できます。
マクロ未定義時の挙動とデフォルト値の考え方
未定義マクロを#ifで使ったらどうなるか
C規格では、未定義のマクロは整数定数0として扱われると規定されています。
例えば次のようなコードを考えます。
#if FOO
/* ... */
#endif
ここでFOOがどこにも定義されていない場合、FOOは0とみなされ、条件は偽になります。
この仕様は便利な反面、「本当は定義しておくべきマクロを定義し忘れても静かに0扱いされる」という落とし穴もあります。
デフォルト値の安全な設定パターン
この問題を避けるために、#ifndef で存在チェックをしてから #define するというパターンがよく使われます。
#ifndef MODE
#define MODE 0 /* デフォルト値 */
#endif
#if MODE == 0
/* ... */
#elif MODE == 1
/* ... */
#endif
こうしておくと、「MODE がどこでも定義されていなくても、必ず既知の値になる」ため、予期せぬ0扱いのリスクを減らせます。
条件式で使える演算子
#ifの条件式では、整数定数式に使える演算子の多くが利用可能です。
代表的なものをまとめます。
| 種類 | 例 | 説明 |
|---|---|---|
| 算術演算子 | +, -, *, /, % | 基本的な四則演算と剰余 |
| 比較演算子 | ==, !=, <, <=, >, >= | 大小比較・等価比較 |
| 論理演算子 | &&, ||, ! | AND・OR・NOT |
| ビット演算子 | &, |, ^, ~, <<, >> | ビットAND, OR, XOR, NOT, シフト演算 |
| その他 | defined | マクロ定義の有無を調べる |
特にdefined演算子は#if と #ifdef の橋渡しとして便利です。
#if defined(DEBUG) && LOG_LEVEL >= 2
/* デバッグかつ詳細ログレベル */
#endif
#ifdefだけでは書きにくい複雑な条件を、#if defined(...) && ...の形式で表現できます。
マクロによる環境切り替え

マクロを使えば、同じソースコードから複数の環境用バイナリを生成することができます。
例えば、次のようにOSごとに別の実装を用意し、_WIN32や__linux__といったマクロで切り替えます。
#include <stdio.h>
void print_os(void) {
#if defined(_WIN32)
printf("Windows 用ビルドです。\n");
#elif defined(__linux__)
printf("Linux 用ビルドです。\n");
#elif defined(__APPLE__)
printf("macOS 用ビルドです。\n");
#else
printf("未知のプラットフォームです。\n");
#endif
}
ここでは、コンパイラや標準ヘッダが自動的に定義してくれるマクロを利用しています。
また、独自の環境フラグ(例: USE_FAST_MATH)を-Dオプションで定義すれば、ビルドコマンドだけで挙動を切り替えられます。
実践テクニックと注意点
OS別・コンパイラ別の条件付きコンパイル例

OS別切り替えの実例
#include <stdio.h>
void show_path_separator(void) {
#if defined(_WIN32)
printf("Windows: パス区切りは '\\\\' です。\n");
#else
printf("Unix系: パス区切りは '/' です。\n");
#endif
}
int main(void) {
show_path_separator();
return 0;
}
(Windowsでの実行例)
Windows: パス区切りは '\' です。
(Linuxでの実行例)
Unix系: パス区切りは '/' です。
コンパイラ別の分岐
コンパイラごとに独自マクロが定義されているため、それを使って最適化オプションや拡張機能の有無を切り替えることもできます。
| コンパイラ | 主なマクロ例 |
|---|---|
| GCC/Clang | __GNUC__ |
| MSVC(Visual C++) | _MSC_VER |
| Intel C Compiler | __INTEL_COMPILER |
#if defined(_MSC_VER)
/* MSVC 固有の最適化や警告抑制 */
#elif defined(__GNUC__)
/* GCC/Clang 用の属性指定など */
#endif
このように、環境ごとの差異を1つのソースに吸収するために条件付きコンパイルが使われます。
デバッグ用ログ出力を条件付きコンパイルで制御

デバッグログは開発時には重要ですが、本番環境では不要どころか性能低下の原因にもなります。
条件付きコンパイルでデバッグログを制御するのが一般的です。
#include <stdio.h>
// DEBUG が定義されているときだけ DBG_PRINT が有効になる
#ifdef DEBUG
#define DBG_PRINT(fmt, ...) \
fprintf(stderr, "[DEBUG] " fmt "\n", ##__VA_ARGS__)
#else
#define DBG_PRINT(fmt, ...) \
/* 何もしないマクロ。実行時コストゼロ */
#endif
int main(void) {
DBG_PRINT("プログラム開始");
printf("Hello, world!\n");
DBG_PRINT("プログラム終了");
return 0;
}
(DEBUG を定義してビルドした場合の出力例)
[DEBUG] プログラム開始
Hello, world!
[DEBUG] プログラム終了
DEBUG未定義でビルドすると DBG_PRINT は空マクロに展開されるため、ログ出力処理そのものがコンパイル対象から外れます。
これにより、実行時オーバーヘッドを完全になくすことができます。
可読性を保つための条件付きコンパイルの書き方
条件付きコンパイルは非常に強力ですが、乱用すると「プリプロセッサ地獄」と呼ばれる読みづらいコードを生みます。
可読性を保つためのポイントをいくつか紹介します。
原則1: ネストを浅く保つ
入れ子が深くなると、どの条件がどこまで効いているのかが分かりづらくなります。
可能であれば1〜2段以内に抑えるよう心がけます。
原則2: #endif にコメントを付ける
#if defined(DEBUG) && LOG_LEVEL >= 2
/* ... */
#endif /* DEBUG && LOG_LEVEL >= 2 */
コメントを付けることで、長いブロックでも対応がすぐに確認できます。
原則3: できるだけ関数・ファイル単位で分ける
複雑な条件付きコンパイルは、関数ごと・ファイルごとに分割した方が読みやすくなります。
/* os_win.c */
void do_something(void) {
/* Windows 実装 */
}
/* os_linux.c */
void do_something(void) {
/* Linux 実装 */
}
このようにファイル分割し、ビルド設定でどのファイルをコンパイルするか選ぶ方法も有効です。
よくある間違い
間違い1: #if と #endif の対応ミス
#if DEBUG
printf("debug\n");
/* #endif を書き忘れ */
このようなミスはプリプロセッサエラーになります。
エディタの補完機能やフォーマッタを活用し、必ず対応させるようにします。
間違い2: 未定義マクロを想定せずに #if で比較
#if MODE == 1
/* ... */
#endif
ここでMODEが定義されていないと0 == 1と解釈され、条件は偽になります。
意図していない場合は#ifndef でデフォルト設定を行うか、#error を使って強制的にエラーにするのも有効です。
#ifndef MODE
#error "MODE マクロが定義されていません"
#endif
間違い3: 実行時条件と混同する
ifと#ifは似ていますが、前者は実行時の分岐、後者はコンパイル時の分岐です。
ユーザ入力や変数の値に応じて動的に処理を変えたい場合はifを使い、ビルドごとに固定の構成を切り替える場合に#ifなどを使います。
条件付きコンパイルとビルド設定の連携方法

コンパイラオプション -D の活用
多くのコンパイラは-Dオプションでマクロ定義を受け取ります。
これにより、ソースコードを編集せずにビルドごとに挙動を変えることが可能になります。
# デバッグ版ビルド
gcc -DDEBUG -DLOG_LEVEL=2 main.c -o main_debug
# リリース版ビルド
gcc -O2 main.c -o main_release
ソース側では次のように書きます。
#ifndef LOG_LEVEL
#define LOG_LEVEL 0
#endif
#if LOG_LEVEL >= 1
/* ログ出力用コード */
#endif
Makefile や CMake との連携
ビルドシステムからマクロを一括して設定することもよく行われます。
Makefile の例:
CFLAGS_DEBUG = -DDEBUG -DLOG_LEVEL=2 -g
CFLAGS_RELEASE = -O2 -DLOG_LEVEL=0
debug:
$(CC) $(CFLAGS_DEBUG) main.c -o main_debug
release:
$(CC) $(CFLAGS_RELEASE) main.c -o main_release
CMake の例:
add_executable(myapp main.c)
target_compile_definitions(myapp PRIVATE
$<$<CONFIG:Debug>:DEBUG;LOG_LEVEL=2>
$<$<CONFIG:Release>:LOG_LEVEL=0>
)
このように、「ビルド設定 → コンパイルオプション → 条件付きコンパイル」という流れで、プロジェクト全体の構成を柔軟に制御できます。
まとめ
条件付きコンパイルは、コンパイル前にソースコードの一部を取捨選択するための強力な仕組みです。
#ifは整数定数式による値の分岐、#ifdef / #ifndefはマクロ定義の有無による分岐に適しており、ヘッダガードやOS別実装、デバッグログの制御など、実務で欠かせない用途に広く使われています。
一方で、ネストしすぎや未定義マクロの扱いを誤ると可読性低下やバグの原因にもなります。
ビルド設定との連携や命名ルール、#endif コメントなどの工夫を取り入れ、意図が明確な条件付きコンパイルを心がけることで、保守性の高いC言語プログラムを構築できるようになります。
