初級C言語Q&A(8)

初出: C MAGAZINE 1996年1月号
Updated: 1996-03-12

[1つ前] [1つ後] [質問一覧] [記事一覧] [ホームページ]


前処理

 C言語を強力にしている機能の一つに、前処理があります。 #include を使ってヘッダーファイルを分離したり、 #ifdef でマシン依存のコードを使い分けるようにして一つのプログラムを複数の環境に 対応させるといったテクニックは、多くのプログラムで使われています。今回は、 これらの前処理に関係する、よくある質問を取り上げてみます。


前処理一般


Q 【前処理とは何か】

 前処理とは何か。

 プリプロセッシングとも呼びます。昔のC言語処理系は、まずソースコードをプ リプロセッサと呼ばれているプログラムにかけてからコンパイラを通す、という ように段階を経た処理を行うものが主流でした。このプリプロセッサが行う処理 が“前処理”に該当します。最近のコンパイラは、一度で前処理からコンパイル までを行うものが多くなりましたが、C言語の仕様の中に、前処理という段階が依 然として残っており、プログラマーは見かけ上は前処理があってその後コンパイ ル、という段階があるように意識してコーディングする必要があります。

 前処理の基本的な内容は、マクロの置き換えやインクルードファイルの読み込 み、コメントの空白化などで、その後に字句解析、コードの置き換えなどのコン パイルが行われることになります。

JISでは、前処理を次のように定義しています。「処理系は、ソースファイルの 一部分を条件によって処理したり読み飛ばしたり、他のソースファイルを組み込 んだり、マクロを置き換えたりすることができる。これらの処理は、概念的には その翻訳単位の翻訳の前に行われるので、前処理(preprocessing)と呼ぶ。」


Q 【sizeof】

 次のコードはなぜうまく動作しないのか。
    #if (sizeof(int) == 2)
        ...
    #else
        ...
    #endif

 前処理は、コンパイルの前に実行されますから、コンパイルしなければ分から ない情報を参照することはできません。この場合、 sizeof(int) の値が決定するのはコンパイル時ですから、 #if の処理中にはその値を用いることができません。

 このコードは int が2バイトか4バイトかによって振る舞いを変えたいという目的があるようですが、 それを実現するのであれば、 limits.h をインクルードして INT_MAX の値で判断するという手があります。もし可能なら、 int のサイズに依存しないようなコードを書くことがベストです。

(参考 c.l.c FAQ 10.13)


Q 【#ifdef】

 長期にわたって大勢が手を加えたコードには #ifdef がたくさん現れて、実際にコンパイルした時に一体どこが有効なのか分からない ことがある。そこで、プリプロセッサだけ通してみたら、今度は、 #include#define が展開されてしまい、余計わけが分からない。 #ifdef だけを処理するようなことは可能か?

 そのための特殊なプログラムがいくつか出回っているようですが、プリプロセ ッサだけで実現することは、特にそのような機能が追加されていない限り不可能 でしょう。

(参考 c.l.c FAQ 10.18)


Q 【#if 0】

 #if 0 がたくさん出てくるコードを見た。 #if 0#endif の間のコードは無視されるはずだから、無駄ではないのか。

 このようなコードは、作成途中で一時的に使ったコードや、デバッグ用に使う ためのコードを、最終的に無視するために使われることが多いようです。確かに、 このコードはコンパイル時に無視されますから、削ってしまっても構わないので すが、経験的に、デバッグ用のコードなどは特に、未練が残ってしまい、なかな か消せないもののようです。

 #if 0#endif の間のコードは、コンパイルの前処理の早い段階で読み飛ばされるので、このコ ードがあるためにコンパイルの負荷が高くなるといったことは、まず考えなくて も大丈夫です。


