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言語におけるコールバック関数の本質を理解することができるでしょう。