ポインタとメモリと型(構造体)の関係 (4)
今回は前回の続きで、ちょっと高度なプログラミング技術を身につける上で重要になってくる、 関数ポインタというものを扱います。
関数ポインタ
さて、ふつうの入門書では扱っていないかもしれませんが、関数にもポインタというものがあります。
関数のポインタというと、イメージがつかめない人も多いかと思いますが、
プログラムというものをアセンブラとかのレベルで考えると※1、一列に並んだ命令の集まりといえます。
その中で関数というのは、その命令列の中のどこかにかかれている命令の小さな集合です。
関数呼び出しを一種のジャンプ命令(実行中に処理対象がこの集合の先頭へ移り、終わると戻る)と考えれば、
ポインタの値はジャンプ先の位置情報と考えることができます
(サブルーチンという言葉を知っている人には簡単に理解できますね。 GOSUB~RETURN とか)。
高級言語を使っていると忘れがちですが、いわゆるノイマン型コンピュータにおいてデータと命令は同等に扱われるので、
データの位置を表すポインタと、命令の位置を表すポインタは同等のものとして扱えます。※2
では、ポインタの型はどうなっているかというと、C言語の場合、関数に必要な情報はジャンプ先に加えて、
引数と戻り値の型情報が必要で、これらをあわせて型情報として保持しています
(型情報がインターフェイスを表し、実体をポインタの値として保持しているわけです)。
逆に言えば、通常の関数は、この型の情報とジャンプ先ポインタをもっている定数のようなものとも考えられます。
※1 偉そうに書いておいてあれですが、私はアセンブラでプログラムを書いたこともありませんし、
コンパイラ技術に関しても全くの素人で、ここで書いているのはあくまで私の中での解釈にすぎません。
あくまで解釈の仕方の一つとしてとらえてください。
※2 このポインタをたどって実行コードをみることもできますし、
実行コードをデータとして書き込んでそれをネイティブ実行するなんてこともできます(JIT とかの手法)。
高級言語でありながらこういうことができるC言語ってスゲーなと思ったり。
関数のポインタがあるのは分かった、で、何に使うんだ?
っていう疑問が出てきますが、関数の引数を考えてみてください。
あれは、この値を使って処理してくれという命令ですが、
ここで変数と同様に関数ポインタを引数に指定すると、
この関数を使って処理してくれという命令を書くことができる。
すなわち、「機能」を数値などと同等に受け渡しできると言うことなのです。
オブジェクト指向言語では当たり前ですが、これを使えば、実行中に関数の機能を変えたりできます。 また、ライブラリを作るときに、ライブラリの中の関数で「~が起こったときの処理はライブラリを使う人が決めてほしい」 といった時にも、引数で関数ポインタを渡してもらえればこういった記述が可能になるわけです (いわゆるコールバック関数の登録)。 こういったプログラミングスタイルは、一般的なC言語の「関数を呼び出す」形とは異なっています。 「関数を呼び出してもらう」形でプログラミングを行います。 ここでは、プログラマとライブラリ制作者で立場の逆転が起こっています(オブジェクト指向プログラミング的な考え方)。
では、まず解説に入る前に関数ポインタの宣言の仕方についてはじめに示しておきます。 以下は、2つの int 型の引数をとり、int 型の戻り値を持つ関数ポインタ hoge の宣言です。
int (*hoge)(int, int);
記述が複雑ですが、hoge が変数名で、 int (*)(int, int) が型名です。
通常の変数の宣言とは異なり、変数名を取り囲む形で型を記述しています
(私が初めてこの記述をみたときは訳がわからず投げ出してしまいました・・・)。
hogeの頭に着いているアスタリスクはポインタを表すものですが、
そのまま記述してしまうと、戻り値指定の int と結びついて
int 型ポインタを返す関数の宣言になってしまうため、括弧で区切る必要があります。
あと、関数は 関数名() でその機能の呼び出しになりますが、 関数名 のみの記述で関数ポインタの参照となります。
qsortを使った使用例
では使用例を使って説明していきます。
関数ポインタの使用例を説明するのにちょうどいいのは、C言語標準ライブラリ(stdlib.h)の qsort 関数です。
これは、配列のクイックソートを行ってくれる関数ですが、まずはその書式を見てください。
(クイックソートがなんだか分からないという人はまずそっちを勉強してください)
void qsort(void *base, size_t n, size_t size, int (*fnc)(const void*, const void*));
さて、この関数は、 void 型ポインタでソートする配列(引数: void *base)を受け取っています。
前回解説したように void 型ポインタはどんなポインタでも受け入れることができます。
つまり、この関数はどんな値の配列であってもソートができるというわけです。
その後の引数は、要素の数(引数: sizez_t n )と、一つ要素の大きさ(引数: size_t size )です。
ここまでの引数でソートする配列を表現できますね。
ここまで分かれば、たとえば2番目の要素と3番目の要素を入れ替える、みたいな操作を記述することができます。
しかし、ソートというものは大きい順(小さい順)に並べる処理な訳ですが、そのためにはどうしても比較という処理が必要です。
これは、どのようなデータを扱うかが分かるまでは記述できませんので、使う側の人に決めてもらう必要があります。
しかし、通常の数値渡しなどではこれを実現できません。ソートを記述するためには比較するという機能を渡してもらう必要があるのです。
それが4番目の引数 int (*fnc)(const void*, const void*) です。
こうすれば、データの種類によって別の機能を用意しなければならなかった比較部分を引数の関数に任せられるので、
どんな配列でもソートできる関数が記述できるわけです。
また、大きいとき小さいときの戻り値を逆転させることで、昇順、降順も同時に指定できるようになっています。
逆に使う側からすれば、扱うデータごとにいちいちソートすべてを実装するのではなく、
そのデータを比較するという非常に小さな実装をするだけでソートが使えるようになるわけです。
ちなみに、指定する比較関数は昇順に並び替えるものとしたとき、 第一引数が第二引数に対して、1)小さい、2)等しい、3)大きい、とき 1)ゼロより小さい、2)ゼロ、3)ゼロより大きい、整数を返すようにします。 二つの大きさが等しいときの出力の並び順は未定義となります。
では、実際の使用例です。
まずは、非常に基本的な int 型、 float 型と、 int 型配列の各要素へのポインタの配列
(ややこしいけど、要するにポインタの指す変数で評価する)をソートするパターンを示します。
#include <stdio.h>
#include <stdlib.h>
#define N 10
int int_cmp (const void *, const void *);
int float_cmp(const void *, const void *);
int pint_cmp (const void *, const void *);
main(){
int i;
int a[N];
float b[N];
int *c[N];
for(i = 0; i < N; i++){
a[i] = rand() % 20;
b[i] = (float)a[i] / 10.0f;
c[i] = a+i;
printf("%3d ", a[i]);
}
puts("");
/* int型のソート */
qsort(a,N,sizeof(int),int_cmp);
for(i = 0; i < N; i++)
printf("%3d ",a[i]);
puts("");
/* float型のソート */
qsort(b,N,sizeof(float),float_cmp);
for(i = 0; i < N; i++)
printf("%0.1f ",b[i]);
puts("");
/* int型ポインタのソート */
qsort(c,N,sizeof(int*),pint_cmp);
for(i = 0; i < N; i++)
printf("%3d ",*c[i]);
puts("");
}
/* int型の比較 */
int int_cmp (const void *arg0, const void *arg1){
int a = *(int*)arg0;
int b = *(int*)arg1;
if(a == b) return 0;
else if(a > b) return 1;
else return -1;
}
/* float型の比較 */
int float_cmp(const void *arg0, const void *arg1){
float a = *(float*)arg0;
float b = *(float*)arg1;
if(a == b) return 0;
else if(a > b) return 1;
else return -1;
}
/* int型ポインタの比較 */
int pint_cmp (const void *arg0, const void *arg1){
int a = **(int**)arg0;
int b = **(int**)arg1;
if(a == b) return 0;
else if(a > b) return 1;
else return -1;
}
結果
3 6 17 15 13 15 6 12 9 1 1 3 6 6 9 12 13 15 15 17 0.1 0.3 0.6 0.6 0.9 1.2 1.3 1.5 1.5 1.7 1 3 6 6 9 12 13 15 15 17
ってな具合に、それぞれ異なる解釈をしなければ比較できない配列が、 比較する関数を渡すことによって一つの関数でソートできるようになりました。
最後の int 型要素へのポインタの例で分かると思いますが、 void 型ポインタはポインタのポインタも受け付けます。 「ポインタのポインタ」という感じで特別なものと思っているとちょっと意外な気がしますが、「「ポインタ」のポインタ」 なので所詮ポインタです。当然ポインタのポインタのポインタの・・・(略
でも、私が初めて int と float のソートをやったときはそんなに便利に思えませんでした。
まだ、この時点では本当の意味を理解していなくて、
同じ数値なんだから float でやっちゃえば一緒だろうとか思ってたわけです。
しかし、次に示すような例をやったとき初めて、汎用性のある処理ができるんだと理解できました。
以下の例は名前、身長、体重を保持する構造体の配列を、身長と体重でソートする例です。
本当はデータをファイル読み込みとかしたかったんですがデータを作るのが面倒なので、ランダムに生成してます。
#include <stdio.h>
#include <stdlib.h>
struct status{
char *name;
float height;
float weight;
};
void print_status(struct status *);
int hcmp(const void *, const void *);
int wcmp(const void *, const void *);
main(){
int i;
struct status a[] = {
{"鈴木",0.0f,0.0f}, {"佐藤",0.0f,0.0f}, {"田村",0.0f,0.0f},
{"山田",0.0f,0.0f}, {"山本",0.0f,0.0f}, {"池田",0.0f,0.0f},
{"田中",0.0f,0.0f}, {"中村",0.0f,0.0f}, {"井上",0.0f,0.0f},
{"内海",0.0f,0.0f},
};
for(i = 0; i < 10; i++){
a[i].height = 170.0 + (rand()%300 - 150)/10.0f;
a[i].weight = 70.0 + (rand()%400 - 200)/10.0f;
print_status(a+i);
}
puts("");
/* 身長でソート */
qsort(a,10,sizeof(struct status),hcmp);
for(i = 0; i < 10; i++)
print_status(a+i);
puts("");
/* 体重でソート */
qsort(a,10,sizeof(struct status),wcmp);
for(i = 0; i < 10; i++)
print_status(a+i);
}
void print_status(struct status *data){
printf("名前: %s 身長: %.1f 体重: %.1f\n",data->name,data->height,data->weight);
}
/* 身長の比較 */
int hcmp(const void *arg0, const void *arg1){
struct status *a = (struct status *)arg0;
struct status *b = (struct status *)arg1;
if(a->height == b->height) return 0;
else if(a->height > b->height) return 1;
else return -1;
}
/* 体重の比較 */
int wcmp(const void *arg0, const void *arg1){
struct status *a = (struct status *)arg0;
struct status *b = (struct status *)arg1;
if(a->weight == b->weight) return 0;
else if(a->weight > b->weight) return 1;
else return -1;
}
結果
名前: 鈴木 身長: 183.3 体重: 58.6 名前: 佐藤 身長: 172.7 体重: 61.5 名前: 田村 身長: 184.3 体重: 83.5 名前: 山田 身長: 183.6 体重: 59.2 名前: 山本 身長: 179.9 体重: 72.1 名前: 池田 身長: 161.2 体重: 52.7 名前: 田中 身長: 184.0 体重: 55.9 名前: 中村 身長: 181.3 体重: 82.6 名前: 井上 身長: 179.0 体重: 72.6 名前: 内海 身長: 172.2 体重: 63.6 名前: 池田 身長: 161.2 体重: 52.7 名前: 内海 身長: 172.2 体重: 63.6 名前: 佐藤 身長: 172.7 体重: 61.5 名前: 井上 身長: 179.0 体重: 72.6 名前: 山本 身長: 179.9 体重: 72.1 名前: 中村 身長: 181.3 体重: 82.6 名前: 鈴木 身長: 183.3 体重: 58.6 名前: 山田 身長: 183.6 体重: 59.2 名前: 田中 身長: 184.0 体重: 55.9 名前: 田村 身長: 184.3 体重: 83.5 名前: 池田 身長: 161.2 体重: 52.7 名前: 田中 身長: 184.0 体重: 55.9 名前: 鈴木 身長: 183.3 体重: 58.6 名前: 山田 身長: 183.6 体重: 59.2 名前: 佐藤 身長: 172.7 体重: 61.5 名前: 内海 身長: 172.2 体重: 63.6 名前: 山本 身長: 179.9 体重: 72.1 名前: 井上 身長: 179.0 体重: 72.6 名前: 中村 身長: 181.3 体重: 82.6 名前: 田村 身長: 184.3 体重: 83.5
という具合に、比較用関数に渡されるポインタは全く一緒であるにもかかわらず、異なる意味の結果を出力できるのです。
当然ですが、比較関数さえしっかり書けば、名前の辞書順に並び替えることだってできます。
qsortは本当にどんな配列でもソートできるんですね~、いや~賢い!!これぞ関数ポインタの威力!
関数ポインタを使った関数の呼び出し
では次、関数ポインタを渡してもらって処理をするところの書き方の例を出してみようと思います。
非常に簡単な例で、意味のある関数ではありませんが、
渡された配列の全要素それぞれに対して(を使って)何かをするという関数を作ってみましょう。
インターフェイスは qsort とほぼ同じで、一個の要素をどうするか引数で入力してもらうというものです。
void print_array(void *base, size_t n, size_t size, void (*fnc)(void *)){
int i;
char *pt = (char*)base;
for(i = 0; i < n; i++){
fnc(pt);
pt += size;
}
}
ちょっと簡単すぎたかな・・・?
とりあえず、関数ポインタ fnc を渡されたときにどうやって関数を呼ぶかを示したかっただけなので。
たとえば、この fnc に一要素を表示させる機能を渡せば全要素を表示するという記述が可能になります。
(本当はもっと大きな機能を記述する場面でないとあまり意味がない・・・)
base を char 型にキャストしているのはポインタ演算ができるように、サイズが1の型にキャストする必要があるからです。
これで、関数ポインタの使い方が分かったでしょうか?
実際のAPIでの使用例
関数ポインタがよく使われる例として、 GUI 関連のライブラリがあります。 たとえば、この手のライブラリではマウスの操作の監視などはライブラリ側が行いますが、 あるボタンをユーザがクリックしたときどうするか? といったことを登録するために関数ポインタが使われます。
gulong g_signal_connect( gpointer *object,
const gchar *name,
GCallback func,
gpointer func_data );
これは、 GTK+ 2.0 のコールバック関数登録 API の例です。 全部の型が typedef されているので分かりづらいと思いますが、 GCallback っていうのが関数ポインタの typedef になっています。 これを使ってボタンがクリックされたとき呼び出される関数とかを登録します。 (参考:gtk+2.0 チュートリアル)
他の使い方として、構造体のフィールドとして関数ポインタを用意して、 隣接リストで接続してリストを回しながら処理をするっていう感じの記述をすると、 この構造体の入れ替えをすることで 処理の流れを動的に変化させるなんてことが容易に記述できるようになります。 (ゲームとかで使われるタスクシステム?)
最後に、ちょっと初心者がステップアップするところであこがれる(?) マルチスレッド関係の API を紹介、 (参考:pthreadリファレンス)
int pthread_create(pthread_t * thread,
pthread_attr_t * attr,
void * (*start_routine)(void *),
void * arg);
これは POSIX という UNIX 系 OS で一般に用いられるスレッド API の一つでスレッドの起動を行います。
ここで渡した、void *(*start_routine)(void *) って関数が別のスレッドとして起動されます。
その際に関数に渡す引数は void *arg で指定したものが渡されます。
ここでも void 型ポインタが使われており、どんな引数でも渡せるし、
どんな戻り値も返せる関数をスレッドにできるようになっています。
どうでしょう? API とかの記述がちょっとは理解できるような気がしてきましたでしょうか?
とりあえず今回はこんなところで
そのうちマルチスレッド関係についても扱えたらいいなと思いつつ、
この更新ペースでいけばいったいいつになるのやら・・・