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

先頭へ戻る

このエントリーをはてなブックマークに追加

この章の概要

この章の概要です。

プリプロセスから実行まで

C言語の特徴的な仕様がプリプロセス(前処理)と呼ばれるものです。 ここでは、何に対しての「前処理」なのかがポイントになります。 そこでまずは、C言語のプログラムが、最終的に実行できる状態になるまでの過程を辿っておきましょう。

前章のプログラム例のように、C言語のプログラムには通常、ソースファイル(.c) とヘッダファイル(.h) が登場します。
VisualC++ や Xcode を使っているとすれば、 これらのファイルを組み合わせてプログラムを作成した後、 「ビルド (build)」を行い、何もエラーが出なければ、 「実行 (Run)」してみて動作を確認すると思います。

ここで「ビルド」というのは、「コンパイル」と「リンク」という 2つの過程が組み合わされた処理です。 VisualC++ や Xcode などの現在の一般的な開発環境では、ボタン1つで簡単に、この過程が行えるようになっていますが、 本来は、コンパイルとリンクは個別に行う必要があります。

コンパイルとは、ソースファイルを翻訳して、別の言語に置き換える過程を指します (ここでの言語というのは、プログラミング言語の話であって、日本語とか英語とかいう意味ではありません)。 また、コンパイルを行うプログラムをコンパイラといいます。

VisualC++ や Xcode はコンパイルもできるし、リンクもできるし、デバッグ作業もできるし、ソースコードを書くエディタも持っているので、 単に「コンパイラ」と呼ぶのは本来適切ではありません。コンパイルは、持っている機能の1つに過ぎません。 普通は、統合開発環境(IDE)と呼びます。

言語には、人間が比較的読みやすい高級言語と、 人間には読みづらいがコンピュータが簡単に理解できる低級言語があります。 この区別は明確なものではなく、言語1つ1つにおいて読みやすさの段階はあります。
この区別でいえば、C言語は高級言語に近いと言えます。

コンパイルという過程は、高級の方向に近い言語を、低級に近い言語へ翻訳することです。 多くの場合、マシン語と呼ばれる最も低級な言語へと翻訳します。 なぜかというと、コンピュータが直接理解でき、直接実行できる言語はマシン語だけだからです。
従って、本当のところ、プログラムというものはマシン語で書かないといけないのですが、 「最も低級な言語」と言ったように、人間がマシン語を覚えてプログラムを書き上げることは、ほとんど不可能に近い作業なのです。 そこで、C言語のような、人間が読みやすい形の言語が多数考案されたのです。

さて、ところが1つのプログラムには、複数のファイルが含まれているので、1つ1つのファイルをコンパイルしたところで、 全体像が見えないと正しく動作できません。 そこで、1つ1つのソースファイルは、1つ1つのオブジェクトファイルという形に翻訳し、 最後にこれらを1つにまとめます。このまとめる過程を「リンク」と呼び、リンクを行うプログラムをリンカといいます。

VisualC++ ならば、ビルドを行った後、エクスプローラでプロジェクトのあるフォルダを覗くと、Debug という名前のフォルダが作成されていることが分かります。 この中にある .obj という拡張子のファイルがオブジェクトファイルです。.cファイルの名前と同じ名前で作成されているはずです。
Xcode の場合は、ものすごく深い階層に作られていますが、.o という拡張子のファイルがあるはずです。

複数のオブジェクトファイルがリンクされた結果、1つの実行可能ファイルが作成されます( Windows であれば .exe という拡張子を持ちます)。 この実行可能ファイルは、Windows のエクスプローラや、OS X の Finder などから実行できる状態になっています。 VisualC++ や Xcode 上で、実行のコマンドを選択したときに実行されているのは、この生成された実行可能ファイルです。

VisualC++ や Xcode でビルドを行うと、そのときの設定に応じて、Debugフォルダや Releaseフォルダの中に実行可能ファイルが生成されています。


長くなりましたが、この、コンパイル⇒リンク⇒実行 という流れの手前にくるのがプリプロセスという段階です。 従って、「プリプロセス⇒コンパイル⇒リンク⇒実行」というのが処理の流れです。 プリプロセスは、作成したプログラムをコンパイルにかける直前で、「ちょっと先にこれだけやらせて」というようなものです。 プリプロセスを行うプログラムは、プリプロセッサと呼びます。

プリプロセッサとコンパイラが完全に独立しているか、コンパイラの一部として動作するのかは、環境にも依りますが、 概念的に独立して考えるべきなのは間違いありません。

