これまでコンパイルしてきたすべての C ファイルは #include <stdio.h> のような行から始まっていましたが、その行は C ではありません。プリプロセッサという別のプログラムへの指示であり、それが先に走ってソーステキストを書き換え、結果をコンパイラに手渡します。
この 2 段階の分離を理解すると、C の振る舞いの多くが腑に落ちます。なぜヘッダはインポートではなく貼り付けなのか、なぜ不出来なマクロがまったく問題なさそうな行でエラーを出すのか、そしてなぜ同じソースファイルがシステムによって異なるプログラムにコンパイルされうるのか。
コンパイル前に実際に起きていること
.c ファイルのコンパイルは 1 つの工程ではありません。おおまかには 4 つです。
- 前処理 -
#で始まる行にすべて従い、1 つの大きな展開済みソーステキスト(翻訳単位)を作る。 - コンパイル - そのテキストをアセンブリに、さらにオブジェクトファイルに変える。
- アセンブル - 機械語を生成する。
- リンク - オブジェクトファイルとライブラリをつないで実行ファイルにする。
プリプロセッサは純粋にテキストの上で働きます。変数が何か、型が何か、波かっこが釣り合っているかどうかを知りません。文字とトークンを見て、その一部を別のものに置き換え、先へ進むだけです。マクロにまつわる奇妙なことはすべて、この 1 つの事実から出てきます。
コンパイラがこのプログラムを読むころには、GREETING はどこにもありません。プリプロセッサが、あなたが自分で打ち込んだのとまったく同じように、リテラル "マクロからこんにちは" に置き換え済みだからです。
ディレクティブの一族
プリプロセッサのディレクティブはどれも、その行の空白でない最初の文字が # で始まります。セミコロンはなく、バックスラッシュで継続しない限り行末でディレクティブは終わります。
| ディレクティブ | 働き |
|---|---|
#include | 別のファイルの中身を貼り込む |
#define | マクロ(テキスト置換)を定義する |
#undef | マクロの定義を取り消す |
#ifdef, #ifndef | マクロが定義されている(いない)ときだけ続くコードを残す |
#if, #elif, #else, #endif | 定数式に基づいてコードを残す |
#error | メッセージを出してコンパイルを止める |
#pragma | #pragma once のような、コンパイラ固有の指示 |
#line | 報告される行番号を変える(まれ) |
このうち 3 つには独立したページがあります。マクロ が #define を掘り下げ、条件付きコンパイル が #if の一族を扱い、ヘッダファイル が複数ファイルのプログラムを組み立てるための #include の使い方を扱います。
#include:山かっこと引用符
#include がすることはただ 1 つ、自分自身の行を、名指したファイルの中身まるごとに置き換えることです。取り込まれたファイル自体も前処理されるので、その中の #include 行も展開されます。
#include <stdio.h> /* システムのインクルードディレクトリを探す */
#include "config.h" /* このファイルのディレクトリを先に探す */
違いは探索の順序です。
<山かっこ>はコンパイラの標準インクルードディレクトリ(/usr/include、ツールチェーン独自のフォルダ、それに-Iで追加したもの)を探します。ライブラリのヘッダ用です。"引用符"はまずインクルードしているファイルがあるディレクトリを探し、それから山かっこと同じリストにフォールバックします。自分で書いたヘッダ用です。
ほとんどのコンパイラではどちらの形式でもどちらの種類のヘッダも扱えますが、この慣習には意味があります。山かっこは「これは他人のヘッダだ」と言い、引用符は「これは自分のものだ」と言っているのです。取り違えると、自前のヘッダの代わりに古いシステムヘッダを取り込んでしまうことになります。
取り込みはテキストの貼り付けなので、同じヘッダを 2 回インクルードすれば 2 回貼り付けられます。ヘッダにインクルードガードが必要なのはそのためです。それが ヘッダファイル のページが最初に直すことです。
#define:テキスト置換、それだけ
#define NAME replacement はプリプロセッサにこう伝えます。ここから先、トークン NAME が現れたら、代わりに replacement を置け、と。
ここで起きていないことに注目してください。MAX_USERS には型がありません。どこにも格納されません。デバッガで覗くこともできません。これは検索・置換のルールであり、前処理の後、プログラムは文字どおり printf("%s allows %d users\n", "Coddy", 100); を含んでいます。
それはつまり、置換が盲目だということでもあります。次のコードはコンパイルが通り、そして驚くことをします。
#define SIZE 5 + 1
int arr[SIZE]; /* 問題なし:int arr[5 + 1]; */
int total = SIZE * 2; /* 5 + 1 * 2 は 7、12 ではない */
対策、つまり何にでもかっこを付けることは、引数を取るマクロと並んで マクロ のページの中心的なルールです。
置換テキストのない #define は、その名前を「存在するが空」として定義します。置換としては役に立ちませんが、フラグとしては不可欠です。
#define DEBUG /* 定義されているが、何にも展開されない */
これでコードは #ifdef を使って DEBUG が存在するかどうかを尋ねられます。
プリプロセッサの出力を見る
直感を養う一番の方法は、プリプロセッサが実際に何を生成したのかを見ることです。gcc -E は前処理の後で止まり、結果を表示します。
gcc -E hello.c
<stdio.h> をインクルードしたファイルなら、システムによって 700 行から 30,000 行になり、そのほとんどはヘッダ自身の中身です。自分の部分だけを見るには末尾を取りましょう。
gcc -E hello.c | tail -20
次のようなファイルで試してみてください。
#define SQUARE(x) ((x) * (x))
#define LIMIT 10
int main(void) {
int n = SQUARE(LIMIT);
return n;
}
出力の末尾はこうなります。
int main(void) {
int n = ((10) * (10));
return n;
}
マクロはすべて消え、置換されたテキストだけが残っています。マクロがおかしな振る舞いをしたとき、このコマンドは数秒で理由を教えてくれますし、当てずっぽうより確実です。役に立つ相棒が 2 つ。gcc -dM -E - < /dev/null はコンパイラが事前定義しているマクロをすべて列挙し、gcc -E -P file.c は行番号マーカーのノイズを省きます。
なぜエラーが違う行を指すのか
コンパイラが見るのは展開後のテキストなので、マクロの中の間違いは、それが書かれた場所ではなく使われた場所で報告されます。
#define HALF(x) (x / 2
int main(void) {
int y = HALF(8); /* ここでエラーが報告される */
return 0;
}
欠けているかっこは #define の行にあるのに、コンパイラは HALF(8) を含む行について文句を言い、しかも文脈上まったく意味を成さない「予期しないトークン」のようなメッセージを出しがちです。エラーがあり得ないものに見えたら、gcc -E でファイルを展開して本当の行を読みましょう。
現代のコンパイラは助けてくれます。GCC と clang は「in expansion of macro」という注記を出して定義の場所を指し示します。その注記を実際に目にできるよう、-Wall -Wextra を付けてコンパイルしましょう。
事前定義マクロ
プリプロセッサ自身が用意しているマクロもあります。これらは診断に本当に役立ちます。
__FILE__ と __LINE__ は現在のファイル名と行番号に展開されます。アサーションやロギングのマクロが、どこで何がおかしくなったかを報告できるのはこの仕組みです。(__func__ は少し違って、プリプロセッサのマクロではなくコンパイラが提供する本物の識別子ですが、使い方は同じです。)
コンパイラは __linux__、_WIN32、__APPLE__ といったプラットフォームのマクロも事前定義します。システムごとに動作を変えなければならないコードは #ifdef でそれらを検査します。それが 条件付きコンパイル の主題です。
持ち帰るべきこと
プリプロセッサは小さく、そして愚かです。どちらも意図的です。3 つの力、すなわちファイルを取り込むこと、テキストを置換すること、コードをオン・オフすることを与えてくれますが、型チェックのたぐいは一切付いてきません。
そのトレードオフこそが、まず言語機能に手を伸ばせという定番の助言の理由です。定数には可能な限り #define ではなく const int か enum を使い、関数風マクロの代わりに本物の関数を使いましょう。プリプロセッサが本当に適した道具である場面(ヘッダ、移植性のスイッチ、コンパイル時の設定)では、それは掛け替えのないものです。
次は マクロ で #define を本格的に扱います。引数、かっこのルール、そして自分が書いたわけではないコードにテキストを差し込むことに伴う罠です。
よくある質問
C のプリプロセッサとは何ですか?
コンパイルの前に走るテキスト処理の工程です。# で始まる行に従い、#include の位置にヘッダファイルの中身を貼り込み、#define で定義されたマクロを置換し、#if/#ifdef に応じてコードを削除したり残したりします。コンパイラが見るのはその結果だけで、元のファイルを見ることは決してありません。
#include <stdio.h> と #include "myfile.h" の違いは何ですか?
山かっこはコンパイラのシステムインクルードディレクトリを探します。標準ライブラリのヘッダがあるのはそこです。引用符はまず現在のファイルのディレクトリを探し、それからシステムのパスにフォールバックします。ライブラリのヘッダには山かっこを、自分で書いたヘッダには引用符を使いましょう。
プリプロセッサが生成したものを見るにはどうすればよいですか?
gcc -E file.c を実行すると前処理の後で止まり、展開されたソースを表示します。<stdio.h> をインクルードしたファイルでは出力が数千行になるので、パイプでつなぎましょう。gcc -E file.c | tail -30 なら、マクロがすべて置換された自分のコードだけを見られます。
#include は C の文ですか?
違います。ディレクティブは C 言語そのものの一部ではありません。独自の行単位の構文を持ち、セミコロンを取らず、コンパイラがプログラムを解析するころには消えています。だからこそ、マクロの間違いは何でもないように見える行で分かりにくいエラーとして現れるのです。