ポインタ⑧(関数ポインタ) | Programming Place Plus C言語編 第38章

トップページC言語編

このページの概要 🔗

以下は目次です。


関数ポインタ 🔗

ポインタは、メモリアドレスを持つものであれば指し示せます。実は、関数の実体(中身のコード)もメモリ上にありますから、関数を指し示すことも可能です。これを、関数ポインタ (function pointer) と呼びます。また、これに対して、これまでのようなオブジェクトを指し示すポインタを、オブジェクトポインタ (object pointer) と呼ぶことがあります。

【上級】オブジェクトポインタも関数ポインタも同じようなもののようですが、その実際的な表現形式はまったく異なる可能性があります。これは、実行環境によっては、データが格納されるメモリと、関数のようなコードが格納されるメモリとが、大きく異なる領域に分離されている場合があるからです。現実的に起こり得る分かりやすい違いは、sizeof を使ったときに返される大きさです。このため、オブジェクトポインタと関数ポインタとの間で、代入を行うことは不適切です。

関数ポインタ型の変数を宣言するには、次のように記述します。

戻り値の型 (*変数名) (仮引数の並び);

関数ポインタは、仮引数の内容と、戻り値の型が一致する関数のメモリアドレスを保持できます。そこで、戻り値の型と、仮引数の並びを関数宣言のようなかたちで記述し、あいだに関数ポインタ変数の名前を置きます。

変数名の頭に * を付けることでポインタであることを示しますが、* と変数名全体を ( ) で囲まなければならないことに注意してください

関数ポインタには、仮引数の型や個数、戻り値の型が異なる関数のメモリアドレスは格納できません。ヌルポインタは格納でき、何も指し示していないことになります。


次のような関数が宣言されていたとします。

int func(const char* str);

この関数を指し示す関数ポインタ変数 func_ptr は、次のように宣言できます。

int (*func_ptr)(const char*) = func;

「= func」は初期値として、func関数のメモリアドレスを渡しているということです。アドレス演算子を使っていませんが、関数の場合、使っても使わなくても同じ意味です

もちろん、初期値は後から与えても構いません。いつものように、変数宣言時に初期値を与えなかった場合の値は、静的記憶域期間をもっているのでなければ不定値です。静的記憶域期間をもっているのなら、ヌルポインタになります。第22章

int (*func_ptr)(const char*);
func_ptr = func;

関数ポインタ変数を配列にする場合、要素数の指定は、変数名の直後のところに置きます。

int (*func_ptrs[10])(const char*);


関数ポインタ変数の宣言は、少々独特で読みづらいです。また、その型名の表現も難しく、「int (*)(const char*)」のような型名ということになります。

関数ポインタを関数の仮引数として使う場合など、型名を記述しなければならない場面があります。そのため、毎回このような複雑な型名を書かなくてすむように、typedef を使って別名を定義しておく方法があります。

typedef 戻り値の型 (*新しい型名) (仮引数のリスト);

これはこれでまた難解ですが、こういうものなので覚えてしまうしかないです。具体的には次のように使います。

typedef int (*func_ptr_t)(const char*);  // 型名を定義
func_ptr_t func_ptr;                     // 定義した型名を使って、関数ポインタ変数を宣言
func_ptr = func;                         // 関数のメモリアドレスを代入

関数ポインタは、関数を指し示していますから、その使い道は当然、関数を呼び出すことです。関数ポインタを経由して関数を呼び出すには、次のようにします。

int ret = func_ptr("abcde");
int ret = (*func_ptr)("abcde");

この2つはどちらも同じ意味です。

1つ目の方法は、普通に関数を呼び出すときとまったく同じ見た目です。2つ目の方法は、間接演算子を使って間接参照した上で、関数呼び出しを行っています。この場面では、わざわざ面倒な方法を採る必要はないと思います。1つ目の方法を選べばよいでしょう。

