先頭へ戻る

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

Programming Place Plus トップページC言語編

先頭へ戻る

この章の概要

この章の概要です。


プリプロセス(前処理)

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

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

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

プリプロセスは、ソースコードをコンパイルする直前で、ソースコード上にあるテキストに変化を加える過程です。これまで何度も使ってきた #include が代表的ですが、このように、先頭が「#」で始まる行が、プリプロセスで行う処理です。また、このような行は、プリプロセッサディレクティブ(プリプロセッサ指令、前処理指令)と呼ばれます。#include であれば、#includeディレクティブとか、#include前処理指令などと呼びます。

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

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

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

プリプロセスに先立って、コメントの部分が、1つの空白文字に置き換えられます

コメントは完全に消されるのではなく、空白文字に置き換わります。そのため、「ret/**/urn」のように書いたときに、コメント部分が無視されて「return」になるということはありません。置換結果は「ret urn」です。

【上級】さらに厳密にいえば、この処理よりも手前で行われている処理もありますが、意識することがほとんど無いのと、この章までの内容とは関係しないので省略します。

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


#include <stdio.h>

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

次に、プリプロセスの処理が行われます。「#」で始まる行に対して、何らかの処理を行い、ソースコードのテキストに変化を与えます。#include の場合は、指定したヘッダの内容を、その位置に取り込みます。


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

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

これが、プリプロセスを終えた後のソースコードの状態です。このコードがコンパイラに渡されて、コンパイルされます。stdio.h の中身には、puts関数の宣言が記述されています。それが取り込まれることで、puts関数の呼び出しを正常に行えます。

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

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

なお、ソースファイルに対して、プリプロセスの処理を実行し終えたものを、翻訳単位と呼びます。


オブジェクト形式マクロ

#include 以外のプリプロセッサディレクティブの例として、#defineディレクティブを紹介します。#define は、ソースコード上の文字の並びを、別の文字の並びに置き換える処理を行います。これをマクロ置換と呼びます。

#define には、大きく分けて2つのタイプがあります。1つはオブジェクト形式マクロ、もう1つは関数形式マクロですが、ここでは前者についてだけ説明します。(後者については第28章で説明します)。

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

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

強制されているわけではありませんが、一般的にマクロ名は大文字で表記することが多いです。また、置換後の文字の並びは空にしても構いません

「#」の直後に空白を空けることは可能です。

先ほどから言っている「文字の並び」というのは、“abcde” のような文字列リテラルのことを指しているのではないことに注意が必要です。ソースコード上に登場する単なる文字(たとえば、int は「i」「n」「t」という 3つの文字の並びです)を指しています。そういった単なる文字を置換するという考え方は、プリプロセスとコンパイルの区別が明確についていないと理解できないと思います。

コンパイルを行う直前で、ソースコード上の文字を置換し、その結果をコンパイルするのです。そうすることで、ソースコードの見た目を読みやすいまま保ちつつ、高度な処理を実現できます。

実際にオブジェクト形式マクロを使ったプログラムを見てみましょう。

#include <stdio.h>

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

    printf( "整数を%d回入力してください。\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( "合計: %d\n", sum );
    printf( "平均: %f\n", (double)sum / (double)INPUT_COUNT );

    return 0;
}

実行結果:

整数を5回入力してください。
9
4
7
-8
6
合計: 18
平均: 3.600000

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

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