では具体的にプリプロセスというのはどんなことをするかと言えば、実はもう何度も使っている #include が好例です。 C言語では、先頭が「#」で始まる行が、すなわちプリプロセスとして処理する対象です。 #include 以外にも幾つか存在しています。
プリプロセスとして処理させる命令のことを、プリプロセッサディレクティブ(前処理命令)などと言います。 #include なら、#includeディレクティブと呼びます。

オブジェクト形式マクロ

#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」に置換されます。 置換後の状態は、次のようになります。

#include <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;
}

こうなります。 このような状態にまで持ってくるのが、プリプロセスであって、この状態のプログラムがコンパイルされます。

プリプロセスで置換作業を行っているので、C言語の文法のルールは通用しません。 #define の効果は、その定義が記述された場所よりも後方であれば、どこまでも適用されます。 要するに、スコープの概念がありません

単なる「5」という整数をプログラム中にばらまくと、後から変更することは大変な作業になりますが、 このようにマクロ置換を利用していれば、#define のところの「5」という数値だけを書き換えれば、全て自動的に変更されます。

このようなオブジェクト形式マクロの利用法は、置換後の結果が定数であることから、記号定数と呼ばれます。 単なる「5」という定数よりも、「INPUT_COUNT」という名前の付いた記号定数である方が、プログラムを読む際に意味が通じやすくなりますし、 前述したように、後から変更を加えることも容易になります。

記号定数を使うのに対し、単なる「5」のような単独で意味が通じない単なる定数をマジックナンバーと呼ぶことがあります。 意味が明確かどうかがポイントですから、例えば for文に使う変数i の初期値 の「0」まで記号定数にする必要があるかと言えば、 そこまでするのはちょっとやり過ぎになります。

このような利点があるので、マジックナンバーを避け、積極的に記号定数を使うべきですが、たまにこう書いてしまう人がいるようです。

#define FIVE 5

定数5 だから、FIVE という名前を付けたという訳ですが、こんなものは無意味であるばかりか、むしろ悪化させています。 前述したように、マクロの利点は「変更を容易にし、読みやすさを向上させること」です。 この FIVEマクロでは、後から「5」を「10」に変更したら、名前も「TEN」に変えないといけなくなります。 名前の変更まで必要というのなら、利点が失われますし、名前を「FIVE」のままにしようものなら、「FIVE という名前なのに実は 10」というウソを付くことになります。


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

#define int float

こんなことも許されてしまいます。 このマクロによって、int はすべて float に置換されてしまう訳です。 この手の置換によって、プログラムはもはや解読不能なものへと変貌してしまうでしょう。

なお、置換後の文字の並びは空にしても構いません。 この場合、置換した結果、何もなくなるという意味になるため、マクロ置換としてはほとんど無意味ですが、 後で説明するプリプロセスでの分岐処理を実現する際に利用することがあります。

マクロを無効化する

#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
main

main関数の内側で定義したマクロFUNC_NAME の効力は、その位置よりも下に影響を与えるため、func関数の中にまで影響します。 コメントアウトしてある func関数の内側の FUNC_NAME を有効にすると、エラーになってしまいます。

このエラーの理由を、「同じ名前のマクロ定義が重複しているから」だと思いがちですが、実はそうではなくて、 「同じ名前なのに、置換後の文字の並びが違っているから」がエラーの理由です

実際、func関数の方の FUNC_NAME の置換後の文字の並びを "main" に変えると、エラーは消えます。 同じ名前での再定義は単に無視されるのです。

実は #define の効力は、#undef という別のプリプロセッサディレクティブで打ち消すことができます。

#undef マクロ名

マクロ名に指定した名前と同じ #define の効力は、#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 で打ち消されます。

このように、#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

多少見やすくなったでしょうか? VisualC++ のエディタなどは、プリプロセッサディレクティブの記述を自動的に行の先頭に寄せるようになっていますが、 これは場合によって、非常に見づらいプログラムになります。
その一方で、プリプロセスで行われるものなのだから、不用意に { } を付け足す等して、 C言語の構文のルールと混在させるべきではないという考え方もあります。

プリプロセッサによる分岐

プリプロセスの段階で分岐させることもできます。
プリプロセスによる分岐も、これがコンパイルの前処理として行われているのだということを頭に置いておかないと、 なかなか理解しづらいかも知れません。