では、実際に使うことを考えていきます。たとえば、何らかの条件で処理内容を切り替えるようなプログラムを考えてみます。まず、関数ポインタを使わずに書いてみます。

#include <stdio.h>

void main_proc_for_easy(void);
void main_proc_for_normal(void);
void main_proc_for_hard(void);

int main(void)
{
    puts("難易度を選んでください。");
    puts("0~2 で大きいほど難しくなります。");

    char str[40];
    int level;
    fgets(str, sizeof(str), stdin);
    sscanf(str, "%d", &level);

    // 難易度に応じたメインの処理を実行する
    switch (level) {
    case 0:
        main_proc_for_easy();
        break;
    case 1:
        main_proc_for_normal();
        break;
    case 2:
        main_proc_for_hard();
        break;
    default:
        puts("入力が正しくありません。");
        break;
    }
}

void main_proc_for_easy(void)
{
    puts("簡単なモードで実行。");
}

void main_proc_for_normal(void)
{
    puts("標準的な難易度のモードで実行。");
}

void main_proc_for_hard(void)
{
    puts("難しいモードで実行。");
}

実行結果:

難易度を選んでください。
0~2 で大きいほど難しくなります。
1
標準的な難易度のモードで実行。

何のプログラムか不明ですが、ともかく、標準入力から入力された難易度に応じて、処理を分けるものだと考えてください。多分、main_proc_XXXX という名前の関数の中では、問題が表示されてそれに答えるような処理があり、難易度によって、出題される問題の難しさが変わるとしましょう。

この実装方法の問題の1つは、switch文(または if文の連続)で分岐を行う必要があるため、後から、難易度の種類が増えると、忘れずに case句なり else句なりを増やさねばならないことです。今回のように、分岐処理を書く場所が1カ所だけならまだいいのですが、ソースファイルが複数あり、いろいろなところで同様の意味合いの分岐処理が書いてあると大変なのです。

次に同じプログラムを、関数ポインタを駆使して書いてみます。

#include <stdio.h>

#define SIZE_OF_ARRAY(array)    (sizeof(array)/sizeof(array[0]))

void main_proc_for_easy(void);
void main_proc_for_normal(void);
void main_proc_for_hard(void);

int main(void)
{
    typedef void (*main_proc_t)(void);  // メイン処理の関数ポインタ型

    static const main_proc_t main_proc_array[] = {  // メイン処理の関数テーブル
        main_proc_for_easy,
        main_proc_for_normal,
        main_proc_for_hard,
    };


    puts("難易度を選んでください。");
    puts("0~2 で大きいほど難しくなります。");

    char str[40];
    int level;
    fgets(str, sizeof(str), stdin);
    sscanf(str, "%d", &level);

    if (level < 0 || SIZE_OF_ARRAY(main_proc_array) <= level) {
        puts("入力が正しくありません。");
        return 0;
    }

    // 難易度に応じたメインの処理を実行する
    main_proc_array[level]();
}

void main_proc_for_easy(void)
{
    puts("簡単なモードで実行。");
}

void main_proc_for_normal(void)
{
    puts("標準的な難易度のモードで実行。");
}

void main_proc_for_hard(void)
{
    puts("難しいモードで実行。");
}

実行結果:

難易度を選んでください。
0~2 で大きいほど難しくなります。
1
標準的な難易度のモードで実行。

こちらは、呼び出すべき関数を配列で管理してあり、難易度と添字を一致させてあります。そのため、分岐構造は必要なくなります(エラーチェックのためには必要ですが)。

もし、後から難易度が増えたら、配列の中身を変えて、新しい関数を作るだけで済みます。つまり、変更すべき箇所が激減し、しかも見つけやすい場所にあります。


qsort関数 🔗

標準ライブラリ関数の中には、関数ポインタを利用した関数あります。たとえば、qsort関数と、bsearch関数が代表的です。まずは qsort関数を取り上げ、この後の項で bsearch関数を取り上げます。

