プリプロセッサ | Programming Place Plus C言語編 第23章

トップページC言語編

このページの概要

以下は目次です。


プリプロセス(前処理)

ソースファイルをコンパイルする処理の手前には、プリプロセス(前処理) (preprocess) という段階があります。プリプロセスは、ソースコードをコンパイルする前に、ソースコードに変化を加える過程です。

コンパイルを実行するプログラムのことをコンパイラというように、プリプロセスを実行するプログラムのことを、プリプロセッサ (preprocessor) と呼びます。

【上級】プリプロセッサとコンパイラが完全に独立しているか、コンパイラの一部として動作するのかは、そのコンパイラにもよりますが、仕組みとしては独立して考えるべきものです。

これまで何度も使ってきた #include のように、先頭が # で始まる行は、プリプロセスで処理が行われることを表しています。このような行を、プリプロセッサディレクティブ(プリプロセッサ指令、前処理指令) (preprocessor directive) と呼ばれます。#include であれば、#includeディレクティブとか、#include前処理指令などと呼びます。

次のサンプルプログラムを使って、#include によって行われることを確認しておきましょう。

// main.c
#include <stdio.h>

int main(void)
{
    puts("Hello");
}

いきなり違う話になってしまいますが、プリプロセスのさらに前段階として、コメントが1つの空白文字に置き換えられます

コメントはソースコードから完全に消えているのではなく、空白文字に置き換わります。たとえば、ret/**/urnreturn と同じではなく、ret urn と書いたことになります。

【上級】コメントを置き換えるよりもさらに手前で行われる処理もありますが1、意識することがほとんどないのと、この章の内容とは関係しないので省略します。

したがって、次のように変換されます。

 
#include <stdio.h>

int main(void)
{
    puts("Hello");
}

次に、プリプロセスの処理が行われます。# で始まるプリプロセッサディレクティブが処理され、ソースコードに変化が起こります。#include であれば、指定したヘッダの内容をその行に取り込みます。

 
(stdio.h の中身がここに取り込まれる)

int main(void)
{
    puts("Hello");
}

これが、プリプロセスを終えた後のソースコードの状態です。このようにソースファイルに対して、プリプロセスの処理を実行し終えたものを、翻訳単位 (translation unit) と呼びます。

このあとの過程で、翻訳単位がコンパイラに渡されてコンパイルされます。このプログラムの場合、puts関数を呼び出している箇所があるので、みえる場所に puts関数の宣言がなければなりませんが(第9章)、それは stdio.h の中身に書かれているので、正常にコンパイルできます。

【上級】プリプロセスとコンパイルの間にもまだ処理がありますが1、ここも意識することはまずないので省略します。

使用している環境によっては、プリプロセス後のコードを確認する方法があります。Visual Studio の場合の方法についての説明が、こちらのページにあります。

#define

#include 以外のプリプロセッサディレクティブの例として、#defineディレクティブ (#define directive) があります。

#define は、第10章で紹介したオブジェクト形式マクロを定義するために使われます。また、第28章で説明する関数形式マクロ (function-like macro) を定義するためにも使われます。オブジェクト形式マクロや関数形式マクロを、単にマクロ (macro) と呼ぶこともあります。

いずれにしても、#define が行うことは、ソースコード上の特定の文字の並びを、別の特定の文字の並びに置き換えることです。この置き換えのことを、マクロ置き換え(マクロ置換) (macro replacement) と呼びます。

#define によるオブジェクト形式マクロは、次のように記述します。

#define マクロ名 置換後の文字の並び

「置換後の文字の並び」は空にしても構いません。

ここでの「文字の並び」というのは、“abcde” のような文字列リテラルのことを指しているのではなく、ソースコード上に登場する単なる文字のことです。たとえば、int は「i」「n」「t」という 3つの文字の並びということになります。

#define はプリプロセッサディレクティブですから、プリプロセスの処理の中で置換されます。実際にオブジェクト形式マクロを使ったプログラムを見てみましょう。

#include <stdio.h>

