S-JIS[2006-10-30]

プリプロセッサ処理

C言語の特徴のひとつに、ソースの前処理(プリプロセス)というものがある。
これは、コンパイラーがコンパイルを行う前に、プリプロセッサがプリプロセッサ命令(「#」で始まるコマンド等)を処理するもの。いわゆる「マクロ」の展開。

とても便利で強力な機能だが、下手に使うとソースがどう展開されるかさっぱり分からないことになってしまうので要注意。

プリプロセッサ命令は、基本的に「#」で始まり、行末で終わる。(通常のC言語の構文では、行末かどうかは無関係だが)
「#」と命令の間にはスペースを入れてもよい。
複数行にまたがるプリプロセッサ命令を書きたい場合は、行の継続記号として、行末に「\」を置く。

また、コメントを処理(ソースから削除)するのもプリプロセッサの仕事。

gccなら「-E」オプション、VC++なら「/E」オプションを付けることにより、プリプロセッサがどういう展開をしたかを見ることが出来る。


#include

#include <stdio.h>
#include "hoge.h"

指定されたファイルを読み込み、#include命令のあった場所に展開する。

ファイル名が角括弧でくくられている場合は、コンパイラ内に指定されているデフォルトの場所(ディレクトリ)を優先的に探す。
ダブルクォーテーションでくくられている場合は、カレントディレクトリ(ソースが置かれているディレクトリ・コンパイルを行うディレクトリ)を優先的に探す。

入門書にはよく「おまじないと思って無視して下さい」と書いてある。
C言語では関数を呼ぶ前に関数宣言(あるいは関数定義)を行う必要があり、毎回自分のソースに書くのはわずらわしいのでヘッダーファイルというものをつくり、その中に宣言を書いておく。
#includeは、そのヘッダーファイルを読み込むもの。

#includeのあった位置ヘッダーファイルの内容がそのまま展開されるという辺りが、Javaのimportとは異なる。


#line

#line 1 "ディレクトリ/stdio.h"

展開前のソースのファイル名と行が書かれる。

プログラマーが使うことは無いと思う。プリプロセッサが#includeを展開したとき、その元となった位置(ファイル名と行)が記述される。


#define

マクロを定義する。


キーワード定義

#define	INCLUDE_HOGE_H

値は定義せず、マクロ名だけ定義する。
#ifdef等で条件に応じたコンパイルを行うのによく使われる。


定数マクロ定義

#define	ARRAY_MAX		100

固定値を定義する。

局所的な定数であれば、constキーワードを使う方が好みだけど。


関数マクロ定義

関数のように引数を持ち(無くてもいいが)、値を返すようなマクロを定義する。
C99では、引数の個数が可変のマクロも扱えるらしい。

展開例
#define	MAX(a,b)		((a) > (b) ? (a) : (b))
	int a = MAX(1,2);
		↓
	int a = ((1) > (2) ? (1) : (2));

引数を指定する場合、引数の変数は括弧でくくるようにする。
そうでないと、意図しない展開がされてしまう場合が出てくる。

意図しない展開例
×
#define	MULT3(a)	(a * 3)
	int m = MULT3(1 + 2);
		↓
	int m = (1 + 2 * 3);	//本当は(1+2)*3にしたいはず
○
#define	MULT3(a)	((a) * 3)

同様に、定義した式全体も括弧でくくっておく方がよい。

意図しない展開例
×
#define	SUM(a,b)	a + b
	int s = SUM(1, 2) * 3;
		↓
	int s = 1 + 2 * 3;		//本当は(1+2)*3にしたいはず
○
#define	SUM(a,b)	((a) + (b))

マクロの引数以外の変数は、そのまま残る。

展開例
#define	ADD(n)	(a + (n))
	int a = 1; int b = ADD(2);
		↓
	int a = 1; int b = (a + (2));

文字列化

マクロの引数を使用する際に「#」を付けると、引数がそのまま展開されるのではなく、文字列定数として展開される。

#define	STR(no)	#no
	printf(STR(1) "\n");
		↓
	printf("1" "\n");

この場合、マクロの引数に式を指定すると式がそのまま文字列となってしまうので注意。

たぶん意図しない展開例
	printf(STR(1+2) "\n");
		↓
	printf("1+2" "\n");

なお、文字列定数が並んでいる場合、(プリプロセッサ処理ではなく)コンパイル処理において結合され、1つの文字列として扱われる。
"1+2" "\n"」→「"1+2\n"


識別子の結合

マクロの引数に「##」を付けて別の識別子(キーワード)とつなぐと、結合されて1つのキーワードとして展開される。

#define	VAR(no)	v##no
	printf("%d\n", VAR(1));
		↓
	printf("%d\n", v1);

文マクロ定義

C言語の文自体もマクロにすることが出来る。

#define	EX	extern
#define	RETURN(str)	if (str == NULL) { \
				return ""; \
			} else { \
				return str; \
			}
	RETURN("abc");
		↓
	if ("abc" == NULL) { return ""; } else { return "abc"; };

