Menu

C言語の関数ポインタ:構文、コールバック、qsort

関数にもアドレスがあり、関数ポインタはそれを格納します。このページでは宣言の構文を読み解き、typedef で読みやすくする方法を示し、コールバック、独自の比較関数を使う qsort、そしてディスパッチテーブルへと積み上げていきます。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

コンパイルされたコードもデータと同じくメモリに置かれるので、関数にはアドレスがあります。関数ポインタはそれを格納する変数です - そして関数を格納できるようになれば、それを引数として渡したり、テーブルに並べたり、実行時に選んだりできます。

それが、独自の順序でのソート、イベントハンドラ、プラグインのインターフェース、状態機械を支える能力です。同時に、読み手が思わず目を細めるC言語の構文の筆頭でもあるので、そこから始めましょう。

宣言の構文

普通の関数を用意します。

int add(int a, int b);

そのアドレスを保持できるポインタを宣言するには、名前を (*名前) に置き換えます。

int (*op)(int, int);

内側から読みましょう。op はポインタで、(int, int) を取り int を返す関数へのポインタです。

かっこは飾りではありません。なければ * は代わりに戻り値の型に結び付きます。

int (*f)(int);   // int を取り int を返す関数へのポインタ
int *g(int);     // int を取り int* を返す「関数」 - まったくの別物

端から端まで動かすとこうなります。

どちらの呼び出し形式も動くのは、関数指示子が自動的にポインタへ変換されるからです。op(10, 3) と書きましょう。(*op)(10, 3) という綴りは名残です。

最後の printf について一言。%p はオブジェクトへのポインタを期待し、関数ポインタは形式上は別の系統なので、それを表示するのは厳密には移植性がありません。このキャストは、そもそも動くプラットフォームでコンパイラを黙らせるためのものです。関数ポインタを表示する機会はまずないでしょう。

typedef で読みやすくする

宣言の構文はすぐに醜くなり、typedef が定石の処方箋です。

int apply(BinaryOp op, int x, int y) を、生の形 int apply(int (*op)(int, int), int x, int y) と比べてみてください。意味は同じですが、一目で読めるのは一方だけです。一般的な規則は typedef を見てください。

コールバック:振る舞いを呼び出し側に決めさせる

コールバックとは、適切な瞬間に呼び返してもらうために別の関数へ手渡す関数のことです。これにより、仕事の骨組みを1つのコードが受け持ち、判断は呼び出し側が与える、という分担ができます。

関数ポインタがなければ、count_evencount_positivecount_big と、同じループのコピーを3つ書くことになります。関数ポインタがあれば、ループは一度だけ書かれ、判定がパラメータになります。

本当の見返り:qsort

標準ライブラリの qsort は、実際に使うことになる関数ポインタの例です。あらゆる型の配列をソートしますが、それができるのは、要素についてサイズと、あなたが提供する比較関数以外は何も知らないからです。

void qsort(void *base, size_t nmemb, size_t size,
           int (*compar)(const void *, const void *));

比較関数は2つの const void * - 2つの要素のアドレス - を受け取り、1つ目が先に来るなら負の数、同順なら0、後に来るなら正の数を返します。

その比較関数から持ち帰るべき習慣が2つあります。return x - y; とは決して書かないこと - 賢く見えますが、大きな値や負の値でオーバーフローし、符号を誤って配列の並びを狂わせます。そしてデリファレンスの前にキャストすることであって、後ではありません:*(const int *)a と書きます。void * に対する *a には読むべきものがないからです。

同じ仕組みで、構造体を好きなフィールドでソートできます。

(q->score > p->score) - (q->score < p->score) という定石は、引き算もオーバーフローもなしに、ちょうど -1、0、1 のいずれかを返します。そして strcmp はすでに正しい戻り値の約束を持っているので、文字列の比較関数は1行で済みます。

関数ポインタのテーブル

関数ポインタの配列は、if/else の連鎖を表引きに変えます。インタプリタがオペコードを振り分ける方法であり、メニューシステムがコマンドを実行する方法でもあります。

演算を追加することが、switch を編集することではなく1行を足すことになりました。これを構造体と組み合わせれば、C言語における標準的なプラグインの形になります - 関数ポインタの構造体こそ、この言語における「インターフェース」の意味です。

噛みついてくるもの

シグネチャは完全に一致しなければなりません。 関数ポインタの型には、パラメータの型と戻り値の型が含まれます。一致しない関数を代入するのは制約違反であり、型の合わないポインタ越しに呼び出すのは、たとえ「動いて」いても未定義動作です。警告を黙らせるために比較関数のシグネチャをキャストしてはいけません - 比較関数のほうを直しましょう。

ヌルの関数ポインタも、やはりヌルポインタです。 データのポインタとまったく同じように、NULL で初期化し、呼ぶ前にチェックしましょう。

if (handler != NULL) {
    handler(event);
}

そのチェックが元を取る理由は ヌルポインタ を見てください。

void ポインタは型安全性を失わせ、型システムはあなた自身になります。 qsort の比較関数の中では、間違った型へキャストすることを何も止めてくれません。double の配列を compare_ints でソートしても、きれいにコンパイルが通り、意味不明な結果を出します。

宣言子の構文はネストします。 関数ポインタを返す関数は int (*get_op(char c))(int, int); です。こう書き始めていることに気づいたら、typedef に手を伸ばしましょう。

typedef int (*BinaryOp)(int, int);
BinaryOp get_op(char c);           // 同じものを、読めるかたちで

よくある質問

C言語で関数ポインタはどう宣言しますか?

関数名が来る位置に (*名前) を置いて、関数のシグネチャを書きます:int (*op)(int, int); は、2つの int を取り int を返す関数へのポインタとして op を宣言します。*op を囲むかっこは必須です - なければポインタを返す関数の宣言になってしまいます。

ポインタ越しに関数を呼ぶには?

op(3, 4) でも (*op)(3, 4) でも構いません - 関数指示子は自動的にポインタへ変換されるので、どちらも合法で同じ動きをします。現代のCコードでは素の op(3, 4) の形を使います。

C言語のコールバックとは何ですか?

別の関数に渡しておいて、あとで呼び返してもらう関数のことです。qsort が典型例で、比較関数を渡すと、qsort は2つの要素のどちらが先に来るかを知りたくなるたびにそれを呼びます。それが、1つのソートルーチンをあらゆる型で動かせる理由です。

関数のアドレスを取るとき & は必要ですか?

不要です。関数名はすでにその関数へのポインタに変換されるので、op = add;op = &add; は等価です。ほとんどのCコードは & を省きます。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める