閉じる

C言語の条件コンパイル#if/#ifdefの違いと使い方まとめ

C言語の条件コンパイルは、ビルド時の条件に応じてコンパイルするコードを切り替える仕組みです。

OSやビルドモードの違いに合わせて処理を分岐したいときに役立ちます。

本記事では#if#ifdefを中心に、違いと正しい使い方、ありがちな落とし穴、ビルド指定の方法を初心者向けに丁寧に解説します。

条件コンパイル入門

何ができるか

条件コンパイルを使うと、コンパイル時の条件に応じてソースコードの一部を有効化または無効化できます

これは実行時のif分岐とは違い、不要なコードはそもそもバイナリに含まれません。

デバッグ出力の有無やOSごとの処理切り替え、試験的機能のオンオフなどに向いています。

基本の流れ

Cのビルドはおおむね次の順で進みます。

プリプロセッサ→コンパイラ→リンカです。

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

つまり#if#ifdefで囲まれたコードは、この段階で「残す」「捨てる」が確定します。

マクロは#defineでソース内に書くか、ビルド時に-Dで与えます。

よくある用途

代表的には次のような使い方があります。

デバッグビルドでは詳細ログを出すがリリースでは消す、WindowsとLinuxでAPIが違う部分だけ切り替える、軽量版とフル機能版で機能を限定するなどです。

不要なコードをビルド対象から外せるため、サイズや依存関係を抑えられます。

#ifと#ifdefの違い

#if

#if定数式を評価して真偽を決めます。

式の中ではマクロが展開され、数値の大小比較や論理演算が使えます。

未定義の識別子は0として扱われますが、コンパイラの警告対象になることがあるため安全に書くにはdefinedを併用します。

例えば#if LOG_LEVEL >= 2のように数値で段階を切ることができます。

#ifdef

#ifdefそのマクロが定義されているかどうかだけを判定します。

値が0であっても「定義されていれば真」です。

0に定義しても#ifdefは有効とみなされるため注意してください。

値ではなく存在の有無で分岐したいときに使います。

#ifndef

#ifndef未定義なら真になる逆バージョンです。

ヘッダの二重インクルードを防ぐインクルードガードでよく使われますが、インクルードガードの詳細は別記事で扱います。

defined(MACRO)の基本

defined(MACRO)#ifや#elifの式の中で、マクロの定義有無を0または1で返します。

#if defined(DEBUG)#ifdef DEBUGと同等で、複合条件が書けます。

例えば#if defined(DEBUG) && LOG_LEVEL > 1のように組み合わせられます。

以下の表で違いをまとめます。

ディレクティブ/書き方主な判定真になる条件0に定義した場合典型例
#ifdef MAC定義の有無MACが定義済み#ifdef DEBUG
#ifndef MAC定義の有無MACが未定義#ifndef INCLUDED_H
#if EXPR定数式EXPRが非0値による#if LOG_LEVEL >= 2
#if defined(MAC)定義の有無MACが定義済み#if defined(_WIN32)

条件コンパイルの書き方と例

#ifの例

数値マクロで段階的にログ出力を切り替える例です。

デフォルト値を#ifndefで補い、#if/#elifで分岐します。

C言語
// log_level.c
#include <stdio.h>

// LOG_LEVELが未定義ならデフォルトの1にする
#ifndef LOG_LEVEL
#define LOG_LEVEL 1
#endif

int main(void) {
    // マクロの値はコンパイル時に決まり、ここでは文字列に展開される
    printf("LOG_LEVEL=%d\n", LOG_LEVEL);

    // 段階的にログを切り替える
#if LOG_LEVEL >= 2
    printf("[VERBOSE] 詳細なログを出力します。\n");
#elif LOG_LEVEL == 1
    printf("[INFO] 通常のログを出力します。\n");
#else
    printf("[SILENT] ログを出力しません。\n");
#endif

    // 非0かどうかの単純判定
#if LOG_LEVEL
    printf("LOG_LEVELが非0なので、この行は出力されます。\n");
#endif

    return 0;
}
実行結果デフォルト、LOG_LEVEL=1
LOG_LEVEL=1
[INFO] 通常のログを出力します。
LOG_LEVELが非0なので、この行は出力されます。
実行結果数値指定、-DLOG_LEVEL=2
LOG_LEVEL=2
[VERBOSE] 詳細なログを出力します。
LOG_LEVELが非0なので、この行は出力されます。
実行結果数値指定、-DLOG_LEVEL=0
LOG_LEVEL=0
[SILENT] ログを出力しません。

コンパイル方法の例は後述の「-Dでマクロを定義する」を参照してください。

#ifdefの例

存在有無で機能を有効化する例です。

#ifdefは値が0でも「定義されていれば真」になることを確認します。

C言語
// feature_toggle.c
#include <stdio.h>

// FEATURE_Xが定義されているときだけ関数を用意する
#ifdef FEATURE_X
static void feature_x(void) {
    printf("Feature X が有効です!\n");
}
#endif

int main(void) {
#ifdef FEATURE_X
    feature_x(); // 関数が存在するのはFEATURE_Xが定義されるビルドだけ
#else
    printf("Feature X は無効です。(-DFEATURE_X で有効化)\n");
#endif
    return 0;
}
実行結果通常ビルド
Feature X は無効です。(-DFEATURE_X で有効化)
実行結果-DFEATURE_X を付けてビルド
Feature X が有効です!
注意

#define FEATURE_X 0としても#ifdef FEATURE_Xは真です。

値で切り替えるなら#if FEATURE_Xを使うか、#if defined(FEATURE_X) && FEATURE_Xのように書きます。