Q 【コメントと#if 0】

 #if 0#endif の間にコメント /* , */ で区切らないコメントを書いてはいけないと言われた。

 #if 0 〜と #endif の間も、厳密に言えば、Cのプログラムのコードとして扱われます。例えば、コ メントを空白に置き換える処理は、 #if の処理の前に行われます。

 これに対して、「 /* 」と「 */ 」で囲んだ個所は、まさにコメント以外の何物でもありません。コメントを書く 場合には、こちらを使うのが原則です。

 以前連載していた「ABC of C」のソースには、 #if 0#endif を使ってドキュメントを埋め込んだものが多数ありますが、これらは本来望まし い書き方ではありません。ただし、C言語にはコメントがネストできないという 制約があるため、コメント中にどんなことでも書こうとすると限界があります。 特に、C言語のソースの説明をソースの中に書くという場合には、どうしようもな い場合もあるため、邪道を承知でそのような書き方をしています。


マクロ


Q 【値の交換】

 二つの変数に入っている値を交換する一般的なマクロはあるか。

 結論からいうと、ありません。最大の壁は、一時変数を使って値を交換するた めには、その変数の型があらかじめ分かっていなければならないという所にあり ます。

 例えば、 int 型の変数であれば、実現したいのは次のような処理となるでしょう。

  #define swap(a, b) { int tmp; tmp = a; a = b; b = tmp}
 この処理が int 以外の型の変数について適用できないことは自明です。逆に考えれば、交換した い型に応じて swap_intswap_longswap_double のようなマクロ を定義すれば、それぞれの型に応じたマクロを使うことは不可能ではありません。

 ただし、このマクロ定義は不完全です。後述の 【マルチステートメント】 の項を参考にしてください。

 値を交換するために必ずしもマクロを使う必然性はありません。マクロを使わ ずに実現する方が、現実的には簡単で確実なこともあります。

(参考 c.l.c FAQ 10.3)


Q 【XORのトリック(1)】

 次の歴史的に有名なコードは、なぜ値を交換することになるのか。また、使わ ない方がいいと言われたが、なぜか。
  #define swap(a, b) (a ^= b, b ^= a, a ^= b)

 自分自身との排他的論理和は、0になります。グラフィックカーソルをXORを使 って描画するというテクニックがいろんな意味で有名です。XORを使って二度同じ イメージを描画すると、何もしなかったのと同じ結果に戻ってくることを利用し ています。

ab の値の中身を追跡してみると、種が簡単であることが分かります。

                   aの内容         bの内容
 最初                a               b
  a ^= b直後         a^b              b
  b ^= a直後         a^b           b^(a^b) = a
  a ^= b直後    (a^b)^a = b           a
 排他的論理和は交換法則、結合法則が成り立つので、 b^(a^b) = b^a^b = b^b^a = 0^a = a となるのです。

 これを使えば型を気にしないマクロが書けそうですが、話はそう簡単ではあり ません。大きな落とし穴は、一つの変数を指定した場合、つまり swap(a, a) のような場合です。

                   aの内容         bの内容
 最初                a               a
  a ^= b直後         a^a = 0          0
  b ^= a直後          0             0^0 = 0
  a ^= b直後         0^0 = 0          0
 このように、途中で値が0になってしまって、元の値が消滅してしまいます。

Q 【XORのトリック】

 次のマクロは使ってはいけないと言われた。なぜか。
  #define swap(a, b) (a ^= b ^= a ^= b)

 見た目のトリッキーさを競うという意味では、この方が奇抜です。ただ、この ように書いた場合、副作用が複数の個所で発生してしまうため、C言語の仕様とし ては未定義のふるまいとなってしまいます。

 現実に動作する処理系も多いと思われますが、話の種ならともかく、実用に使 うコードとしては用いない方が賢明でしょう。

(参考 c.l.c FAQ 10.3)


Q 【トークンの連結】

 古いソースコードに、次のようなコードが含まれていた。
    #define  Paste(a,  b)  a/**/b
 コンパイラを新しいものにして、再コンパイルしたら、思ったように処理され なくなってしまった。

 古いコンパイラの中には、コンパイル時にコメントを消滅させてしまうような 仕様のものがありました。このようなコンパイラを使えば、質問のようなマクロ はトークンの連結に使うことができました。ところが、ANSIの規格では、コメン トは空白に置き換えるという仕様になったため、このマクロでは間に空白が入っ てしまい、トークンは分離したままという結果になるのです。