qsort関数は、ソート(整列) (sort、sorting) という操作を行う関数です。ソートとは、複数のデータを、大きい値から小さい値の順序(降順 (descending order))になるように並び変える、あるいは反対に、小さい値から大きい値の順序(昇順 (ascending order))になるように並び変える操作のことです。

ソートには、さまざまな実装手段があります。具体的な実装方法については、アルゴリズムとデータ構造編【整列】を参照してください。ともかく、qsort関数を使うと、具体的な実装方法を知らなくても、ソートを行えます。

【上級】一般的に、クイックソート(アルゴリズムとデータ構造編【整列】第6章)という方法が使われることから qsort という名前になっていますが、他の方法が使われている可能性もあります。

qsort関数は、<stdlib.h> で、次のように宣言されています。

void qsort(void* base, size_t count, size_t size, int (*compar)(const void* elem1, const void* elem2));

第1引数base は、ソート対象の配列を指すポインタです。第2引数count には、その配列に含まれる要素数を、第3引数size には、要素1つ分の大きさを指定します。第4引数compar は、ソートの条件を指定するための関数ポインタを指定します。

まずは実際に使ってみましょう。

#include <stdio.h>
#include <stdlib.h>

#define SIZE_OF_ARRAY(array)    (sizeof(array)/sizeof(array[0]))

void print_table(const int* array, size_t size);
int compare_int(const void* a, const void* b);

int main(void)
{
    int table[] = {66, 85, 70, 92, 61, 89};

    puts("ソート前");
    print_table(table, SIZE_OF_ARRAY(table));

    qsort(table, SIZE_OF_ARRAY(table), sizeof(int), compare_int);

    puts("ソート後");
    print_table(table, SIZE_OF_ARRAY(table));
}

/*
    int型配列の要素を出力。
*/
void print_table(const int* array, size_t size)
{
    for (size_t i = 0; i < size; ++i) {
        printf("%d ", array[i]);
    }
    printf("\n");
}

/*
    int型による順序比較。

    引数:
        a:  比較する要素。
        b:  比較する要素。
    戻り値:
        a の方が小さいとき負数、
        b の方が小さいとき 0 より大きい値、
        a と b が同じときは 0、
         が返される。
*/
int compare_int(const void* a, const void* b)
{
    int a_num = *(const int*)a;
    int b_num = *(const int*)b;

    if (a_num < b_num) {
        return -1;
    }
    else if (a_num > b_num) {
        return 1;
    }
    return 0;
}

実行結果:

ソート前
66 85 70 92 61 89
ソート後
61 66 70 85 89 92

qsort関数を使う際の肝となるのは、第4引数に渡す関数ポインタです。この関数ポインタが指し示す関数は、2つの要素の値の大小関係を比較して返すように実装しておきます。qsort関数は、このような比較用の関数を何度も繰り返し呼び出して、要素同士がどう並ぶべきなのかを判断して、ソートを行います。

なお、このように、関数ポインタを渡しておいて、必要なタイミングで呼び出させるような手法を、コールバック (callback) と呼びます。

比較用の関数は決まったルールで実装しなければなりません。まず、仮引数は const付きの voidポインタが2つで、戻り値は int型です。関数ポインタ型は、仮引数の並びと戻り値の型がきちんと一致していなければならないので、この形に従う必要があります。

2つの引数には、比較する2つの要素を指すポインタが渡されてきます。「どの要素?」と思うかもしれませんが、それは予測できません。この辺りは、ソートのアルゴリズムを学んだ経験がないとイメージしづらいですが、ソートの処理の最中に、何度も何度も、ある要素とほかの要素との比較を行います。ともかく、2つの要素の値の大小関係を知りたいだけなので、どの要素なのかは気にせずに実装します。

比較用の関数の戻り値は、第1引数のポインタが指す要素の値の方が大きいなら 0 より大きい値を、同じなら 0 を、小さいなら 0 より小さい値を返すようにします。このルールは、strcmp関数のものと同じです。