int main(void)
{
    #define INPUT_COUNT 5       // 入力させる回数

    printf("Please enter the integer %d times.\n", INPUT_COUNT);

    int sum = 0;

    for (int i = 0; i < INPUT_COUNT; ++i) {
        char buf[40];
        int num;

        fgets(buf, sizeof(buf), stdin);
        sscanf(buf, "%d", &num);
        sum += num;
    }

    printf("sum: %d\n", sum);
    printf("average: %f\n", (double)sum / (double)INPUT_COUNT);
}

実行結果:

Please enter the integer 5 times.
9   <-- 入力した内容
4   <-- 入力した内容
7   <-- 入力した内容
-8   <-- 入力した内容
6   <-- 入力した内容
sum: 18
average: 3.600000

#define を使い、INPUT_COUNT というマクロを定義しています。置換後の文字の並びは 5 です。この定義よりも後続の行に現れる INPUT_COUNT という文字の並びは 5 に置換されます。したがって、プリプロセスを終えた後の状態は、次のようになります。

(※ここに、stdio.h の内容がある)

int main(void)
{




    printf("Please enter the integer %d times.\n", 5);

    int sum = 0;

    for (int i = 0; i < 5; ++i) {
        char buf[40];
        int num;

        fgets(buf, sizeof(buf), stdin);
        sscanf(buf, "%d", &num);
        sum += num;
    }

    printf("sum: %d\n", sum);
    printf("average: %f\n", (double)sum / (double)5);
}

単なる 5という整数をソースファイル内にばらまくと、後から変更することは大変な作業になります。どこか1箇所でも書き換えることを忘れれば、すぐさまバグに直結することになるでしょう。マクロを使用すれば、#define に与えた 5 という数値のところだけを書き換えれば、すべてを一斉に漏れなく変更できます。また、第10章で解説したとおり、マジックナンバーを避ける意味があります。

【C++プログラマー】マクロは可能なかぎり避けるべきであるという考え方は変わりませんが、C言語には constexpr変数がないので、#define を使うことにそれなりに必要性があります。

ただし、プリプロセスの段階で処理されるものであるため、スコープなどのC言語の文法ルールの枠外にあることには注意が必要です。マクロ定義を関数の外側に書くことも、内側に書くこともできますが、関数の内側に書いたからといって、その効果が関数内にとどまることはなく、とにかくマクロ定義の記述よりも後ろであれば、どこまでも効果が及びます。

【上級】このことは、ヘッダファイル(第24章)にマクロ定義を書いた場合、そのヘッダファイルをインクルードしたすべてのファイルに影響を及ぼすことを意味しています。影響範囲が非常に大きくなることには注意が必要です。マクロ名にだけ、すべて大文字にするルールを適用するのは、思わぬマクロ置換に備えるためでもあります。

マクロ置換は、本当に強力な機能です。強力過ぎて、とんでもないことも可能になってしまうため、使い方には注意が必要です。たとえば、次のようなマクロも作れます。

#define int float

このマクロによって、ソースコード上に現れる intfloat に置換されます。このようなマクロ置換は、プログラムを解読不能なものへと変貌させてしまいます。いうまでもなく、こういうことは避けるべきです。

