WisdomSoft - for your serial experiences.

9.3 関数のポインタ

関数にもアドレスが存在します。関数アドレスをポインタに保存することで、呼び出すべき関数を変数としてやり取りできます。これによって、呼び出す関数を実行時に変化させることができます。

9.3.1 関数アドレスの保存

変数にアドレスが存在するに用に、実は関数にもアドレスというものが存在します。関数も最終的には機械語となり、すなわち 2 進数列で表現されたデータが保存されているということです。関数を実行するためには、関数本体の処理情報を含むデータをメモリに読み込まなければなりません。CPU は機械語を処理する時に、メモリに読み込まれている機械語をCPUに取り込む必要があります。

物理的なコンピュータの仕様に依存しますが、通常、CPU は実行するべき機械語のメモリアドレスを表すレジスタを保有し、単一の機械語を実行するたびにレジスタの値をインクリメントするような仕組みになっています。つまり、機械語レベルでは命令ごとにポインタが存在するようなものです。

C 言語では文にアドレスは存在しません。文の位置を表すにはラベルを使いました。文単位の移動を行うにはこれで十分だからです。しかし、関数の場合は関数名による呼び出しでは不十分な場合があります。関数名を用いて関数を呼び出す方法は、呼び出すべき関数がコンパイル時に決定されなければならないことを表しています。同時に、呼び出すべき関数を実行時に動的に変更することができないことを意味します。これでは、プログラムの可能性を大幅に制限してしまうことになります。

例えば、実行時に呼び出すべき関数を動的に変更させることができれば、機能の置き換えや部分的な更新が可能な柔軟性のあるシステムを構築することができます。これを実現するための機能が、関数のアドレスなのです。

呼び出すべき関数のメモリ上の位置をエントリーポイントと呼びます。システムが呼び出すべき プログラムの最初の実行地点のことをアプリケーション・エントリーポイントと呼ぶことは「2.2 はじめてのC言語」で解説したとおりです。全ての関数にはアドレスが存在するのです。

そして、関数にアドレスが存在するという事実は、同時に関数をポインタとして扱うことができることを表します。ポインタはすなわちアドレスを保存する手段であり、この事実は関数を変数のように扱えるという大きな可能性を表しています。関数のアドレスを保存する関数ポインタは次のように宣言します。

関数ポインタの宣言
戻り値型 (*識別子)(パラメータ型リスト)

これは、ほとんどが関数の宣言に似ていますが、識別子の周りを括弧で囲む点で異なっています。例えば文字配列への引数を受け取り数値を返す関数のポインタは int (*pf)(const char*) となるでしょう。int *pf(const char*) と記述した場合は int へのポインタを返す関数であると解釈されるため、これらの意味はまったく異なるものであることを理解してください。

変数のアドレスを得る場合は & 演算子を用いましたが、関数のアドレスは関数名を指定します。関数の名前だけを指定した場合、それは関数のアドレスを表すのです。次のような関数が宣言されていると考えてください。

int Function(int , void *);

この場合、次のようにポインタにアドレスを代入することができます。

int (*pf)(int , void*) = Function;

これで、関数へのポインタ pf に Function() 関数のアドレスを代入したことになります。ポインタから関数を呼び出す場合は、通常の関数のようにポインタ名と引数リストを指定するか、あるいは * 演算子を用いて間接参照を明示的に表す2通りの方法があります。

i = pf(iValue , pointer);
i = (* pf)(iValue , pointer);

どちらを用いるかは開発者の好みですが、一般的には通常の関数と関数ポインタを区別する必要はないため、関数ポインタでも通常の関数のように呼び出します。

コード1
#include <stdio.h>

void f(void) {
	printf("Kitty on your lap\n");
}

int main() {
	void (*fp)(void) = f;
	fp();
	(*fp)();
	return 0;
}
実行結果
コード1 実行結果

コード1では、引数を受け取らず値を返さない単純な関数を保存する fp ポインタを宣言し、このポインタを f() 関数のアドレスで初期化しています。このポインタは f() 関数のエントリーポイントを保存しているため、fp  ポインタを間接参照することで関数を呼び出すことができます。

このようなポインタによる関数の呼び出しは、実践でどのように役立てることができるのでしょうか。ひとつは、先ほど説明したように、実行時まで呼び出すべき関数が決定されていない場合などに有効です。例えば、システムに関数のアドレスを渡すことで、必要に応じてシステムから特定の関数を呼び出してもらうことができます。こうしたコールバック機能は、多くのシステムで採用されています。Microsoft Windows のようなグラフィカルなイベント駆動では、ユーザーがボタンを押すなどのアクションによって処理を実行するため、イベントが発生するまではプログラムを待機させる必要があります。そこで、イベントが発生した時に呼び出してほしい関数のポインタをあらかじめシステムに登録しておくという方法が用いられています。