サンプルプログラムでは、compare_int関数が比較用の関数です。引数で渡されてきたポインタから、要素の値を知らなければなりませんから、間接参照をしたいのですが、voidポインタで渡されてくるため、まずキャストが必要です。こうして、要素の本来の型を指し示せるポインタ型を得てから、間接参照を行います。

要素の比較を、qsort関数が内部で自身で行わずに、コールバックを利用する理由は、大きく2つあります。

1つには、対象の要素の型が分からないからです。要素は文字列かもしれないし、浮動小数点数かもしれないし、構造体かもしれません。どのような型であっても扱えるように、比較用の関数の仮引数は void*型になっています。

もう1つの理由は、順序関係の定義がいつも同じとは限らないということです。単純な話、昇順に並べたいのか、降順に並べたいのか、というだけでも比較用の関数の実装を変えなければなりません。

先ほどのサンプルプログラムだと昇順に並びますが、これを降順にしたければ比較関数を、以下のようにします。

/*
    int型による順序比較。

    引数:
        a:  比較する要素。
        b:  比較する要素。
    戻り値:
        b の方が小さいとき負数、
        a の方が小さいとき 0 より大きい値、
        a と b が同じときは 0、
         が返される。
*/
int compare_int(const void* a, const void* b)
{
    int a_num = *(const int*)a;
    int b_num = *(const int*)b;

    if (a_num < b_num) {
        return 1;
    }
    else if (a_num > b_num) {
        return -1;
    }
    return 0;
}

bsearch関数 🔗

bsearch関数は、サーチ(探索) (search) という操作を行う関数です。サーチとは、複数のデータの中から、特定の値を見つけ出す操作のことです。

サーチにも、さまざまな実装手段があります。具体的な実装方法については、アルゴリズムとデータ構造編【探索】を参照してください。bsearch関数を使うと、具体的な実装方法を知らなくても、サーチを行えます。

ただし、bsearch関数は、その実装手段の制約のため、対象となるデータ列が事前に昇順にソートされた状態でないと正しく動作しません。もし対象のデータがソート済みであることが保証できるなら問題ありませんが、そうでないのなら、先に qsort関数を使って、ソートを行っておくと良いでしょう。

bsearch関数は、<stdlib.h> に、次のように宣言されています。

void* bsearch(const void* key, void* base, size_t count, size_t size, int (*compar)(const void* key, const void* value));

第1引数 key は、探し出したい値を指すポインタです。素直に値を渡せないのは嫌らしいところですが、これも、対象の型を固定できないので void*型で扱わざるを得ないことが理由です。

第2引数以降は、qsort関数と同様のものが並んでいます。順番に、対象の配列のメモリアドレス、配列の要素数、要素1個分の大きさ、比較用関数を指し示す関数ポインタです。

戻り値は、目的の値が発見できた場合は、その要素を指し示すポインタが返されます。発見できなかった場合は、ヌルポインタが返されます。

比較用関数は、qsort関数のときと同様に、2つの要素を指すポインタを受け取る const void* の仮引数と、int型の戻り値を持ちます。しかし、仮引数の意味は異なります。

サーチの処理の最中には何度も、「この要素は目的の要素か?」を調べる場面があり、その都度、比較用関数がコールバックされます。第1引数は、探し出したいと思っている値を指すポインタ、すなわち、bsearch関数を呼び出すときに第1引数 key に与えたポインタが渡されてきます。

第2引数には、比較を行う対象の要素を指すポインタが渡されてきます。

比較用関数の戻り値は、第1引数のポインタが指す要素の値の方が大きいなら 0 より大きい値を、同じなら 0 を、小さいなら 0 より小さい値を返すようにします。

このように、比較用関数の仕様は qsort関数と bsearch関数とで異なっているので、別個に関数を用意した方が安全なのですが、実際には同じ関数でもうまく動作することもあります。たとえば、次のプログラムでは、同じ関数でまかなっています。

#include <stdio.h>
#include <stdlib.h>

