閉じる

C言語の条件コンパイルを徹底解説(#if,#ifdefの使い分け)

現場では同じソースからデバッグ版や製品版、OS別のバイナリを作ることがよくあります。

こうした切り替えを可能にするのが条件コンパイルです。

本稿では#if と #ifdef の違いと使い分けを中心に、プリプロセッサの流れから実用的なサンプル、注意点までを丁寧に解説します。

初心者の方でも手を動かしながら理解できる構成です。

条件コンパイルの基本

条件コンパイルとは

条件コンパイルは、プリプロセッサ指令を使ってコンパイル時にソースコードの一部を有効・無効にする仕組みです。

実行時のif文とは異なり、実行ファイルに含めるかどうかをビルド時点で決めます。

これにより以下のようなニーズに対応できます。

デバッグログのON/OFF、OSやCPUアーキテクチャによる分岐、機能の段階的導入(Feature Toggle)、試験的コードの一時無効化などです。

ポイントは「コードがそもそもコンパイルされない」ため、無効化した部分は実行ファイルのサイズや実行速度に影響しないことです。

プリプロセッサの流れ

C言語のビルドは大まかに次の段階で進みます。

プリプロセス → コンパイル → アセンブル → リンクです。

条件コンパイルは最初のプリプロセス段階で実行されます。

ここで#ifなどの指令が評価され、該当しないコードは完全に取り除かれます。

つまり条件コンパイルは「ソースを作り替える」処理であり、コンパイラに渡る前に決着がつきます。

使う命令

条件コンパイルで頻出する主な指令と役割をまとめます。

どれも行頭で#から始まり、プリプロセッサが解釈します。

指令役割代表的な使い方
#if整数定数式(defined演算子を含む)で判定#if LOG_LEVEL >= 2
#ifdefマクロが定義済みかを判定#ifdef DEBUG
#ifndefマクロが未定義かを判定#ifndef NDEBUG
#elif追加の条件分岐#elif defined(OS_WIN)
#elseどの条件にも合わない場合#else
#endif条件ブロックの終端#endif

#if は「値で判定」し、#ifdef は「定義の有無で判定」するのが最大の違いです。

詳細は次章で解説します。

#if と #ifdef の違いと使い分け

#if の基本

#if整数定数式で判定します。

式中ではdefined(MACRO)を使って定義の有無を整数(1 or 0)として扱えます。

未定義の識別子は0として評価される点に注意します。

C言語
// #if の基本例
#if defined(LOG_LEVEL) && (LOG_LEVEL >= 2)
    // ここは LOG_LEVEL が 2 以上のときだけ有効
#endif

数値レベルや複雑な条件を扱うときは#ifが適切です。

#ifdef の基本

#ifdefマクロの定義有無だけを判定します。

値の大小や式の評価はできません。

反対条件は#ifndefです。

C言語
// #ifdef の基本例
#ifdef DEBUG
    // DEBUG が定義されているときだけ有効
#endif

#ifndef NDEBUG
    // NDEBUG が未定義のときだけ有効(典型的にはデバッグ有効時)
#endif

存在フラグ(スイッチ)のON/OFFには#ifdefが読みやすく便利です。

違いと選び方

ルールとしては次の指針が実務で扱いやすいです。

単純な有無だけなら#ifdef、段階や等号不等号を含む判定なら#ifを使います。

また#if defined(M)#ifdef Mは等価ですが、式の一部として書けるのは前者です。

たとえば#if defined(A) && !defined(B)のように複合条件が必要な場合は#ifを選びます。

#elif と #else の使い方

複数の候補から1つを選ぶ場合は#elifを連ね、最後に#elseでフォールバックを書くと読みやすくなります。

C言語
#if defined(OS_WIN)
    // Windows 向け
#elif defined(OS_MAC)
    // macOS 向け
#elif defined(OS_LINUX)
    // Linux 向け
#else
    // 未対応環境
#endif

#endif の役割

#endifは条件ブロックの終端を示します。

ネストが深くなると見落としやすいため、閉じタグにコメントを付ける習慣を持つと保守性が高まります。

C言語
#ifdef DEBUG
    // デバッグ用のコード
#endif // DEBUG

#endif の入れ忘れはコンパイルエラーや意図せぬコード無効化の原因になります

後述の注意点も参照してください。

基本の書き方とサンプル

ここでは単体でビルド・実行できるサンプルを示します。

同じソースでも与えるマクロ定義で出力が変わることを確認しましょう。

定数で切り替える

バージョンや段階的機能(フェーズ)を数値で管理し、#ifで分岐します。

C言語
// file: feature_version.c
#include <stdio.h>

// バージョン定数を定義(ビルド時に -DVERSION=... で上書きも可能)
#ifndef VERSION
#define VERSION 2
#endif

int main(void) {
    printf("App version: %d\n", VERSION);

#if VERSION >= 3
    // 先進機能
    printf("Enable: new rendering pipeline\n");
#elif VERSION == 2
    // 中間機能
    printf("Enable: optimized cache\n");
#else
    // 旧機能のみ
    printf("Enable: legacy mode\n");
#endif

    return 0;
}

出力例(デフォルトのVERSION=2の場合):

App version: 2
Enable: optimized cache

ビルド時に定数を変える例(GCC/Clang):

Shell
# VERSION=1 としてビルド・実行
cc feature_version.c -o feature_version -DVERSION=1
./feature_version
実行結果
App version: 1
Enable: legacy mode

フラグで切り替える

フラグの有無でON/OFFを切る場合は#ifdefが簡潔です。

C言語
// file: debug_flag.c
#include <stdio.h>

// デバッグを有効にしたい場合はビルド時に -DDEBUG を付ける
int main(void) {
    puts("Start processing");

#ifdef DEBUG
    // DEBUG が定義されているときだけ出力される
    puts("[DEBUG] intermediate value = 42");
#endif

    puts("Done");
    return 0;
}

出力例(フラグ無し):

実行結果
Start processing
Done

コンパイル時にフラグを渡した場合:

Shell
cc debug_flag.c -o debug_flag -DDEBUG
./debug_flag
実行結果
Start processing
[DEBUG] intermediate value = 42
Done

複数条件を分岐する

OSやプラットフォーム別にコンパイル内容を切り替える典型例です。

ここでは学習用に任意のフラグOS_WINなどを自分で与える形にしています(実環境ではコンパイラが用意する_WIN32__linux__などを使います)。

C言語
// file: platform_switch.c
#include <stdio.h>

int main(void) {
#if defined(OS_WIN)
    puts("Platform: Windows");
#elif defined(OS_MAC)
    puts("Platform: macOS");
#elif defined(OS_LINUX)
    puts("Platform: Linux");
#else
    puts("Platform: Unknown");
#endif
    return 0;
}

ビルド例:

Shell
# Windowsとして
cc platform_switch.c -o platform_switch -DOS_WIN
./platform_switch

# Linuxとして
cc platform_switch.c -o platform_switch -DOS_LINUX
./platform_switch

出力例(Windowsとしてビルド):

実行結果
Platform: Windows

出力例(Linuxとしてビルド):

実行結果
Platform: Linux

一時的にコードを無効化する

#if 0は「巨大なコメント」のように使えます。

レビュー中に一時無効化したいときに便利です。

C言語
// file: temporary_disable.c
#include <stdio.h>

int compute(void) {
#if 0
    // 試験中のアルゴリズム(まだ遅いので無効化)
    return 100; // TODO: 高速化ののち再有効化
#else
    // 安定版
    return 42;
#endif
}

int main(void) {
    printf("result=%d\n", compute());
    return 0;
}
実行結果
result=42

#if 0 の内側は「コンパイルされない」ため、型チェックや未使用警告も届きません

長期間放置は避け、必ずTODOや期限を記しておきます。

コンパイル時にフラグを渡す

ビルドコマンドでマクロを定義すると、ソースを書き換えずに振る舞いを切り替えられます。

GCC/Clangなら-D、MSVCなら/Dです。

C言語
// file: build_flags.c
#include <stdio.h>

#ifndef LOG_LEVEL
#define LOG_LEVEL 1 // 既定のログレベル(1=INFO, 2=DEBUG)
#endif

int main(void) {
    puts("Start");

#if LOG_LEVEL >= 2
    puts("[DEBUG] detail A");
    puts("[DEBUG] detail B");
#endif

    puts("Finish");
    return 0;
}

デフォルト(定義なし):

実行結果
Start
Finish

ビルド時に与える例:

Shell
# 詳細ログを有効化
cc build_flags.c -o build_flags -DLOG_LEVEL=2
./build_flags
実行結果
Start
[DEBUG] detail A
[DEBUG] detail B
Finish

ソースはそのままに、ビルド引数だけで動作を切り替えられるのが条件コンパイルの大きな利点です。

よくある用途と注意点

デバッグログのON/OFF

開発中は大量のログが役立ちますが、製品版では邪魔になることがあります。

#ifdef DEBUGで絞り込み、さらにLOG_LEVELの数値で段階制御するのが定石です。

マクロを薄いラッパとして整備すると呼び出し側がすっきりします。

C言語
// 例: ログ用マクロ
#if defined(DEBUG) && (LOG_LEVEL >= 2)
  #define DBG_PRINT(msg) puts(msg)
#else
  #define DBG_PRINT(msg) ((void)0) // 空展開(副作用なし)
#endif

呼び出し側は DBG_PRINT(“…”) と書くだけで、ビルド種別に応じて自動的に有効・無効が切り替わります。

環境やOSで分岐する

実運用ではコンパイラが環境マクロを提供します。

例として_WIN32(Windows)、__linux__(Linux)、__APPLE__(Appleプラットフォーム)などがあります。

可搬性のためOS専用APIの呼び出しは条件コンパイルで囲むと安全です。

代表的マクロ意味(代表例)
_WIN32Windows(32/64両方)
_WIN64Windows 64bit
__linux__Linux
__APPLE__Appleプラットフォーム(macOS/iOS系)
__GNUC__GCC系コンパイラ
_MSC_VERMSVCコンパイラ

実コード例:

C言語
#if defined(_WIN32)
    // Windows固有の処理
#elif defined(__APPLE__)
    // Apple系固有の処理
#elif defined(__linux__)
    // Linux固有の処理
#else
    // 既定処理または未対応
#endif

#endif の入れ忘れに注意

#endif の抜けはビルドエラーや広範囲の無効化につながる致命的ミスです。

対策として次をおすすめします。

閉じ#endifに短いコメントを付ける、IDEのプリプロセッサ可視化やフレーム表示を有効にする、ネストを浅く保つ、CIで-Werrorなどの厳格な警告を有効化する、です。

条件は整数で判定

#ifが扱えるのは整数定数式のみです。

浮動小数点や文字列比較はできません。

どうしても文字列で切り替えたい場合は文字列を整数にマップするか、definedと個別のフラグに分解します。

悪い例:

C言語
// NG: 文字列比較は不可
// #if MODE == "fast"

良い例:

C言語
// OK: 整数で比較
#if MODE == 2

未定義マクロは0扱い

#if の式中で未定義の識別子は 0 として扱われます

これは便利な半面、スペルミスに気づきにくい原因にもなります。

C言語
// LOGLEVEL と書くべきところを LOG_LEVL と誤記
#if LOG_LEVL >= 2   // 未定義 -> 0 >= 2 -> 偽
    // ここは実行されない(気づきにくい)
#endif

対策として原則 #if defined(MACRO) を併用し、綴りミスを早期に発見できるようにしましょう。

C言語
#if defined(LOG_LEVEL) && (LOG_LEVEL >= 2)
    // 正しく判定
#endif

ネストを浅くする

条件コンパイルは強力ですが、ネストし過ぎると可読性が急落します。

可能ならガード節パターンで早めに枝刈りし、#if/#endifの入れ子を避けます。

C言語
// ガード節: まず対象外を落とす
#if !defined(ENABLE_FEATURE)
  // 何もしない(または互換実装)
#else
  // 機能の本体
#endif

この形にすると有効経路のコードが左に寄り、読みやすくなります。

可読性を保つ書き方

可読性の秘訣は次の通りです。

マクロ名は役割が分かるよう接頭辞(例: OS_, FEAT_, CFG_)を付ける、ブロックの終わりに#endif // 名前の形でコメントを入れる、プロジェクトの「ビルド時に与えるマクロ一覧」をドキュメント化する、値を持つマクロは範囲や意味をコメントで明記する、などです。

また条件式は短く・平易に保ち、可能なら関心ごとごとに#includeされたモジュールへ分離するのが保守性に優れます。

まとめ

本稿では#if と #ifdef の違いと使い分けを中心に、プリプロセッサの動作、よくある実装パターン、落とし穴までを解説しました。

要点は次の通りです。

単純な有無は#ifdef、数値や複合条件は#ifを用いる、#elif/#else/#endifで分岐を整理する、-D/Dでビルド時に柔軟に切り替える、未定義識別子は0扱いであることに注意する、です。

条件コンパイルは実行コストなしで振る舞いを変えられる強力な道具です。

過剰なネストを避け、意図と範囲をコメントで明確にしながら、堅牢で読みやすいコードベースを育てていきましょう。

この記事を書いた人
エーテリア編集部
エーテリア編集部

プログラミングの基礎をしっかり学びたい方向けに、C言語の基本文法から解説しています。ポインタやメモリ管理も少しずつ理解できるよう工夫しています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!