関数ポインタ †
C/C++でプログラムを書いているとき関数を入れる変数や配列がほしいと思ったことはないですか?
実は関数ポインタを使うとほぼ同等のことが出来ます。
関数ポインタとは †
プログラム実行時、変数や関数はコンピュータのメモリ上に配置されます。そのときどこに配置されているかを表すのがアドレスでした。
&変数名
これが変数のアドレスを取得する方法です。scanf関数などで使ったことがあると思います。
関数のアドレスを取得も同じです。
&関数名
アドレスを取得したらそれを入れる変数が必要になります。変数のアドレスを入れる変数がポインタでした。
int num = 10;
int *pVar = # // pVarという名前のポインタを宣言し、numのアドレスで初期化する
*pVar = 20; // pVarを介してnumに20を代入する
同様に関数のアドレスを入れる変数があります。それが今回のテーマ関数ポインタです。
ポインタと関数ポインタは区別して使い分けてるので注意してください。
使用例 †
関数ポインタの例をあげます。
double AAA(int a) {
return a;
}
double (*pFunc)(int) = &AAA; // pFuncという名前の関数ポインタを宣言し、AAAのアドレスで初期化する
double d = (*pFunc)(30); // pFuncを介してAAA(30)が実行され、dにはAAA(30)の戻値が入る
宣言が複雑ですね。ポインタのときポインタが指す変数の型を指定したように、関数ポインタは関数ポインタが指す関数の型をしているのです。
関数の型はその関数の引数と戻値で表されます。引数の型・数、戻値の型の全てが一致しなければ同じ型とはみなされません。
(ここでは"関数の型"と表現しましたが、一般には関数の引数の型・数、戻値の型の組み合わせのことをシグネチャと言います。)
上の例ではpFuncは『int型の引数を1つ取り、doubleを返す』関数しか代入できません。
(*pFunc)の括弧を忘れて
double *pFunc(int);
とすると『int型の引数を1つ取り、doubleへのポインタを返す』関数の宣言になるので注意してください。
ちなみに関数のアドレスを取得するときの&と、関数ポインタを介して関数を実行するときの(*と)は省略可能です。
double AAA(int a) {
return a;
}
double (*pFunc)(int) = AAA; // pFuncという名前の関数ポインタを宣言し、AAAのアドレスで初期化する
double d = pFunc(30); // pFuncを介してAAA(30)が実行され、dにはAAA(30)の戻値が入る
これは上記の例と同じです。僕はこっちの方が直感的だしタイプ量も少ないので好きです。
関数ポインタの配列 †
ではポインタ配列と関数ポインタの配列の例を見てみましょう。
int *pVarArray[4]; // ポインタの配列
double (*pFuncArray[4])(int); // 関数ポインタの配列
pFuncArray[0] = AAA;
double d = pFuncArray[0](40);
これで関数を変えながらのループなどができますね。
さて、ポインタには『ポインタへのポインタ』というのがありました。関数ポインタに対しても同様に『関数ポインタへのポインタ』を作ることができます。
double (**ppFunc)(int) = &pFunc;
double d = (*ppFunc)(50);
あくまでも『int型1つを引数に取り、doubleを返す関数ポインタ』へのポインタなので使い方は通常のポインタと同じです。
typedefで名前をつける †
関数ポインタでややこしいのは宣言だと思います。なのでtypedefで簡単な名前をつけてしまいましょう。
typedef void (*HOGE)(int,int); // int型の引数を2つ取り、何も返さない関数の型にHOGEという名前をつける
HOGE hoge; // void (*hoge)(int,int);という宣言と同じ意味
HOGE hogeArray[8]; // HOGE型の配列
HOGE *pHoge; // HOGE型へのポインタ
ずいぶん簡単になりました。ソースコードの可読性のためにも関数ポインタはtypedefするべきだと思います。
関数を引数に持つ関数 †
正確には関数ポインタを引数に持つ関数を作ることもできます。例えば上記のHOGE型を使えば
void Foo(int x,HOGE h) { ←int型1つとHOGE型1つの引数を持ち、何も返さない関数
h(x,60);
}
のような感じです。とても柔軟性があり、ダイナミックな処理が可能になりますね。
実際にC言語標準ライブラリにはqsort関数やbsearch関数のような関数を引数に取る関数があります。
例としてqsort関数の使い方をあげておきます。
qsort関数は配列内の要素を比較関数を用いて並び替えます。並び替えのことをソート(sort)と言い、qsort関数はクイックソート(quick sort)というアルゴリズムでソーティングします。
#include <stdio.h>
#include <stdlib.h> /* qsort関数 */
int comp(const int* a,const int* b) { /* int型変数を昇順(小さい順)にソーティングするための比較関数 */
return *a - *b;
}
void main() {
int i;
int a[10] = { 12 , 64 , 34 , 11 , 5 , 82 , 4 , 99 , 70 , 36 };
typedef int (*COMP)(const void*,const void*);
qsort(a,10,sizeof(int),(COMP)comp);
for ( i = 0 ; i < 10 ; ++i ) {
printf("%d\n",a[i]);
}
}
/* 他のデータも同様
typedef struct Point {
int x,y;
} Point;
int compPointX(const Point* p,const Point* q) { /* Point型変数をxについてソーティングするための比較関数 */
return p->x - q->x;
}
qsort関数の第3引数とcomp関数のシグネチャ(関数の型)が違うためCOMP型にキャストしていることに注意してください。
他にもコールバック関数という仕組みは関数ポインタが用いられています。
構造体に関数を持たせる †
C言語でC++のクラスもどきを作ってみましょう。と言っても継承とかは出来ないので構造体に関数を持たせるだけですが(´・ω・`)
struct Player;
typedef void (*MOVE)(struct Player*,int,int);
typedef void (*DRAW)(struct Player*);
typedef struct Player {
int x,y;
MOVE move;
DRAW draw;
} Player;
void MovePlayer(Player* this,int dx,int dy) { /* C++だとthisは予約語 */
this->x += dx; this->y += dy;
}
void DrawPlayer(Player* this) {
/* 描画処理 */
}
void main() {
Player player = {0,0,MovePlayer,DrawPlayer};
player.move(&player,10,20);
player.draw(&player);
}
ポリモーフィズム(多相性)的なことができますね。
感想・質問・要望・間違いの指摘など †
何かあればお気軽にどうぞ。
- すばらしいです。ようやく関数ポインタのtypedefが理解できました -- Dr_Radialist?
- 関数ポインタってすでに関数がコールされてる状態なの? --
- 関数のアドレスを関数ポインタに代入するだけでは関数はコールされません。関数ポインタにはあくまで関数のアドレスが入っているだけです。通常の関数コールも関数ポインタを介した関数コールも「関数アドレス(引数)」の形で書くことに違いはありません。 -- formula