今回は、関数ポインタを使った「コールバック関数」について説明します。
「コールバック関数」とは、引数として「他の関数のアドレス」を受け取る関数だと思ってください。
つまり、引数に関数ポインタを設定するという事です。
まず、元となるプログラムを作ります。
<sample program 175-01>
#include <stdio.h> void ShowABC(void); int main(void) { return 0; } void ShowABC(void) { printf("ABC\n"); } |
あまり意味のない関数ですが、"ABC"という文字列を表示する関数を作りました。
引数も戻り値もvoidですね。
このShowABC関数のアドレスを受け取る別の関数を追加します。
名前は、CallABC関数にでもしておきましょう。
CallABC関数は、引数としてShowABC関数のアドレスを受け取ります。
そして、アドレス経由でShowABC関数を呼び出すのです。
実際に作ってみましょう。
<sample program 175-02>
#include <stdio.h> void ShowABC(void); void CallABC(void(*pShowABC)(void)); int main(void) { CallABC(ShowABC); return 0; } void ShowABC(void) { printf("ABC\n"); } void CallABC(void(*pShowABC)(void)) { pShowABC(); } |
<実行結果>
ABC 続行するには何かキーを押してください・・・
CallABC関数の引数は、
void(*pShowABC)(void) |
となっています。
引数も戻り値もvoidの関数のアドレスを受け取る関数ポインタですね。
そして、CallABC関数内で、
pShowABC(); |
と書き、アドレス経由でShowABC関数を呼び出しました。
さて、これだけでは何のために必要な考え方か、まだ分かりません。
C言語の標準関数の中で、コールバック関数を必要とする関数がありますので、それを使ってみましょう。
その関数とは、「qsort」という関数です。
これは、「クイックソート」の略で、高速なソートが出来る関数です。
まずは、元となるプログラムから作成しましょう。
<sample program 175-03>
#include <stdio.h> #include <stdlib.h> #include <time.h> #define DATA 10 int main(void) { int data[DATA]; int i; srand((unsigned int)time(NULL)); for (i = 0; i < DATA; i++) { data[i] = rand(); } for (i = 0; i < DATA; i++) { printf("%d\n", data[i]); } return 0; } |
<実行結果>
14827 16834 12873 22148 7826 9623 15021 26089 10728 15688 続行するには何かキーを押してください・・・
配列dataに乱数を10個セットし表示するプログラムです。
この配列をqsort関数を使って、昇順(小さい順)にソートします。
qsort関数は stdlib.h をインクルードすると使えるようになります。
qsort関数の引数は以下の4つです。
※少し省略して書いています。
第1引数 void* _Base 第2引数 size_t _NumOfElements 第3引数 size_t _SizeOfElements 第4引数 int (*_PtFuncCompare)(void const*, void const*) |
第1引数は、配列の先頭アドレスです。
第2引数は、配列の要素数です。
第3引数は、要素1つのサイズ(バイト数)です。
※size_t型はtypedefされたunsigned int型です。
第4引数が今回のポイントとなる、関数ポインタです。
戻り値がint型で、引数が void const* 型2つと書いてあります。
void const* |
これは、
const void* |
と同じで、ポインタが指している中身を変更出来ないという意味です。
関数ポインタの名前は、
_PtFuncCompare |
「比較関数のポインタ」と書いてあります。
ソートのプログラムでは、順番に並び替えるために要素同士の大小比較を行います。
2つの要素の大小関係を調べる関数を作り、そのアドレスを渡さなければなりません。
比較する要素は、用意するデータによって異なる可能性があります。
今回のデータはint型配列ですが、char型の時もあるでしょうし、構造体配列かも知れません。
どんなものが引数になるか分からないので、「voidポインタ」で受け取っているのです。
では、比較するための関数のプロトタイプ宣言を書きます。
int Compare(void const* pLhs, void const* pRhs); |
関数名は、そのまま「Compare(比較)」という名前です。
引数の名前は、pLhs(左辺へのポインタ)とpRhs(右辺へのポインタ)としました。
関数の中身については、プログラムを作った後で説明します。
<sample program 175-04>
#include <stdio.h> #include <stdlib.h> #include <time.h> #define DATA 10 int Compare(void const* pLhs, void const* pRhs); int main(void) { int data[DATA]; int i; srand((unsigned int)time(NULL)); for (i = 0; i < DATA; i++) { data[i] = rand(); } qsort(data, DATA, sizeof(int), Compare); for (i = 0; i < DATA; i++) { printf("%d\n", data[i]); } return 0; } int Compare(void const* pLhs, void const* pRhs) { if (*(int*)pLhs < *(int*)pRhs) { return -1; } else { if (*(int*)pLhs > *(int*)pRhs) { return 1; } else { return 0; } } } |
<実行結果>
4599 9053 10040 10098 11670 16533 16847 22439 25681 30306 続行するには何かキーを押してください・・・
Compare関数の中身については、
左辺 < 右辺 であれば 負の値 左辺 > 右辺 であれば 正の値 左辺 = 右辺 であれば 0
を返すように作ります。
引数はvoidポインタですから、キャストしなければ使えません。
配列のデータはint型ですので、
(int*)pLhs |
と書いて、int型のポインタに変換し、
*(int*)pLhs |
で、中身にアクセスしています。
これで4つの引数をセットし、qsort関数を呼び出す事で昇順ソートが出来ました。
このクイックソートは「高速にソートが出来る」と書きました。
折角ですからどのくらい早いのか、体感してもらいましょう。
データを5万件に増やして、実行してみましょう。
※ただし、結果の表示には時間がかかるので、表示の箇所は無効にしておきます。
<sample program 175-05>
#include <stdio.h> #include <stdlib.h> #include <time.h> #define DATA 50000 int Compare(void const* pLhs, void const* pRhs); int main(void) { int data[DATA]; int i; srand((unsigned int)time(NULL)); for (i = 0; i < DATA; i++) { data[i] = rand(); } qsort(data, DATA, sizeof(int), Compare); #if 0 for (i = 0; i < DATA; i++) { printf("%d\n", data[i]); } #endif printf("Finish!\n"); return 0; } int Compare(void const* pLhs, void const* pRhs) { if (*(int*)pLhs < *(int*)pRhs) { return -1; } else { if (*(int*)pLhs > *(int*)pRhs) { return 1; } else { return 0; } } } |
<実行結果>
Finish! 続行するには何かキーを押してください・・・
結果が表示されませんが、プログラムはすぐに終了するはずです。
これだけでは、早いかどうか分かりませんよね・・・
では、ソート部分を「選択法」に変えてみましょう。
<sample program 175-06>
#include <stdio.h> #include <stdlib.h> #include <time.h> #define DATA 50000 int main(void) { int data[DATA]; int i; int j; int work; srand((unsigned int)time(NULL)); for (i = 0; i < DATA; i++) { data[i] = rand(); } for (i = 0; i < DATA - 1; i++) { for (j = i + 1; j < DATA; j++) { if (data[i] > data[j]) { work = data[i]; data[i] = data[j]; data[j] = work; } } } printf("Finish!\n"); return 0; } |
<実行結果>
Finish! 続行するには何かキーを押してください・・・
どうでしょうか?
プログラムが終了するまで、少し時間がかかったと思います。
※全く変わらなかったという場合は、データ数を増やして試してください。
私の環境では、クイックソートは一瞬、選択法は6秒程度かかりました。
正直、10個程度のデータでは、このような差は出ないと思います。
しかし、一度に多くのデータを扱うことも当然あります。
その時に、クイックソートの存在を知らなければ、遅いプログラムしか作れません。
アルゴリズムというものは大事なものですが、今回の主題ではありませんので、ここまでにしておきましょう。