ANSI Cには、 ## という書き方追加されたので、これを使えば、やりたいことを次のようにして実 現することができます。

#define Paste(a, b) a##b

(参考 c.l.c FAQ 10.20)


Q 【マルチステートメント】

 マルチステートメントのマクロの定義はどのようにして書けばよいか。すなわ ち、複数の処理を一つのマクロとして定義したい。

 単に複数の行に分けて書きたいというのであれば、行の最後に '¥' を付ければ、前処理時に一行とみなされますから、見た目に複数行に書くという 目的は達成されます。しかし、ここで問題なのは、関数呼び出しと同じように使 えるマクロが欲しいということでしょう。

 例えば、 print_val(i, value) というマクロを考えてみましょう。このマクロは、 i が1の時には valuechar 型とみなして、文字そのものを、 i が2の時には short 、4の時は long とみなして、その値をプリントするものとします。残念ながら、引数の型が不定 になってしまうため、C言語の仕様では関数で書くことはできません。イメージと しては次のリストのようになります。

    print_val(i, value)
    {
        if (i == 1)
            printf("%c", value);
        else if (i == 2)
            printf("%d", value);
        else if (i == 4)
            printf("%ld", value);
    }
 このようなマクロを実現する古典的な方法は、 do-while(0) を使うというものです。
    #define print_val(i, value) do { ¥
        if (i == 1) ¥
            printf("%c", value); ¥
        else if (i == 2) ¥
            printf("%d", value); ¥
        else if (i == 4) ¥
            printf("%ld", value); ¥
    } while (0)
 これは、次のように書いてもよさそうに見えます。
    #define print_val(i, value) { ¥
        if (i == 1) ¥
            printf("%c", value); ¥
        else if (i == 2) ¥
            printf("%d", value); ¥
        else if (i == 4) ¥
            printf("%ld", value); ¥
    }
 しかし、これは失敗することがあります。なぜなら、関数を呼び出す場合には、 プログラマーは殆ど無意識のうちに最後にセミコロンを書くでしょう。例えば、 次のように書くでしょう。
    print_val(2, i);
 しかし、後者のようなマクロ定義だと、ブロックそのものが文とみなされるの で、マクロを展開した時に、最後のセミコロンが冗長になります。これが、場所 によっては邪魔になって、予期せぬ結果となることがあります。特に、 else の付いた if 文の後にこのようなマクロを用いると、文法エラーになってしまいます。

 マクロの内容が式の羅列で書けるのであれば、このような工夫をしなくても、 コンマ演算子で区切ったマクロを使うことができます。

(参考 c.l.c FAQ 10.4)


Q 【デフォルトマクロの一覧】

__DATE____TIME__ のように、特に記述しなくても最初から定義されているマクロの一覧を知りたい のだが、どうすれば知ることが出来るだろうか。

 不思議なことに、標準的な方法は存在しません。コンパイラに付属しているマ ニュアルに一覧が書いてあるかもしれません。UNIXのstringsというツールを使っ て、コンパイラの実行ファイルから可読文字列を拾いだすというハッカー流の探 し方が現実的かもしれません。

なお、ANSIの仕様として定義されているマクロは、以下のものがあります。

  __LINE__ 現在のソース行の行番号。
  __FILE__ ソースファイル名。
  __DATE__ コンパイルした日時。
  __TIME__ コンパイルした時刻。
  __STDC__ 規格合致処理系であることを示す定数。1である。
(参考 c.l.c FAQ 10.19)