マクロを無効化する (#undef)

#define によるマクロ置換はプリプロセスで行われるため、スコープの概念がなく、{ } で囲ったとしても、関数内で定義したとしても、思いがけないところにまで影響を与えます。

#include <stdio.h>

void func(void);

int main(void)
{
    #define FUNC_NAME   "main"

    puts(FUNC_NAME);
    func();
}

void func(void)
{
    #define FUNC_NAME   "func"

    puts(FUNC_NAME);
}

main関数の内側で定義した FUNC_NAME マクロは、func関数の中にまで影響を及ぼします。

ここで、func関数の中にも FUNC_NAME が定義されています。C言語の規格上、同じ名前のオブジェクト形式マクロの定義が2度現れた場合、置換結果がまったく同じであれば問題ないことになっています。2このプログラムのように、置換結果が異なっている場合はエラーとなる可能性があります(Visual Studio 2015 では警告されますが、別の定義として機能します)。


#define の効力は、#undefという別のプリプロセッサディレクティブで打ち消せます。#undef は、次のような形式で記述します。

#undef マクロ名

「マクロ名」に指定した名前と同じ名前のマクロの効力は、#undef の記述がある位置で無効化されます。存在しないマクロの名前を指定した場合は何も起こりません。

さきほどのサンプルプログラムは、#undef を使って、マクロの効力をそれぞれの関数内部に閉じ込められます。

#include <stdio.h>

void func(void);

int main(void)
{
    #define FUNC_NAME   "main"

    puts(FUNC_NAME);
    func();

    #undef FUNC_NAME
}

void func(void)
{
    #define FUNC_NAME   "func"

    puts(FUNC_NAME);

    #undef FUNC_NAME
}

実行結果:

main
func

main関数内の FUNC_NAME は、main関数の終わりのところで #undef によって打ち消されます。同様に、func関数内の FUNC_NAME は、新たなマクロとして定義され、func関数の終わりにある #undef で打ち消されます。

分岐 (#if、#elif、#else、#endif)

プリプロセスの中で分岐構造を作る方法があり、条件によって、有効にするコードと無効にするコードを切り替えられます。その結果のコードがコンパイルされるため、コンパイルする部分としない部分を作れます。このようにコンパイルを制御することを、条件コンパイル (conditional compilation) と呼びます。

もっとも単純な分岐構造は、#if#endif によって構築できます。

#if 条件式
    条件式が真のときに有効なコード
#endif

「条件式」の結果が 0 以外になる場合に、「条件式が真のときに有効なコード」の部分は有効であり、コンパイルされることになります。プリプロセスの時点で評価できなければならないため、定数式でなければなりません。

通常の if文に対する else は #else で、else if は #elif で表現します。

#if 条件式1
    条件式1が真のときに有効なコード
#elif 条件式2
    条件式1が偽、条件式2が真のときに有効なコード
#else
    条件式1、条件式2がともに偽のときに有効なコード
#endif

以下は使用例です。PROGRAM_MODE の置換後の値を変更してビルドしなおすと、有効になるコードの部分が変化します。

#include <stdio.h>

#define PROGRAM_MODE    (1)

int main(void)
{
#if PROGRAM_MODE == 0
    puts("program mode 0.");
#elif PROGRAM_MODE == 1
    puts("program mode 1.");
#else
    puts("program mode is unknown.");
#endif
}

実行結果:

program mode 1.

有効にならない部分のコードはコンパイルされないため、プリプロセスでの分岐構造を、コメントアウトの代わりに用いることがあります(#if 0 ~ #endif で囲む)。/* ~ */ と違って、ネストできる利点があるほか、01 に変えるだけでアンコメントできる手軽さもあります。

#error

さきほどのサンプルプログラムで、PROGRAM_MODE の値が 0 でも 1 でもないときは、モード不明としてエラーにしたいかもしれません。そのようなときには、#error を使います。#error は強制的にエラーを発生させ、プリプロセスを強制終了します。コンパイルの過程に進ませません。

#error の使い方は次のとおりです。

#error エラーメッセージ

#error の行が有効になった場合、エラーになり、「エラーメッセージ」の内容が出力されます。

さきほどのサンプルプログラムを書き換えてみます。

#include <stdio.h>

#define PROGRAM_MODE    (2)

int main(void)
{
#if PROGRAM_MODE == 0
    puts("program mode 0.");
#elif PROGRAM_MODE == 1
    puts("program mode 1.");
#else
    #error "program mode is unknown.\n";
#endif
}

Visual Studio 2015 では、次のエラーが報告されます。

1>  main.c
1>c:\test_program\main.c(12): fatal error C1189: #error:  "program mode is unknown.\n";

マクロによる分岐 (#ifdef、#ifndef、defined)

プリプロセスによる分岐にはもう1種類あって、マクロが定義されているかどうかを判断基準にします。

#ifdef はマクロが定義されているかどうか、#ifndef はマクロが定義されていないかどうかを判定します。いずれも #else を加えることが可能です。

#ifdef マクロ名
    マクロが定義されているときに有効なコード
#endif

#ifndef マクロ名
    マクロが定義されていないときに有効なコード
#endif

#undef によって定義が消されている場合、そこから後ろの位置では「定義されていない」とみなされます。

#if と defined を組み合わせて実現することもできます。defined は、指定したマクロが定義されていたら 1 に、定義されていなければ 0 になります。

#if defined(マクロ名)
    マクロが定義されているときに有効なコード
#endif

// ( ) はなくても同じ
#if defined マクロ名
    マクロが定義されているときに有効なコード
#endif

こうした分岐の制御のためだけに使うマクロは、置換後の結果には興味がないので、「置換後の文字の並び」を省略して定義することもあります。

#define DEBUG_MODE
#ifdef DEBUG_MODE
// ...
#endif

次のサンプルは、標準入力から5つの整数を受け取り、その合計を出力するだけの簡単なプログラムです。NEGATIVE_NUMBER_NOT_ALLOW マクロが定義されているときには、負数をエラーとして扱うようにします。

#include <stdio.h>

#define NEGATIVE_NUMBER_NOT_ALLOW   // 有効な場合、負数を許可しない

int main(void)
{
    int total_values = 0;

    for (int i = 0; i < 5; ++i) {

#ifdef NEGATIVE_NUMBER_NOT_ALLOW
        puts("Please enter the integer greater than or equal to 0.");
#else
        puts("Please enter the integer.");
#endif

        char buf[64];
        fgets(buf, sizeof(buf), stdin);
        int value;
        sscanf(buf, "%d", &value);

#ifdef NEGATIVE_NUMBER_NOT_ALLOW
        if (value < 0) {
            puts("error.");
            return 1;
        }
#endif

        total_values += value;
    }

    printf("total: %d\n", total_values);
}

実行結果:

Please enter the integer greater than or equal to 0.
3  <-- 入力した内容
Please enter the integer greater than or equal to 0.
5  <-- 入力した内容
Please enter the integer greater than or equal to 0.
-1  <-- 入力した内容
error.

実行結果(NEGATIVE_NUMBER_NOT_ALLOW を定義しない場合):

Please enter the integer.
3  <-- 入力した内容
Please enter the integer.
5  <-- 入力した内容
Please enter the integer.
-1  <-- 入力した内容
Please enter the integer.
2  <-- 入力した内容
Please enter the integer.
-3  <-- 入力した内容
total: 6

空指令

#記号から始まる行は、プリプロセッサが処理を行いますが、#記号だけしかない行も同様です。この場合、何も行われることはないので、空指令 (null directive) と呼びます。

空指令は、#if - #elif のような並びが連続する場合など、ソースコードが見づらくなりがちな場合に、行間を空ける目的で利用できます。

#ifdef SIGNED
#
#define BYTE_MAX  127
#define BYTE_MIN  -128
#
#else
#
#define BYTE_MAX  255
#define BYTE_MIN  0
#
#endif


練習問題

問題① 円周率を表す記号定数を定義し、円の面積を求めるプログラムを作成してください。

問題② 次のプログラムを実行すると、出力結果はどうなるか答えてください。

#include <stdio.h>

#define PUT_SW


void func(int num);

int main(void)
{
    func(1);

#undef PUT_SW

    func(2);

#define PUT_SW

    func(3);

#undef PUT_SW

    func(4);
}

void func(int num)
{
#ifdef PUT_SW
    printf("%d\n", num);
#endif
}

問題③ まず、次のプログラムを見てください。

#include <stdio.h>

int main(void)
{
    puts("1");
    puts("2");
    puts("3");
    puts("4");
}

#if を使って、3 を出力している puts関数の呼び出しをコメントアウトしてください。 さらにその後、すべての puts関数の呼び出しをコメントアウトしてください。

/* と */ によるコメントアウトと比較すると、どのような違いがありますか?


解答ページはこちら

参考リンク


更新履歴

≪さらに古い更新履歴を展開する≫



前の章へ (第22章 スコープ)

次の章へ (第24章 複数ファイルによるプログラム)

C言語編のトップページへ

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る