#define SIZE_OF_ARRAY(array)    (sizeof(array)/sizeof(array[0]))

int compare_int(const void* a, const void* b);

int main(void)
{
    int table[] = {66, 85, 70, 92, 61, 89};


    // bsearch関数を使う前には、対象データがソート済みでなければならないので、
    // 事前に qsort関数でソートしておく
    qsort(table, SIZE_OF_ARRAY(table), sizeof(int), compare_int);


    puts("サーチする値を入力してください。");
    char str[40];
    int target;
    fgets(str, sizeof(str), stdin);
    sscanf(str, "%d", &target);

    // サーチ
    int* search_result = bsearch(&target, table, SIZE_OF_ARRAY(table), sizeof(int), compare_int);
    if (search_result == NULL) {
        printf("%d は存在しません。\n", target);
    }
    else{
        printf("%d を発見しました。\n", target);
    }
}

/*
    int型による順序比較。

    引数:
        a:  比較する要素。
        b:  比較する要素。
    戻り値:
        a の方が小さいとき負数、
        b の方が小さいとき 0 より大きい値、
        a と b が同じときは 0、
         が返される。
*/
int compare_int(const void* a, const void* b)
{
    int a_num = *(const int*)a;
    int b_num = *(const int*)b;

    if (a_num < b_num) {
        return -1;
    }
    else if (a_num > b_num) {
        return 1;
    }
    return 0;
}

実行結果:

サーチする値を入力してください。
70
70 を発見しました。

compare_int関数を qsort関数のために使うときには、仮引数 a と b は、比較する2つの要素を指し示しています。一方、bsearch関数のために使うときには、a は目的の値を指し示し、b は比較対象の要素を指し示しています。どちらのつもりで実装しても、結局コードはまったく同じになるので、このケースでは同じ関数を使いまわせます。

しかし、たとえば要素が文字列の場合を考えてみると、そうはいかないことが分かります。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define SIZE_OF_ARRAY(array)    (sizeof(array)/sizeof(array[0]))

int compare_string_for_qsort(const void* a, const void* b);
int compare_string_for_bsearch(const void* a, const void* b);

int main(void)
{
    char* table[] = {
        "Japan",
        "America",
        "Italy",
        "Thailand",
        "Mexico",
        "Russia"
    };


    // bsearch関数を使う前には、対象データがソート済みでなければならないので、
    // 事前に qsort関数でソートしておく
    qsort(table, SIZE_OF_ARRAY(table), sizeof(char*), compare_string_for_qsort);

#if 0
    // 正しくソートできていることを確認
    for (size_t i = 0; i < SIZE_OF_ARRAY(table); ++i) {
        puts(table[i]);
    }
#endif


    puts("サーチする文字列を入力してください。");
    char str[40];
    char target[40];
    fgets(str, sizeof(str), stdin);
    sscanf(str, "%s", target);

    // サーチ
    char** search_result = bsearch(target, table, SIZE_OF_ARRAY(table), sizeof(char*), compare_string_for_bsearch);
    if (search_result == NULL) {
        printf("%s は存在しません。\n", target);
    }
    else {
        printf("%s を発見しました。\n", target);
    }
}

/*
    char*型による順序比較 (qsort用)

    引数:
        a:  比較する要素。
        b:  比較する要素。
    戻り値:
        a の方が小さいとき負数、
        b の方が小さいとき 0 より大きい値、
        a と b が同じときは 0、
         が返される。
*/
int compare_string_for_qsort(const void* a, const void* b)
{
    const char* s1 = *(const char**)a;
    const char* s2 = *(const char**)b;

    return strcmp(s1, s2);
}

/*
    char*型による順序比較 (bsearch用)

    引数:
        a:  探索キー。
        b:  比較する要素。
    戻り値:
        a の方が小さいとき負数、
        b の方が小さいとき 0 より大きい値、
        a と b が同じときは 0、
         が返される。
*/
int compare_string_for_bsearch(const void* a, const void* b)
{
    const char* s1 = (const char*)a;
    const char* s2 = *(const char**)b;

    return strcmp(s1, s2);
}