int main(void)
{




    printf( "整数を%d回入力してください。\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( "合計: %d\n", sum );
    printf( "平均: %f\n", (double)sum / (double)5 );

    return 0;
}

単なる「5」という整数をプログラム中にばらまくと、後から変更することは大変な作業です。しかし、マクロ置換を利用すれば、#define のところの「5」という数値だけを書き換えれば、すべてを一斉に変更できます。これは、直し忘れを防ぐ意味があります

オブジェクト形式マクロは、「5」のような定数に、名前(ここでは「INPUT_COUNT」)を付けていると考えられます。このような名前の付いた定数を、記号定数と呼びます。これは、プログラムを分かりやすくするという価値があります

なお、プリプロセスの段階では、スコープなどのルールは関係しません。マクロ定義を関数の外側に書くことも、内側に書くこともできます。関数の内側に書いたからといって、その効果が関数内にとどまることもありません。マクロ定義を行った箇所以降、ずっとその効果は有効です。

【上級】これは、ヘッダファイル(第24章)にマクロ定義を書いた場合、そのヘッダファイルをインクルードしたすべてのファイルに影響を及ぼすことを意味しています。影響範囲が非常に大きくなることには注意が必要です。ヘッダファイルの中身を確認しないと、どんなマクロが定義されているか分からないですから、思わぬ置換が起こるかもしれません。

記号定数に対して、単なる「5」のように、単独では意味が通じない定数を、マジックナンバーと呼ぶことがあります。一般的に、マジックナンバーは避けて、記号定数を使うようにした方が良いとされます。

ただし、記号定数の価値は、直し忘れを防いだり、意味を明確にしたりするところにあるのですから、たとえば for文に使う変数i に与える初期値「0」のようなものまで記号定数にするのは、やり過ぎといえます。

また、マジックナンバーを極度に避けようとするあまり、次のようなマクロ定義を書こうとするかもしれません。

#define FIVE 5

定数5 だから、FIVE という名前を付けたということですが、これは無意味であるばかりか、むしろ悪化させています。問題が2つあります。

まず、「意味を明確に」はしていません。「5」が数字の 5 であることは十分に明確ですから、「FIVE」にしたらより明確になるということはないでしょう。

また、「直し忘れを防ぐ」ことに意味がありません。仮に「FIVE」の置換結果が、「7」であるべきだと後で気づいたとしたらどうでしょう? 「#define FIVE 7」に直すべきなのでしょうか? 「FIVE」という名前なのに、置換結果が「7」なのは明らかにおかしく、混乱させるだけです。

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

#define int float

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


マクロを無効化する

#define によるマクロ置換は、プリプロセスで行われるため、{ } で囲ったとしても無意味であり、スコープがありません。思いがけないところにまでマクロ置換の影響を与えてしまう可能性があるので、少々危険です。

{ } では #define の効果が閉じ込められないということは、関数の内側で定義を行っても、他の関数にも影響を与えるということです。

#include <stdio.h>

void func(void);

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

    puts( FUNC_NAME );
    func();

    return 0;
}

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

    puts( FUNC_NAME );
}

main関数の内側で定義したマクロFUNC_NAME の効力は、その位置よりも下に影響を与えるため、func関数の中にまで影響します。

このプログラムは、エラーになるかもしれません(Visual Studio 2017、clang 5.0.0 では警告されます)。規格上は、同じ名前のオブジェクト形式マクロの定義が2度現れた場合、置換結果がまったく同じであれば問題ありません。このプログラムの場合、置換結果が異なっていますから、認められない可能性があります。

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

#undef マクロ名

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

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

#include <stdio.h>

void func(void);

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

    puts( FUNC_NAME );
    func();

    return 0;

    #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ディレクティブを取り上げます。最も基本的な構文は次のようになります。

#if 条件式
// ここに何か書く
#endif

プリプロセスの中で行う処理ですから、「条件式」のところでは定数しか使えません。そして、整数型でなければなりません。

#if と #endif で挟んだ部分は、条件式が真のときにだけコンパイルの対象になります。偽の場合は、挟まれた部分は読み飛ばされ、コンパイルされません。

次のサンプルプログラムの場合、#if の条件式は偽ですから、#if と #endif で挟まれた部分はコンパイルされません。

#include <stdio.h>

#define DEBUG 0

int main(void)
{
#if DEBUG == 1
    printf( "%d\n", num );
#endif

    return 0;
}

実行結果:

もしコンパイルされていれば、変数num が宣言されていないためコンパイルエラーになるはずですが、このプログラムは問題なくコンパイルできます。DEBUG を 1 に置換されるように変えてみると、コンパイルエラーになります。

条件式が偽になった場合、挟まれた部分がコンパイルされないことから、/* と */ によるコメントアウトの代わりに使われることもあります。/* と */ によるコメントアウトと比べると、#if ~ #endif は入れ子にできるという違いがあります。

このサンプルプログラムは、次のように書くこともできます。

#include <stdio.h>

#define DEBUG