#elif/#elseの使い方

複数の候補からいずれか1つだけを選ぶには#elif#elseを連ねます。

以下はプラットフォームごとにメッセージを切り替える例です。

C言語
// platform_message.c
#include <stdio.h>

int main(void) {
#if defined(_WIN32)
    printf("Windows向けの処理を実行します。\n");
#elif defined(__APPLE__)
    printf("Apple/macOS向けの処理を実行します。\n");
#elif defined(__linux__)
    printf("Linux向けの処理を実行します。\n");
#else
    printf("未知のプラットフォームです。\n");
#endif
    return 0;
}
実行結果Linuxでのビルド想定
Linux向けの処理を実行します。
実行結果Windowsでのビルド想定
Windows向けの処理を実行します。
補足

これらのOSマクロは多くの場合コンパイラが自動で定義します。

通常は自分で-D__linux__のように指定しません。

プラットフォーム切替

プラットフォームで使える代表的なマクロは次のとおりです。

自動定義されるマクロを前提にし、必要であれば自前の抽象化マクロに集約して使うのが保守しやすいです。

目的よく使うマクロの例
Windows_WIN32
Linux__linux__
macOS__APPLE__
Unix系全般__unix__

推奨パターンとして、自前の統一マクロを定義し、以降のコードではそれだけを見る方法があります。

C言語
// os_detect.h (抜粋)
// ここでは「検出」と「抽象化」を一箇所に集約する
#if defined(_WIN32)
  #define OS_WINDOWS 1
#elif defined(__APPLE__)
  #define OS_MAC 1
#elif defined(__linux__)
  #define OS_LINUX 1
#else
  #define OS_UNKNOWN 1
#endif

以降の各所では#if defined(OS_WINDOWS)のように分岐すれば、将来別OSを足す際も変更点が限定されます。

ビルド指定と注意点

-Dでマクロを定義する

ビルド時の-D指定は最も手軽な外部設定手段です。

GCC/ClangとMSVCでの指定方法は次のとおりです。

// GCC/Clang
cc -DDEBUG main.c -o app               // DEBUGを定義
cc -DLOG_LEVEL=2 log_level.c -o app    // 数値で定義
cc -DFEATURE_X feature_toggle.c -o app // 存在で切り替え

// MSVC
cl /DDEBUG main.c
cl /DLOG_LEVEL=2 log_level.c
cl /DFEATURE_X feature_toggle.c

ビルドツールを使う場合の例です。

CMake
# CMake
target_compile_definitions(myapp PRIVATE DEBUG LOG_LEVEL=2)

文字列を渡す場合はシェルのクォートに注意します。

-DNAME="Alice"

#endifの閉じ忘れ対策

プリプロセッサのブロックは必ず閉じる必要があります。

閉じ忘れは見つけづらいので、対応する条件を#endifの行末コメントに書く習慣がおすすめです。

C言語
#if defined(DEBUG)
// デバッグ用コード...
#endif // defined(DEBUG)

長いファイルではセクションごとに空行やコメントで区切って視認性を上げます。

ネストしすぎない

条件コンパイルを深くネストすると読みづらくなります。

次のような入れ子は避けましょう。

C言語
// 悪い例: 条件が入り組んでいて読みづらい
#if defined(_WIN32)
  #if defined(DEBUG)
    // ...
  #else
    // ...
  #endif
#else
  #if LOG_LEVEL > 1
    // ...
  #endif
#endif

より良い書き方は、中間の「意味のあるマクロ」を導入して平坦化することです。

C言語
// まず集約マクロを作る
#if defined(_WIN32)
  #define PLATFORM_WINDOWS 1
#endif

#if defined(DEBUG) || LOG_LEVEL > 1
  #define NEED_VERBOSE_LOG 1
#endif

// 以降は意味論に基づく分岐だけにする
#if defined(PLATFORM_WINDOWS)
// ...
#endif

#if defined(NEED_VERBOSE_LOG)
// ...
#endif

条件はシンプルに

式が複雑だと誤解や警告を招きます。

特に未定義マクロを#if式で直接使うと、多くのコンパイラは警告します。

安全な書き方を心がけましょう。

C言語
// 悪い例: FOOやBARが未定義だと警告や意図しない0扱い
#if FOO && (BAR > 1)
#endif

// 良い例1: defined()で存在を確認してから値を見る
#if defined(FOO) && (FOO) && defined(BAR) && (BAR > 1)
#endif

// 良い例2: 事前にデフォルト値を与えておく
#ifndef FOO
  #define FOO 0
#endif
#ifndef BAR
  #define BAR 0
#endif
#if FOO && (BAR > 1)
#endif

また、値で切り替えたい場合は#ifを、存在有無なら#ifdef/#ifndefを使うと読み手に意図が伝わりやすくなります。

まとめ

条件コンパイルは、コンパイル時にコードを選別して最終バイナリから不要な部分を取り除ける強力な仕組みです。

数値判定ができる#if、存在有無を見る#ifdef/#ifndef、そしてdefined(MACRO)を適所で使い分けることで、デバッグ切り替えやプラットフォーム分岐を明瞭に記述できます。

ビルド時の-D指定も活用しつつ、ネストを浅く保ち、条件をシンプルにするのが保守のコツです。

なお、定数マクロや関数風マクロ、インクルードガードは本記事の範囲外ですので、別の記事で詳しく解説します。

C言語 プリプロセッサとマクロ
この記事を書いた人
エーテリア編集部
エーテリア編集部

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

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

URLをコピーしました!