★ポインタ(関数ポインタ2)★


今回は、関数ポインタを使った「コールバック関数」について説明します。

「コールバック関数」とは、引数として「他の関数のアドレス」を受け取る関数だと思ってください。

つまり、引数に関数ポインタを設定するという事です。

まず、元となるプログラムを作ります。

<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個程度のデータでは、このような差は出ないと思います。

しかし、一度に多くのデータを扱うことも当然あります。

その時に、クイックソートの存在を知らなければ、遅いプログラムしか作れません。

アルゴリズムというものは大事なものですが、今回の主題ではありませんので、ここまでにしておきましょう。


次へ

戻る

目次へ