int main(void)
{
#if defined(DEBUG)
    printf( "%d\n", num );
#endif

    return 0;
}

#if を使っていますが、definedというキーワードを付け加えています。defined(DEBUG) のように記述すると、( ) 内に記述した名前のマクロが有効かどうかを条件にできます。これは、defined演算子と呼ばれます。

マクロが有効かどうかというのは、マクロ定義が、この場所に効果を及ぼしているかどうかということです。たとえば、#define の方が後方にあれば有効ではありませんし、#undef で効果を消していた場合も有効ではありません。

これと同様のことが、#ifdefディレクティブを使って実現できます。#ifdef は次のような形式です。

#ifdef マクロ名
// ここに何か書く
#endif

「マクロ名」に記述した名前を持ったマクロが有効であれば、真であるとみなされます。

#include <stdio.h>

#define DEBUG

int main(void)
{
#ifdef DEBUG
    printf( "%d\n", num );
#endif

    return 0;
}

次に否定形を確認しましょう。#if の場合なら、!演算子で反転できます。defined とも組み合わせられます。

#if !DEBUG
// ここに何か書く
#endif

#if !defined(DEBUG)
// ここに何か書く
#endif

#ifdef を否定する場合なら、#ifndefディレクティブを使います。

#ifndef DEBUG
// ここに何か書く
#endif

また、#if、#ifdef、#ifndef ともに #elseディレクティブで2方向に分岐できます。

#if defined(DEBUG)
// ここに何か書く
#else
// ここに何か書く
#endif

#ifdef DEBUG
// ここに何か書く
#else
// ここに何か書く
#endif

また、3方向以上に分岐するためには、#elifディレクティブを使います。

#if defined(DEBUG)
// ここに何か書く
#elif defined(RELEASE)
// ここに何か書く
#else
// ここに何か書く
#endif

「#else if」ではなく「#elif」と書くことに注意してください。なお、switch文のように、1度に多方向に分岐できるものはありません。

また、#ifディレクティブの場合は、&&演算子と ||演算子も使えます。#ifdef や #ifndef には、これらの演算子は使えません。そのため、2つ以上のマクロの定義の有無によって分岐させるには、#if defined の形で書く必要があります。

#if defined(DEBUG) && defined(TEST)
// ここに何か書く
#endif

#if defined(DEBUG) || defined(TEST)
// ここに何か書く
#endif

本格的なプログラム開発では、プログラムが完成品となるまでの間、何度もテストを行う必要があります。そのような時期には DEBUGマクロを定義し、完成品には定義しないようにすることで、テスト中にだけ有効になる処理を埋め込めます。たとえば、何か処理を行うたびにログメッセージを出力するというような用途が考えられます。

空指令

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

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

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

プログラムを見やすく書くことは、当然ながら、プリプロセスの部分でも例外ではありません。

#error指令

#error指令は、指定されたメッセージを出力し、コンパイル作業を中止します。

#include <stdio.h>

// 下の2つは、いずれか一方だけを有効にすること
#define DEBUG_MODE          // デバッグモードでコンパイル
#define RELEASE_MODE        // リリースモードでコンパイル

int main(void)
{
#if defined(DEBUG_MODE) && defined(RELEASE_MODE)
    #error "DEBUG_MODE and RELEASE_MODE can define only either of them."
#endif

    puts( "OK" );

    return 0;
}

#error の箇所がコンパイルされると、コンパイルエラーになり、指定した文字列がエラーメッセージとして出力されます。

このサンプルプログラムのように、同時に定義されていてはならないマクロが定義されている場合や、定義されていなければならないマクロが定義されていない場合、置換結果が適切でない場合など、プリプロセスの段階で検出できるエラーの有無をチェックするために役に立ちます。

複数のコンパイラでコンパイルできるソースコードを作成している場合に、コンパイラの種類やバージョンを調べるのにも使われます。普通、コンパイラメーカーはそれぞれの製品に、自身の情報を表すマクロ(製品の種別やバージョン番号など)を定義しています。


練習問題

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

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

#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 );

    return 0;
}

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" );

    return 0;
}

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

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


解答ページはこちら

参考リンク


更新履歴

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



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

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

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

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
Twitter でツイート Twitter をフォロー LINE で送る
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー