泣く子とC言語のポインタには勝てない

ポインタの図解

id:temtan さんが「C 言語ポインタ基礎中の基礎の図解」というエントリで、C言語のポインタについて解説している。C言語アセンブラの代用品(←ボカッ)であった頃を思い出すような図を交えて解説されている。「細けぇこたぁいいんだよ」的に言えば、だいたい合っている。まあ、関数へのポインタを取り上げるくらいなら、配列とポインタを比較してみる方がよい気はするけれど。

このエントリの後半で「関数ポインタ」という言葉が出てくる。これについて、はてブでコメントした。

「pointer to int」を「intのポインタ」というのが誤解を招く元という気もする→「関数ポインタ」/printfはポインタではない。/(*fp)("Hello")をfp("Hello")と書けるようになったのは後から。
b:id:mohno:20101109

この後でエントリを少し修正されたようだが、まだ正確ではないので、ここで取り上げてみる。

C言語の“ポインタ”とは

まず、前半でも「int のポインタ」という言葉が出てくるのだが、これだと pointer "of" int という印象がないだろうか。ポインタとは何かと言えば、「point するもの(point-er)」、つまり何かを指し示すものである。そして、(この場合)指し示すものの先に int 型の値があるのだ。ここで「int のポインタ」と呼んでいるものは pointer to int(int へのポインタ)なのである。そして、ここで「関数ポインタ」と呼んでいるものは「関数へのポインタ」(pointers to functions)なのだ*1

関数と関数へのポインタ

printf は何だろう。printf は関数そのものである。printf が「何かを指し示している」わけでは本来ない。printf("Hello, World!");と書くのは、関数を呼び出す形式がそのように決められているからだ。

関数へのポインタとはどんなものか。"pointer to function" なのだから「関数を指し示すもの」である。これは関数を宣言するのと同じやり方で、識別子の前にアスタリスク(*)を付ければよい。たとえば、printf(と同じ引数と戻り値を持つ関数)を指し示すポインタ変数 pfunc を定義したいのであれば、次のように書ける。

    int (*pfunc)(const char *format, ...);

pfunc に printf のアドレスを代入したいのであれば、次のようになる。

    pfunc = &printf;

つまり、「int a;」に対して、「int *p = &a;」と書くように、「int printf(const char *format, ...);」に対して、「int (*pfunc)(const char *format, ...) = &func;」と書くのだ。「*pfunc」にカッコが必要なのは、関数呼び出しの方が優先順位が高いためだ。「int *pfunc(const char *format, ...);」と書くと、「int *」を返すpfuncという関数になってしまう。また、「a」は式の中でそのまま使うが、「printf」はカッコを付けて関数呼び出しする以外に使うことがない。だから関数名が単独で(関数呼び出しではなく&を付けずに)使われたら、&をつけてその関数へのポインタを取り出すのと同じように評価しようということに(後から)決めたのである。

ついでに、関数へのポインタを使った関数呼び出し次のようになる。

    (*pfunc)("Hello, World!");

つまり、pfunc が指し示す先(関数そのもの)を評価して、引数を指定するカッコを付けて呼び出しているのだ。ここでも「*pfunc」にカッコが必要だ。*pfunc("Hello, World")と書いてしまうと、先に pfunc("Hello, World") を実行して、その後で関数の戻り値が指すポインタの内容を評価しようとする*2

そして、これも次のように簡略的に記述できるように(後から)決めたのである。

    pfunc("Hello, World!");

上記を振り返れば、次のように書くこともできる。

    (*printf)("Hello, World!");

だから printf は関数へのポインタみたいなものに見えるかもしれない。それは「関数名だけを取り出したときには関数へのポインタということにしましょう」という取り決めによるもので、関数名が関数へのポインタであるということではない。実際、関数printfの呼び出しと、関数へのポインタpfuncの呼び出しでは、内部でやっていることが違うのである。

printf と &printf の違いを示す例がある。sizeof だ。sizeof は、指定されたもののバイト数を返す予約語で、変数名を指定すれば、その変数名の大きさが、配列名を指定すれば、配列全体の大きさが返される。配列名も、sizeof 以外のところで使われると「先頭の要素を指すポインタ」として評価されてしまうのだが、sizeof だけは配列全体を示すことになっている。

次のコードを見てみると、最初の行はエラーになり、2行目はコンパイルできる。「printf そのもの」といっても、生成される関数のバイト数を事前に確定できるわけではないのでコンパイルエラーになるのだ。「&printf」であれば、「printf という関数へのポインタ」になるので、ポインタの大きさが返される。

    printf("%d\n", sizeof printf);
    printf("%d\n", sizeof &printf);

ポインタは鬼門……だからこそ

C言語のポインタは、C言語の中でも理解されにくい部分だ。私も正確に理解するまでには随分時間がかかった覚えがある。そして今でも“教える側”ですら正確な理解を欠いていることが散見される。何年か前にとあるネットメディアで始まった「C言語入門」では初心者がやりそうな間違いがそのまま“テキスト”として掲載されてしまっていた。

その意味で、たしかに難しい部分ではあるのだけれど、「教える側が間違っていたら、教わる側が正確に理解できることは絶対にない」のも事実だ。“細けぇこと”を言うようだけれど、間違った理解からバグが生まれることもあるので、ぜひとも正しい理解に努めてほしいと思う。

※2010/11/16追記。コミPo!で作った1コママンガを追加しました。

*1:英語で "function pointers" と言わないわけではない。wikipedia には "Function pointer" という項目がある。

*2:もちろん、できないのでコンパイルエラーになる。