第24問の答え

答え

関数を指すポインタを引数にしたいので、こう書くのが正解!
void GreetTo(const char *pName, void (*pGreeter)(const char *)) {
  pGreeter(pName);
}

解説

これは、関数ポインタについての問題でした。

関数ポインタって、C言語のとくに難しいところですよね……
まぁ、「関数ポインタ」って聞いただけで拒否反応を示す人もいるわね。でも大丈夫よ!
ほんとですか?
関数だって、メモリーに格納されている点は変数と一緒じゃない?
はい、一緒ですね。
だとしたら、当然アドレスもあるわよね。
ありますね。
アドレスがあるんだったら、ポインタに格納できるでしょ?
できますね。あ、それが関数ポインタってことですか?
そういうこと!

関数ポインタも、「アドレスを格納した変数である」という点では変数のポインタと変わりません。ただ、そのアドレスに何が格納されているかが違うだけなのです。

今回のプログラムの穴埋め部分は、次のようになっていました。

void GreetTo(const char *pName, /* ここに入るコードは? */) {
  pGreeter(pName);
}

この関数の第2引数にどうやって関数ポインタを書けばいいのか考えてみましょう。

関数を呼び出している部分では、次のようにMorningGreeter()EveningGreeter()を渡しています。

  GreetTo("Leo", MorningGreeter);
  GreetTo("Yuki", EveningGreeter);

これら2つの関数は、次のような形をしていました。

void MorningGreeter(const char *pName) {
  ……
}

void EveningGreeter(const char *pName) {
  ……
}

2つの関数は名前が違いますが、引数や戻り値の型は同じですね。そのため、どちらも同じ型の関数ポインタに代入できます。

また、関数ポインタに代入した関数は、通常の関数のように括弧を付けて呼び出すことも可能です。この括弧は「関数呼び出し演算子」と呼ばれます。関数呼び出しは、実は演算の一種なんですね。

  void (*pGreeter1)(const char *) = MorningGreeter;
  pGreeter1("Leo");

  void (*pGreeter2)(const char *) = EveningGreeter;
  pGreeter2("Yuki");
あぁ、これですよ。関数ポインタって、この見た目が怖くないですか?
そうねぇ、たしかに文法がややこしいところはあるのよね。でも、それだけのことなのよ。

第23問で、typedefを使ってポインタの配列を分かりやすくしたのを覚えているのでしょうか。関数ポインタにも、同じテクニックが使えます。同じ型の関数ポインタを繰り返し使うときは、このように書けば分かりやすいでしょう。

  typedef void (*Greeter)(const char *); /* この関数ポインタの型を「Greeter」と呼ぶことにする */

  Greeter pGreeter1 = MorningGreeter;
  pGreeter1("Leo");

  Greeter pGreeter2 = EveningGreeter;
  pGreeter2("Yuki");
あ、pGreeter1pGreeter2が関数ポインタの変数名だったんですね!
ふふふ。分かったみたいね。
「見えた!」って感じですかね。だいぶ怖くなくなったかも。
関数ポインタはね、見慣れない文法のせいで敬遠されがちだけど、もともと怖いものではないのよ。
そっかぁ。僕も今まで見た目で誤解してたかもなぁ……

さて、問題の穴埋め部分の答えは、もう書けるのではないでしょうか。正解は次のようになります。

void GreetTo(const char *pName, void (*pGreeter)(const char *)) {
  pGreeter(pName);
}

または、typedefを使って次のようにしてもいいですね。

typedef void (*Greeter)(const char *);

void GreetTo(const char *pName, Greeter pGreeter) {
  pGreeter(pName);
}
ここがポイント!
関数ポインタは、実は見た目ほど怖くない!
あのー、関数ポインタを怖がる必要はないってことは分かったんですけど……
うんうん。
そもそも関数ポインタの使い道って、そんなにあります?
あるのよ。コールバックが欲しいときなんかに、よく使うわね。
やっぱり、よく使うんですね。で、コールバックってなんですか?
先に関数を渡しておいて、あとで呼び出してもらう手法のことよ。
あとで……?呼び出してもらう……?
そうねぇ。C言語では、標準関数のatexit()qsort()が有名かしら。

関数ポインタを使ったコールバックについて、ここではatexit()の使用例を紹介しておきましょう。次のように事前に関数を渡しておくと、それをプログラム終了時(at exit)に呼び出してくれます。後片付けが必要な場合などに便利ですね。

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

void ShowFinalMessage(void) {
  puts("Bye!");
}

int main(void) {
  atexit(ShowFinalMessage); /* あとで呼び出して欲しい関数を渡しておく */

  puts("Hello!");

  return EXIT_SUCCESS;
}
実行結果
Hello!
Bye!
あとで呼び出してもらうって、こういうことだったんですね!
だいぶイメージがわいたみたいね。
はい。もう関数ポインタは怖くないので、チャンスがあったら使ってみたいです!

修正後のプログラム

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

void GreetTo(const char *pName, void (*pGreeter)(const char *)) {
  pGreeter(pName);
}

void MorningGreeter(const char *pName) {
  printf("Good morning, %s!\n", pName);
}

void EveningGreeter(const char *pName) {
  printf("Good evening, %s!\n", pName);
}

int main(void) {
  GreetTo("Leo", MorningGreeter);
  GreetTo("Yuki", EveningGreeter);

  return EXIT_SUCCESS;
}
実行結果
Good morning, Leo!
Good evening, Yuki!