C言語で処理を差し替える仕組み:関数ポインタとコールバック

この記事はchatGPTの助けを借りて作成したものです。

1. はじめに:関数を「渡す」とはどういうことか?

C言語では、関数もデータと同じように「ポインタ」として扱うことができます。つまり、関数の処理そのものを、別の関数に引数として"渡す"ことができるのです。これを使うと、処理の一部を後から差し替えたり、呼び出し側に委ねたりと、柔軟なプログラム構造が可能になります。

そのような使い方の典型が「コールバック関数」です。


2. コールバック関数とは何か?

コールバック関数とは、関数に引数として渡された関数ポインタのことです。渡された関数は、あとで"呼び戻す(コールバックする)"形で実行されます。

たとえば、以下のようなコードが基本例です:

 1#include <stdio.h>
 2
 3typedef int (*CallbackFunc)(int);
 4
 5void process(int arr[], int size, CallbackFunc cb) {
 6    for (int i = 0; i < size; i++) {
 7        int result = cb(arr[i]);
 8        printf("Result: %d\n", result);
 9    }
10}
11
12int doubleValue(int x) {
13    return x * 2;
14}
15
16int main() {
17    int data[] = {1, 2, 3, 4, 5};
18    process(data, 5, doubleValue);
19    return 0;
20}

この例では、process()関数が「配列の各要素に何らかの処理をする」という部分を、doubleValue()という関数に委ねています。しかし、この時点では「関数を渡す利点」がピンと来ないかもしれません。


3. 利用価値が見える!qsortを使ったコールバック

標準ライブラリ関数qsort()は、コールバック関数の実用例として非常に有名です。

qsortの基本仕様

1void qsort(void *base, size_t nmemb, size_t size,
2           int (*compar)(const void *, const void *));
  • base: ソート対象の配列の先頭ポインタ
  • nmemb: 配列の要素数
  • size: 1要素のサイズ(バイト数)
  • compar: 比較関数(コールバック)

qsort()はソートの際に、要素同士を比較するためにcompar関数を呼び出します。比較関数には、配列中の要素のポインタ(const void *)が2つ渡されます。それらを適切な型にキャストして比較します。

比較関数の戻り値の意味:

  • 負の値:a < b(aはbより前に並べる)
  • 0:a == b(順序はそのまま)
  • 正の値:a > b(aはbより後に並べる)

コード例

 1#include <stdlib.h>
 2#include <stdio.h>
 3
 4int compare_asc(const void *a, const void *b) {
 5    int x = *(const int *)a;
 6    int y = *(const int *)b;
 7    return x - y;
 8}
 9
10int compare_desc(const void *a, const void *b) {
11    int x = *(const int *)a;
12    int y = *(const int *)b;
13    return y - x;
14}
15
16int main() {
17    int data[] = {5, 1, 4, 2, 3};
18    int size = sizeof(data) / sizeof(data[0]);
19
20    qsort(data, size, sizeof(int), compare_asc);
21    printf("昇順: ");
22    for (int i = 0; i < size; i++) printf("%d ", data[i]);
23    printf("\n");
24
25    qsort(data, size, sizeof(int), compare_desc);
26    printf("降順: ");
27    for (int i = 0; i < size; i++) printf("%d ", data[i]);
28    printf("\n");
29    return 0;
30}

配列の要素数はsizeof(data) / sizeof(data[0])で求められています。これは、配列全体のサイズを1要素のサイズで割ることで、要素の数を算出する標準的なテクニックです。


4. 複雑な処理条件を組み込める!構造体ソートの応用

コールバック関数を使えば、構造体などの複雑なデータ構造を、任意の優先順位に基づいてソートすることもできます。

 1#include <stdio.h>
 2#include <stdlib.h>
 3#include <string.h>
 4
 5typedef struct {
 6    int age;
 7    char name[32];
 8} Person;
 9