あるいは、開発者が自由に機能を拡張できるシステムを構築する時にも用いられます。関数のポインタ型配列を用意し、必要に応じてこれに開発者が関数のポインタを登録します。システムは処理を行うときに、配列から順に関数を呼び出すのです。この関数の鎖によって関数の呼び出しを自由に連鎖させることができるため、システムは拡張性の高い柔軟なものとなります。これは、オブジェクト指向の継承やオーバーライドの考え方に結びつきます。

コード2
#include <stdio.h>
#define ADD_FUNC 2
#define REMOVE_FUNC 4
#define RESULT_OK 0
#define RESULT_ERROR 1

typedef void(* REGISTERPROC)(const char* , int);

int RegisterFunc(REGISTERPROC func , int mode) {
	static int iLen = 0;
	static REGISTERPROC procs[256];
	int iCount;
	char removed;

	switch(mode) {
	case ADD_FUNC:	/*関数の追加*/
		if (iLen == 256) return RESULT_ERROR;

		/*同じ関数は追加できない*/
		for(iCount = 0 ; iCount < iLen ; iCount++)
			if (procs[iCount] == func) return RESULT_ERROR;

		procs[iLen] = func;
		iLen++;

		for(iCount = 0 ; iCount < iLen ; iCount++)
			(*procs[iCount])("Add" , iCount);
		break;
	case REMOVE_FUNC:	/*関数の削除*/
		removed = 0;
		for(iCount = 0 ; iCount < iLen ; iCount++) {
			if (procs[iCount] == func) {
				for(;iCount < iLen - 1 ; iCount++)
					procs[iCount] = procs[iCount + 1];
				iLen--;

				for(iCount = 0 ; iCount < iLen ; iCount++)
					(*procs[iCount])("Remove" , iCount);

				removed = 1;
				break;
			}
		}
		if (!removed) return RESULT_ERROR;
		break;
	default:
		return RESULT_ERROR;
	}

	return RESULT_OK;
}

void Function1(const char * msg , int iValue) {
	printf("Function1 : msg = %s , iValue = %d\n" , msg , iValue);
}
void Function2(const char * msg , int iValue) {
	printf("Function2 : msg = %s , iValue = %d\n" , msg , iValue);
}
void Function3(const char * msg , int iValue) {
	printf("Function3 : msg = %s , iValue = %d\n" , msg , iValue);
}

int main() {
	printf("Add Function1\n");
	RegisterFunc(Function1 , ADD_FUNC);

	printf("\nAdd Function2\n");
	RegisterFunc(Function2 , ADD_FUNC);

	printf("\nAdd Function3\n");
	RegisterFunc(Function3 , ADD_FUNC);

	printf("\nRemove Function2\n");
	RegisterFunc(Function2 , REMOVE_FUNC);

	printf("\nRemove Function3\n");
	RegisterFunc(Function3 , REMOVE_FUNC);

	return 0;
}
実行結果
コード2 実行結果

コード2は、関数のポインタが実践でどのような効果を生み出してくれるかを知るための試験的なコールバック設定関数 RegisterFunc() 関数を作成しています。RegisterFunc() 関数は第1引数に REGISTERPROC 型の関数のポインタを、第2引数に追加するか削除するかを表す数値を指定します。関数が追加や削除に成功すれば 0 以外を、そうでなければ 0 を返します。

まず、このプログラムで興味深いのは関数の型に別名をつけていることです。関数のポインタ型には typedef 記憶クラス指定子を用いて名前を付けることができるのです。この場合は、関数名にあたる部分がポインタ型の別名となります。REGISTERPFOC 型は第1引数に文字列へのポインタを、第2引数に数値を受け取ります。これらの関数は発生したイベントを示す文字列と、登録されている関数チェインのインデックスを受け取ります。

RegisterFunc() 関数は最大で 256 の関数を登録することができます。関数を登録する場合は第2引数に ADD_FUNC を指定し、解除する場合は REMOVE_FUNC を指定します。関数が登録されると、RegisterFunc() 関数は登録する関数及びすでに登録済みの関数を呼び出し、イベントとして通知します。解除する場合は、解除された関数を除いて登録されている関数にイベントを通知します。

実行結果を見れば、追加や解除が行われるたびに、RegisterFunc() 関数は登録されている関数を呼び出して追加や解除があったことを伝えていることが確認できます。このように、システムに登録して機能を拡張するような手法を用いる場合、登録する関数をユーザー定義コールバック関数と呼んだりすることがあります。設計の分野になるので詳細は避けますが、高度なシステムは開発者がシステムの拡張を簡単に実現できるように、こうした動的な機能拡張手段を提供するべきです。そうすれば、開発者はシステムの実体を知ることなく、目的の機能だけを拡張することができるからです。