Q 【可変数の引数を取るマクロ】

 可変個の引数を取るマクロを書くには、どうすればよいか。

 引数を一つだけ取るようなマクロを定義して、使う時に二重の括弧を使って引 数を渡すという古典的トリックがあります。
    #define DEBUG(args) (printf("DEBUG:  "),  printf  args)

    if (n != 0)
        DEBUG(("n  is  %d¥n",  n));
 この場合は、プログラマーが二重に括弧を使うことを忘れると失敗するという リスクがあります。プログラムを書く時点で引数の数が分かっているのであれば、 引数の数に応じたマクロをそれぞれ定義してしまうか、最大の引数の数に対応し たマクロを用意しておき、それより少ない引数の場合にはダミーの引数を与えて しまうという手もあります。

(参考 c.l.c FAQ 10.26)


Q 【可変個の引数を持つ関数】

 可変個の引数を取る関数は、どのように書けば作ることができるか。

 <stdarg.h> をインクルードして、 va_startva_argva_end マクロを使って実現するのが基本です。これらのマクロの使い方は、リファレン スマニュアルに例が示されていると思いますので詳細は、それを参照してくださ い。一例を示します。
/* 複数の文字列を改行して表示する。
 * 最後はNULLを指定する。
 * ex. printfx("一行目", "二行目", NULL);
 */
#include  <stdio.h>
#include  <stdarg.h>

void printfx(char *str, ...)
{
    char *p;
    va_list argp;

    va_start(argp, str);

    for (p = str; p != NULL; p = va_arg(argp, char *))
        printf("%s¥n", p);

    va_end(argp);
}
(参考 c.l.c. FAQ 15.4)

マクロ引数の評価


Q 【括弧】

 マクロ定義を見ると、「 () 」がずいぶんたくさん付いているような気がするが、なぜそんなに括弧を付ける 必要があるのか。

 おそらく、次のような定義を指しているのでしょう。
    #define power(a) ((a) * (a))
 この定義にある落とし穴は、 power(a + b); のようなマクロの使い方をした場合を考れ分かります。マクロの処理は、単に文 字を置き換えるだけなので、括弧がないと変な個所が優先的に結合することがあ るのです。

 原則としては、引数自体に括弧を付け、マクロ全体をさらに括弧で囲むように すれば安全です。


Q 【評価の回数】

 次のコードは安全か。
    if (isspace(c = *s++) == 0)
        putchar(c);

 マクロの評価の回数には気を付けなければなりません。次の例で考えてみまし ょう。
    #define power(a) ((a) * (a))
 このマクロ power は、 a を二回評価します。しかし、 power(*p++); のようにコードを書いた時に、 *p++ が二回評価されることに気付かないかもしれません。しかも、その結果はC言語と しては正しくないプログラムになります。なぜなら、マクロを展開した結果は次 のような未定義のコードになるからです。
    ((*p++) * (*p++))
 この危険を避けるために、プログラマーはマクロの評価回数を知った上でコー ドを書くか、あるいは、マクロの評価回数が複数でも構わないようなコードを書 くことになります。これは面倒な心構えです。特に、多くの場合マクロは関数呼 び出しと見分けが付かないことが、トラブルの原因となるでしょう。

 そこで、もう一つの逃げ道として、マクロを書く時には引数の評価回数が1以下 であるようにする、ということが考えられます。

 さて、実は、 isspace マクロは、引数の評価回数が1であることが仕様により保証されていますから、

    isspace(c = *s++)
 が、二回以上評価されることはなく、期待通りの結果が得られるはずです。

 もっとも、読みやすさを重視するなら、ここは行を分けて、次のように書いた 方がよいと思います。一般論として、一度にたくさんのことをしようとすると、 それだけ分かりにくいコードになることが多いからです。

    c = *s++;
    if (isspace(c) == 0)
        putchar(c);

#include


Q 【インクルードファイル】

#include でファイルを読み込む場合に、ファイル名を "" で囲むのと <> で囲むのとでは、何が違うのか。

 #include <ファイル名> の形式で指定した場合、このファイルは処理系によって定められた標準的なパス から検索されます。通常、これは/usr/includeなどのパスになっています。また、 コンパイラのオプションやカスタマイズファイルの定義によって、パスをユーザ ーが指定できる場合が殆どです。