10int compare_person(const void *a, const void *b) {
11    const Person *p1 = (const Person *)a;
12    const Person *p2 = (const Person *)b;
13    if (p1->age != p2->age) {
14        return p1->age - p2->age;
15    } else {
16        return strcmp(p1->name, p2->name);
17    }
18}
19
20int main() {
21    Person people[] = {
22        {25, "Alice"},
23        {30, "Charlie"},
24        {25, "Bob"},
25        {30, "Bob"},
26        {20, "Eve"},
27    };
28    int n = sizeof(people) / sizeof(people[0]);
29    qsort(people, n, sizeof(Person), compare_person);
30    for (int i = 0; i < n; i++) {
31        printf("%d歳: %s\n", people[i].age, people[i].name);
32    }
33    return 0;
34}

このように、構造体のフィールドを使った複雑な多段階の比較も、コールバック関数を使えば簡潔に実装できます。


5. 関数名と &関数名が同じになる理由とは?

process(data, 5, doubleValue); のような記述で、doubleValueは関数のアドレスを渡しています。C言語では、関数名は式の中で自動的にその関数のポインタ(アドレス)に変換されるため、&doubleValueと書いても意味は同じです。

これはC言語の仕様であり、変数と違って関数名が「関数の先頭アドレスを指す記号」として扱われるためです。


6. 応用例:さまざまな場面で使われるコールバック

コールバック関数は、ソートだけでなく、さまざまなタイミングで「あとで呼ばれる関数」として利用されます。

タイマーによる非同期処理(一定時間後に呼び出す)

C言語では、UNIX系環境でalarm()signal()を使って「一定時間後に関数を呼ぶ」という非同期処理が可能です。

 1#include <stdio.h>
 2#include <signal.h>
 3#include <unistd.h>
 4
 5void timeout_handler(int signum) {
 6    printf("タイマーが発動しました!\n");
 7}
 8
 9int main() {
10    signal(SIGALRM, timeout_handler);
11    alarm(3);
12    for (int i = 0; i < 5; i++) {
13        printf("待機中... (%d)\n", i);
14        sleep(1);
15    }
16    return 0;
17}

ここでもtimeout_handler()という関数が、タイマーの発火時にコールバックされる仕組みになっています。

ログ出力処理をコールバックで差し替える

ログの出力先を、標準出力・ファイルなどに切り替えたいとき、コールバック関数を使うと柔軟に対応できます。

 1#include <stdio.h>
 2
 3// ログ関数の型(関数ポインタ)
 4typedef void (*Logger)(const char *);
 5
 6// デフォルトのログ出力(標準出力に表示)
 7void default_logger(const char *message) {
 8    printf("[LOG] %s", message);
 9}
10
11// ファイルにログを出力する例
12void file_logger(const char *message) {
13    FILE *fp = fopen("log.txt", "a");
14    if (fp) {
15        fprintf(fp, "[LOG] %s", message);
16        fclose(fp);
17    }
18}
19
20// 実際の処理関数(logger にログ関数を渡す)
21void do_task(Logger logger) {
22    logger("タスク開始");
23    for (int i = 0; i < 3; i++) {
24        char buf[64];
25        snprintf(buf, sizeof(buf), "中間処理: ステップ %d", i);
26        logger(buf);
27    }
28    logger("タスク終了");
29}
30
31int main() {
32    // 1. 標準出力にログ出力
33    printf("=== 標準ログ ===");
34    do_task(default_logger);
35
36    // 2. ファイルにログ出力
37    printf("=== ファイルログ ===");
38    do_task(file_logger);
39
40    return 0;
41}

コールバックは**「いつ何をするかを外から決められる」**という特徴を活かして、幅広い場面で使われています。


7. まとめ:コールバック関数を使いこなす設計的発想

コールバック関数は、処理の一部を呼び出し側に委ねることで、コードの汎用性・再利用性・拡張性を大きく向上させます。関数ポインタの正しい扱いやvoidポインタのキャストには注意が必要ですが、それを乗り越えれば柔軟な設計が可能です。

標準ライブラリ(qsort, signal)を通して、C言語におけるコールバック関数の本質を理解することができるでしょう。

関連ページ