ショートカット
ファシリテーター × あり方
コーディングの向こう側
Hello, ANOTHER world!
オブジェクト指向のはなし
プログラミングのはなし
C言語実力診断クイズ
eSkillBooks
Hello, ANOTHER world!

メモリの中を想像する - その3

関数の動作にバリエーションを持たせたい場合は、その動作を指定するパラメータをつけることで実現できます。例えば次のようにします(プログラム中の ASSERT については別のページの解説を参照してください)。
typedef enum {
     AT1,
     AT2
} ActionType;

void SwitcherFunc( ActionType eAT )  /* パラメータ eAT で動作を振り分けている */
{
      switch ( eAT ) {
      case AT1:
          ...;
          break;
      case AT2:
          ...;
          break;
     default:
          ASSERT( false );
     }
}
さて、これだとあらかじめ決められた種類の動作しか行なえません。あとで動作の種類を増やそうと思ったら、関数自体を書き換える必要があります。ということは、動作の種類を増やすたびに以前の動作が変わってしまわないように気をつけなければならないし、我々が人間である以上、間違えて以前の動作を壊してしまうかもしれません。
そこで、関数ポインタを使います。関数ポインタを使えばひとつのポインタ変数で関数のエントリポイントを受け渡すことができるので、関数自体(すなわち何らかの動作)を引数にするようなことができます。次のプログラムは、関数ポインタによって動作を引数にしている例です。
#include <stdio.h>
#include <stdlib.h>

#include <assert.h>
#define ASSERT(C) assert(C)

static void Hello(void);
static void CallerFunc( void (*pHello)(void) );

int main(void)
{
     CallerFunc( Hello );  /* ここで「動作」を引数にしている */
     return EXIT_SUCCESS;
}

static void Hello(void)
{
     puts( "Hello, CALLBACK world!" );
}

static void CallerFunc( void (*pHello)(void) )
{
     ASSERT( pHello );
     pHello();  /* 引数として渡された「動作」を実行している */
}
このとき CallerFunc の引数 pHello と呼び出される関数 Hello() はメモリの中で次のようになっています。前回のトピックで出てきた図とそっくりですね。
このプログラムでは、 CallerFunc に渡すパラメータを別の関数(ただし同じ型の関数でなければなりません)に差し替えるだけでいろいろな動作をさせることができます。このような引数として渡す関数をコールバックルーチン(call back routine)または単にコールバックと呼びます。コールバックルーチンには次のような利点があります。
  • 動作を引数にできる
  • 新しい動作を加えるときに、呼び出す側の関数をいじる必要がない
    ・・・バグを作らない唯一の方法は「何も作らないこと」ですから、いじる必要がないということはすばらしいのです
  • 少しずつ動作が違う機能がたくさんあるとき、コールバックを使えば作る量が減る
    ・・・作る量が減るということは、その分バグの数も減るということです
「動作を引数にできる 」ということをより理解するために、もうひとつ例題をやってみましょう。ANSI-C に atexit() という関数がありあます。次のようなプロトタイプです。
int atexit( void (*pCallback)(void) );
これは、プログラムが終了するとき(つまり英語で " at exit")に、あらかじめ登録しておいたコールバック関数を実行するためのものです(詳しくはリファレンスマニュアルを参照してください)。これを使って無理矢理 "Hello, world" を書いてみましょう。
#include <stdio.h>
#include <stdlib.h>

static void Hello(void);

int main(void)
{
     atexit( Hello );
     return EXIT_SUCCESS;
}

static void Hello(void)
{
     puts( "Hello, ANSI CALLBACK world!" );
}
メモリの中で何が起こっているか想像するのを忘れないでください! atexit() は引数として受け取った関数のアドレスを登録しておくための関数ポインタを内部に持っていて、main 関数が終了するときにそこに登録された関数を呼び出すのです。
atexit() 以外にもコールバックを使った ANSI-C の標準関数があります。次の関数について調べてみてください。
  • qsort()
  • bsearch()
関数ポインタとコールバックを理解してしまえばメモリの完全制覇は目前です。次回はメモリに情報を格納するということはそもそもどういう意味なのかについて考えます。

("Hello, ANOTHER world!" は2001年11月から2002年11月にかけて作成されたコンテンツです。)