#include " ファイル名 " による指定も同様ですが、通常、この指定の場合はカレントディレクトリを検索 対象とします。

 標準ヘッダを指定する場合は <> 、ユーザ定義ヘッダを指定する場合には "" を使うのが一般的な用法です。


Q 【ヘッダーのネスティング】

 ヘッダーの中で、さらに別のヘッダーを #include することはできるか。

 言語仕様としては、このようなネスティングは許されており、ANSIの仕様を満 たすためには、少なくとも8レベルのネスティングが可能でなければなりません。

 仕様ではなく主義の問題としては、ネスティングをすべきでないという主張と、 しても構わないという主張が、真っ向から対立しています。どちらの言い分にも 一理あり、決定的な結論はありません。


Q 【ヘッダーの二重読み込み】

 ヘッダーの中からヘッダーを読み込むような書き方をしていたら、二重定義の エラーが出てしまった。同じヘッダーを二度読み込んでも問題ないように書くこ とはできるか。

 最初に読み込んだ時点で、そのヘッダー内で特別なマクロを定義してしまうと いう古典的な方法があります。
  #ifndef already_defined
  #define already_defined
    ...
  #endif
 このようにすれば、最初の読み込みで already_defined というマクロが定義されるため、二回目に読み込んだ時には、 #ifndef#endif の間に書かれている定義は無視されます。従って、二重定義になることはありま せん。

(参考 c.l.c. FAQ 10.7)


Q 【標準ヘッダーの読み込み】

 標準関数を使う時に、どの標準ヘッダーを #include すればよいのかよく分からない。簡単に知る方法はないか。

 最も簡単なのは、リファレンスマニュアルを見ることです。マニュアルには、 その関数を使う時にどのファイルを #include すればよいか書いてあります。

 また、実際にヘッダーファイルを検索すれば、どこに必要な定義があるか知る ことは簡単です。grepなどのツールを使ってもよいし、エディタで開いて検索し てみてもよいでしょう。

 標準ヘッダーファイルは、ある程度まとまった処理毎に分類されていますから、 それらを覚えるのは対した手間ではありません。最も典型的なものとして、以下 のようなものはすぐ覚えるはずです。

  <stdio.h>  入出力
  <stdlib.h> malloc、exit等の関数
  <string.h> 文字列処理関数
  <math.h>   数学関数
 もし、必要なヘッダーファイルが #include されていなければ、コンパイル時に関数プロトタイプが定義されていないという 警告メッセージが表示されるはずですから、それを見て何か #include し忘れたということに気づくはずです。

Q 【ヘッダーを全部読む】

 標準ヘッダーをその都度必要なだけ #include するのは面倒なので、全ての標準ヘッダーを #include するようなall.hというヘッダーを作って、コンパイルの時に はこれをまず #include することにすれば、頭を使わずに済むのでは。

 ヘッダーファイルは、時にはかなり大きなファイルとなるし、コンパイルの時 間や資源を浪費する原因になります。基本的な考えとして、必要最低限のものだ け処理すれば、これらの節約にはなります。

 ただ、最近の処理系は、資源が豊富で、かつ処理速度も速くなったため、場合 によっては全てのヘッダーを無駄なものも含めて全て読み込んでしまっても、さ ほど影響はないかもしれません。いちいちどのヘッダーを読み込むか考えなくて 済むので、かえって効率的だ、という意見の人もいます。

 コンパイル時に気にならないのなら、特に実害はないでしょう。コンパイル時 間がやけに長いとか、途中でメモリ不足になって中断してしまうのであれば、考 え直した方がよいでしょう。


※ c.l.c FAQ : comp.lang.c FAQ list
URL: http://www.eskimo.com/‾scs/C-faq.top.html
(C) 1995-1996 Phinloda, All rights reserved
無断でこのページへのリンクを貼ることを承諾します。問い合わせは不要です。
内容は予告なく変更することがあります。