これをあまり使いすぎると、見た目は関数なのに関数でない動作をするわけで、分かりにくくなるので要注意。

また、関数のように思ってif文で使ったりすると意図せぬ展開をされてしまうので注意。

	if (strcmp(s, t) == 0)
		RETURN(s);
	else
		RETURN(t);
	↓
	if (strcmp(s, t) == 0)
		if (s == NULL) { return ""; } else { return s; };	//←×
	else
		if (t == NULL) { return ""; } else { return t; };

×印の行の末尾の「;」のせいで、外側のif文がそこで終わっていると解釈され、次のelseに対応するif文が無いと判断されてコンパイルエラーとなる。

これを回避する手段としては、常套手段としてdo〜while(0)が使われる。

#define	RETURN(str)	do { \
				if (str == NULL) { \
					return ""; \
				} else { \
					return str; \
				} \
			} while(0)

逆に、「if文を使う際には必ず中括弧『{〜}』でくくれ」と言われるのは、こうしたことが考慮されていないマクロによる予期せぬ動作を回避する為でもある。


初期値マクロ定義

配列や構造体の初期値の並びを マクロ化することが出来る。

static struct {
	int		no;
	const char	*p;
} var[] = {
#define	INI(n)	{ n, #n }
	INI(1),
	INI(2),
	INI(3)
#undef	INI
};

static struct {
	int		no;
	const char	*p;
} var[] = {
	{ 1, "1" },
	{ 2, "2" },
	{ 3, "3" }
};

#undef

#defineで定義したマクロを無効化する。

#undef	INI

#defineで定義したのと同じ名称のマクロは再定義できないが、#undefで無効化すれば 再定義できるようになる。


#if
#elif
#else
#endif

条件を満たすソースだけを有効とする。

#define	TEST 2
#if TEST == 1
	printf("1\n");
#elif TEST == 2
	printf("2\n");
#else
	printf("else\n");
#endif

#ifや#elifの「条件」にはC言語の通常の演算がだいたい使えるが、sizeof演算子プリプロセッサ処理より後のコンパイル時点 (あるいは実行時)で値が決定される為、プリプロセッサ処理時点では使うことができない。

#if (TEST == 1) || (TEST == 2)

defined」という条件(演算子)は、マクロが定義されているかどうかを判断する。

#if defined(TEST)
#if !defined(TEST)

#ifdef
#ifndef

definedを使った条件判断と同等。

#ifdef TEST	//#if defined(TEST)
#ifndef TEST	//#if !defined(TEST)

よく使われるのが、ヘッダーファイルを一度しか読み込まないようにすること。

hoge.h:

#ifndef	INCLUDE_HOGE_H
#define	INCLUDE_HOGE_H
〜
//ヘッダー本体
〜
#endif	//INCLUDE_HOGE_H

ちなみに、コンパイラーによっては同等のことを「#pragma once」という命令で行うことが出来る。
(つまり、「#pragma once」が書かれていると、そのファイルは一度しか読み込まれない)

hoge.h:

#pragma once
〜
//ヘッダー本体
〜

#error

コンパイルエラーを発生させる。

#ifの中で使用し、在り得ない状態になったことを示すのに使う。

#if TEST==1 || TEST==2
#else
#error	TEST range error!
#endif

#pragma

コンパイラー(プリプロセッサー)独自のプリプロセッサ命令を定義するのに使われる。

有名なところでは「#pragma asm」で、アセンブラの命令をそのまま書くとか。

VC++だと「#pragma comment」とか「#pragma warning」とか「#pragma once」が有名。


C言語目次へ戻る / 技術メモへ戻る
メールの送信先:ひしだま