C言語編 第24章 プリプロセッサ

先頭へ戻る

この章の概要

この章の概要です。


プリプロセス(前処理)

前章で、ビルドと実行の流れを説明しました。ビルドは、「コンパイル⇒リンク」の順で行われるということでしたが、このさらに手前に、プリプロセス(前処理)という段階があります。また、本題と外れる話ですが、プリプロセスが行われる直前で、コメントを、1つの空白文字に置き換えるという処理がなされます

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

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

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

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

/* my.c */
#include "my.h"

void func(void)
{
}
/* my.h */
void func(void);
/* main.c */
#include "my.h"

int main(void)
{
    func();
    return 0;
}

まず、コメントの部分が、1つの空白文字に置き換えられます。

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

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


#include "my.h"

void func(void)
{
}

void func(void);

#include "my.h"

int main(void)
{
    func();
    return 0;
}

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



void func(void);

void func(void)
{
}

void func(void);


void func(void);

int main(void)
{
    func();
    return 0;
}

これが、プリプロセスを終えた後のソースコードの状態です。このコードがコンパイラに渡されて、コンパイルされます。

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

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

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

オブジェクト形式マクロ

#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		/* 入力させる回数 */

    char buf[40];
    int sum = 0;
    int num;
    int i;


    printf( "整数を%d回入力してください。\n", INPUT_COUNT );

    for( i = 0; i < INPUT_COUNT; ++i ){
        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)
{


    char buf[40];
    int sum = 0;
    int num;
    int i;


    printf( "整数を%d回入力してください。\n", 5 );

    for( i = 0; i < 5; ++i ){
        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」)を付けていると考えられます。このような名前の付いた定数を、記号定数と呼びます。これは、プログラムを分かりやすくするという価値があります

記号定数に対して、単なる「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関数の中にまで影響します。

このプログラムは、エラーになるかもしれません(VisualStudio 2015/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 の箇所がコンパイルされると、コンパイルエラーになり、指定した文字列がエラーメッセージとして出力されます。

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

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

インクルードガード

前章のように、ヘッダファイルを作成し、複数のファイルが連携するプログラムの場合、多重インクルードという問題が起こることがあります。

多重インクルードは、その呼び名の通り、1つのヘッダファイルを重複してインクルードすることで起きる問題です。以前説明したように、#include は、指定されたファイルの中身をそっくりそのまま取り込むだけですから、同一のヘッダファイルを何度もインクルードすると、宣言や定義が重複してしまう可能性があります。

特に問題なのは、ヘッダファイルからさらに別のヘッダファイルをインクルードしているケースです。例えば、aaa.h は bbb.h をインクルードしているときに、main.c が aaa.h と bbb.h を両方ともインクルードすると、bbb.h は 2度取り込まれます。

実際のところ、関数宣言や、extern付きのグローバル変数といったものは、重複してもエラーにはなりません。マクロ定義も、置換後の結果が同じであれば、重複しても問題ありません。ただし、#if によって #define の置換結果が変化することがあれば、重複定義が問題になる可能性もあります。

そのような問題に直面していないにしても、多重インクルードによる多重定義問題への対策は常に取るようにすべきです。この対策のために、プリプロセスでの分岐処理が利用できます。すべてのヘッダファイルを次のように記述することで、多重インクルードは防止できます。

#ifndef MY_HEADER_H_INCLUDED
#define MY_HEADER_H_INCLUDED

/* ヘッダの中身はここに書く */

#endif

「MY_HEADER_H」の部分は、ヘッダファイルごとに異なる名前を使うようにします。確実に異なる名前を付けるために、ヘッダファイル自身の名前を元に命名することが多いです(異なるディレクトリに同じ名前のファイルがある場合は、これだけではダメです。その可能性があるのなら、ディレクトリ名(グループ名)を付け足すなどの工夫が必要です)

VisualStudio や clang の場合、#ifndef、#define の2行 を、「#pragma once」という1行に置き換え、#endif を消してしまうことで、同じ効果が実現できます。この方法はそれなりに一般的になっているものではありますが、コンパイラ依存です。

このヘッダファイルが初めてインクルードされるときには、MY_HEADER_H というマクロは未定義状態なので、#ifndef は真となり、内側にある記述は有効です。#ifndef の直後には、#define があり、ここで MY_HEADER_H が定義されます。

2度目以降のインクルード時には、1度目のときに MY_HEADER_H が定義されているので、#ifndef が偽となり、内側にある記述はすべて無視されます。こうして、2度目以降の取り込みは行われはするものの、#ifndef の効果によって、中身が事実上、空の状態になり、多重定義の問題は起こり得なくなります。


練習問題

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

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

#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関数の呼び出しをコメントアウトしてください。
/* と */ によるコメントアウトと比較すると、どのような違いがありますか?


解答ページはこちら

参考リンク



更新履歴

'2018/8/30 VisualStudio でプリプロセス後のコードを確認する方法を、開発ツールの情報のページでサポートするようにした。

'2018/7/24 翻訳単位という用語の解説を補った。

'2018/7/21 defined演算子という用語を補った。

'2018/4/5 VisualStudio 2013 の対応終了。

'2018/4/2 「VisualC++」という表現を「VisualStudio」に統一。

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



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

次の章へ (第25章 配列と文字列)

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

Programming Place Plus のトップページへ


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