実行結果:

サーチする文字列を入力してください。
Italy
Italy を発見しました。

ソートおよびサーチを行う配列 table の要素は、char*型です。

qsort関数を呼び出す際、第1引数に table を渡しています。いつものように、これは式の中で配列が現れており、ポインタ型に暗黙的に変換されます。元の型は char*[6] ですから、char** に変換されるのですが、qsort関数の第1引数は void* です。voidポインタである void* は、レベルの違いすら気にせずに受け取ってしまうのでした(第36章)。

なお、qsort関数側が void* であるため、配列 table の要素型を const char* にしていると、const を取り除こうとして、コンパイラが警告を出すかもしれません。この辺りの都合上、C言語の const の利用は中途半端にならざるを得ません。

qsort関数のための比較用関数は、compare_string_for_qsort関数です。比較用関数の仮引数は const void* です。qsort関数の場合、これらの引数の意味は、比較する要素を指すポインタです。

今回、要素の型は char* なので、それを指すポインタの型は char** です。つまり、本来なら char** で表現されるべきポインタが、比較用関数の仕様の都合上、const char* で渡されてきているという状況です。ここでもやはり、voidポインタであるがゆえに、レベルの情報が失われているのです。

そこで、compare_string_for_qsort関数の中では、まず、本来のポインタ型に戻すキャストを行います。つまり、const char* を const char** に変換すればよいです(配列 table の要素には const は付いていませんが、付いた状態で渡されてくるのなら、それに従っておけば良いでしょう)。

その後、間接参照によって、要素の値そのものを得ます。比較は、文字列同士なので strcmp関数を使えば簡単です。

次に、bsearch関数の呼び出しを見てみましょう。今度も qsort関数のときと同じで、本来、char** で渡されるべき第2引数ですが、bsearch関数の第2引数は void* なので、レベルが失われて void* に変換されています。

また、第1引数に、サーチしたい目的の値を指すポインタを渡しています。これは本来、char[40] ですが、暗黙的に char* に変換されます。bsearch関数の第1引数は const void* なので、こちらはレベルはそのまま保たれて const void* として渡されます。

bsearch関数のための比較用関数は、compare_string_for_bsearch関数です。bsearch関数の場合、1つ目の仮引数は、目的の値を指すポインタです。これは、もともと char* だったものが const void* として渡されてきています。

一方、2つ目の仮引数は、比較対象の要素を指すポインタです。要素の型は char* なので、それを指すポインタは char** です。

この2つの引数の違いが問題です。つまり、1つ目の引数は const void* を char* に戻してやるのが正しく、2つ目の引数は const void* を char** に戻してやるのが正しいのです。このような違いは、qsort関数の比較用関数にはありませんから、比較用関数は別個に実装したものが必要です。


練習問題 🔗

問題① 関数ポインタのところで登場した、難易度に応じて処理を分岐させるサンプルプログラムに、結果を出力するような関数を追加してください。この関数は、引数で得点を受け取り、難易度ごとに異なる方法で合格・不合格を決定するものとします。たとえば、簡単なモードなら 60点で合格ですが、難しいと 80点必要という感じです。得点は、main_proc_XXXX関数が戻り値で返すようにするなど、元のプログラムを一部修正して構いません。
なお、関数ポインタを使う方法と、使わない方法の両方で、プログラムを作成してください。

問題② qsort関数や bsearch関数に渡す関数ポインタにおいて、指し示す先の関数の引数が void*型ではなく、const void*型である理由はなぜでしょうか。


解答ページはこちら

参考リンク 🔗


更新履歴 🔗

≪さらに古い更新履歴を展開する≫



前の章へ (第37章 ポインタ⑦(構造体とポインタ))

次の章へ (第39章 ファイルの利用)

C言語編のトップページへ

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る