例えば、プリプロセスでの分岐では、条件式に定数しか使えません。 なぜだか分かりますか?

通常の if文や switch文による分岐処理は、プログラムを実行しているときに条件を判定します。 ですから、変数を使って、そのときの状況に応じた分岐処理が実現できます。

一方、プリプロセスでの分岐は、プログラムの実行中どころか、コンパイルすらしていない段階での処理です。 この時点では、変数はまだメモリ上に定義されていません(実行すらしていないので)。 従って、プリプロセスで使える条件式は、定数だけで完結したものである必要があります。


プリプロセスで使える分岐命令は幾つかありますが、まずは #ifディレクティブを取り上げます。 #if は、他のプリプロセッサディレクティブとセットで使われます。 最も基本的な構文は次のようになります。

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

#if と #endif で挟んだ部分は、条件式が真になったときにだけ有効になります。 くどいようですが、プリプロセスはコンパイルの前段階にあたるので、 もし #if の条件式が偽になった場合、挟み込まれた部分はコンパイルされません

#include <stdio.h>

#define DEBUG 0

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

実行結果:




このサンプルプログラムの場合、#if の条件式は偽になりますから、#if と #endif で挟まれた部分はコンパイルされません。 もしコンパイルされていれば、変数num が存在しないためコンパイルエラーになるはずです。 しかし、このプログラムは問題なくコンパイルできます。
記号定数DEBUG の値を 1 に変えてみれば、コンパイルエラーになります。

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

#if はこのように使いますが、今回の場合なら、次のように書くこともできます。

#include <stdio.h>

#define DEBUG

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

#if に代わって、#ifdefディレクティブを使っています。 #ifdef の後ろには、条件式ではなくマクロの名前を書きます。

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

もし、指定した名前のマクロが定義されていれば、真であるとみなされます。 定義されていなければ偽です。

#ifdef の場合、そのマクロが実際にどんな値に置換されるかは問題ではありません。 そこで、#define でマクロを定義するときに、置換後の値を空にしてしまえます。

また、更にもう1つ書き方があります。

#include <stdio.h>

#define DEBUG

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

今度は #if を使っていますが、defined というキーワードを付け加えています。 defined(DEBUG) のように記述すると、( ) 内に記述した名前のマクロが定義されているかどうかを調べられます。 これを使えば、#ifdef と同じことができます。


次に否定形を確認しておきます。 #if の場合なら、条件式全体を ( ) で囲んで !演算子で反転してしまえます。 defined と組み合わせることもできます。

#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文のようなものは存在しませんから、#elif を利用するしかありません。

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

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


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

インクルードガード

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

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

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

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

ただ、現状で問題ないにしても、多重インクルードによる多重定義問題への対策は取っておくことが事実上必須事項です。 この対策のために、プリプロセッサを使った分岐処理が利用できます。 すべてのヘッダファイルを次のように記述することで、多重インクルードは防止できます。

#ifndef MY_HEADER_H
#define MY_HEADER_H

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

#endif

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

VisualC++ や clang の場合、#ifndef、#define の2行 を、「#pragma once」という1行に置き換え、#endif を消してしまうことで、同じ効果が実現できます。 ただし、コンパイラ依存の方法になってしまいます。

このヘッダファイルが初めて include されるときには、MY_HEADER_H というマクロは未定義状態なので、#ifndef は真となり、内側にある記述が有効になります。 #ifndef の直後には、#define があり、ここで MY_HEADER_H が定義されます。
2度目以降の include時には、1度目のときに MY_HEADER_H が定義されているので、#ifndef が偽となり、内側にある記述はすべて無視されます。 こうして、2度目以降の取り込みは行われはするものの、#ifndef の効果によって、中身が事実上、空の状態になり、多重定義の問題は起こり得なくなります。


練習問題

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

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

#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
}

問題B まず、次のプログラムを見て下さい。

#include <stdio.h>

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

	return 0;
}

#if を使って、3 を出力している puts関数の呼び出しをコメントアウトして下さい。
更に、その後、すべての puts関数の呼び出しをコメントアウトして下さい。
/* と */ によるコメントアウトと比較すると、どのような違いがありますか?


解答ページはこちら

参考リンク

更新履歴

'2015/8/23 文章を Xcode (OS X環境)にも対応させた。

'2009/9/13 「インクルードガード」の項を追加。

'2009/8/13 新規作成。



前の章へ

次の章へ

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

Programming Place Plus のトップページへ