C言語の関数ポインタの簡単なサンプルコード

C言語には関数ポインタという関数への場所を指し示すポインタがあります。この関数ポインタの書き方(使い方)を忘れてしまうので、サンプルコードを載せておきます。使い方はコードを見ながら説明してみます。

コピペするなら、以下を使ってください。


#include <stdio.h>

/* 関数宣言 */
int func( int a, int b );

int main( void )
{
	/* 関数ポインタ「funcptr」の宣言 */
	int (*funcptr)(int, int);

	/* 関数ポインタへの代入 */
	funcptr = func;

	/* 関数ポインタから関数の実行 */
	printf("関数func()の戻り値:%d\n", (*funcptr)( 3, 4 ) );

	return 0;
}

int func( int a, int b )
{
	return( a * b );
}

main関数から整数の「3」と「4」をfunc関数に渡して、func関数では「3×4」の結果をmain関数に返しています。実行結果は「関数func()の戻り値:12」と画面に表示されます。

まず最初に、関数へのポインタを格納する変数を「funcptr」として宣言しています。func関数は2つのint型の変数を引数に取り、戻り値がint型の関数です。この関数へのポインタ宣言は以下となります。

int (*funcptr)(int, int);

あくまでポインタ変数の宣言であるためfuncptrの前には「*(アスタリスク)」が必要です。またfuncptrをカッコで囲っていますが、このカッコも必要です。もしカッコがなかった場合、

int *funcptr(int, int);

となりますが、これはfuncptrが2つのintを引数にしてintのポインタを返す「関数」ということになってしまいますので。

次に、関数ポインタに関数へのアドレスを代入します。それが以下となります。

funcptr = func;

ここで注意すべきはfuncにカッコがないことです。つまり「funcptr = func();」としてはダメです。もしfuncにカッコがあるとfunc関数の呼び出しが行われてしまうからです。またfunc関数の前に「&演算子」がないことにも気をつけましょう。関数の末尾にカッコをつけない形で関数を参照する場合、コンパイラは「その関数のアドレスを参照する」となっているためです。

最後に、関数ポインタが指し示すアドレスの関数を実行します。コード上では以下の部分です。

printf(“関数func()の戻り値:%d\n”, (*funcptr)( 3, 4 ) );

「(*funcptr)( 3, 4 )」の戻り値をprintf関数で印字しています。funcptrの前の「*(アスタリスク)」はポインタが指し示すオブジェクト(この場合は関数)にアクセスしなければならないため必要です。またfuncptrを囲むカッコも必要です。

ちなみに別の書き方として、以下でもコンパイルは通ります(gcc version 7.5.0 (Ubuntu 7.5.0-3ubuntu1~18.04)の場合)。

printf(“関数func()の戻り値:%d\n”, funcptr( 3, 4 ) );

こちらのほうが直感的にはわかりやすいかもしれません。どちらを使うかは、好みの問題でしょうか。

– – – – – – – – – –
追記しました。「ポインタを返す関数」への関数ポインタを使ったサンプルも載せておきます。

コピペするなら、以下を使ってください。


#include <stdio.h>
#include <string.h>

/* 関数宣言 */
char *func( char *c );

/* 外部変数 */
char message[100] = "Hello world. ";

int main( void )
{
	/* 関数ポインタ「funcptr」の宣言 */
	char *(*funcptr)(char *);

	/* 関数ポインタへの代入 */
	funcptr = func;

	/* messageの出力 */
	printf("%s\n", message );

	/* 関数ポインタからmessageの書き換え */
	printf("%s\n", (*funcptr)("HELLO WORLD!") );

	return 0;
}

char *func( char *c )
{
	strcat( message, c );
	return message;
}

実行結果は以下となります。

$ ./a.out
Hello world.
Hello world. HELLO WORLD!

1つ目のサンプルコードとの違いは、関数ポインタの宣言の際に「*(アスタリスク)」がもう一つ付くことです(charのポインタを返す関数ということを示すため)。

char *(*funcptr)(char *);

関数ポインタへの代入や、関数ポインタからの関数の実行の仕方は同じになります。

C言語の記号定数「EOF」を使うときの心構え

「プログラミング言語C」という書籍に書かれていた内容で印象に残ったコトの話です。まずは以下のソースを見てください。


#include <stdio.h>

int main(void)
{
	int c;

	c = getchar();
	while( c != EOF )
	{
		putchar(c);
		c = getchar();
	}
	printf("終了\n");
}

キーボードで打った文字をディスプレイ画面に表示するだけのプログラムです。EOFを検知するとプログラム終了となります。Linux環境だとEOFはキーボードで「ctrl」+「D」(同時に押す)を入力します。実行結果は以下です。

$ ./a.out
a
a
x
x
終了
$

このときは「a」を入力してリターンキー押下、「x」を入力してリターンキー押下、そして「ctrl」+「D」の入力をしました。

さて、最初のプログラムソースですが違和感とかありますでしょうか?

僕は特になかったです(というかまったく気づいていませんでした)。何を気にするかというと、変数「c」の型です。cには文字が格納できればいいわけですから、intと宣言しなくてもcharで事足りるのでは?ということです。キーボードからの1文字を入力して、ディスプレイにその1文字を表示しているだけですから。実際に変数cの宣言をchar型に変更して同じようにやってみたところ、結果は全く同じでした。

でも、intで宣言する理由がちゃんとあったのです。それは記号定数「EOF」が、はたしてどんな値なのか?ということによります。つまり1バイトしかないchar型でEOFを格納できるのか?ということです。もしEOFが1バイトで収まらなかった場合、桁溢れを起こしてしまいますのでwhileの終了条件にはいつまでたっても合致しません。そのためEOFが収まる十分大きなint型として変数cを宣言していたのです。

では、charとintでどれだけ大きさの違いがあるのかを確認するためlimits.hを見てみました。以下はlimits.hからの抜粋です。


/* Minimum and maximum values a `signed char' can hold.  */
#  define SCHAR_MIN     (-128)
#  define SCHAR_MAX     127

/* Maximum value an `unsigned char' can hold.  (Minimum is 0.)  */
#  define UCHAR_MAX     255

/* Minimum and maximum values a `signed int' can hold.  */
#  define INT_MIN       (-INT_MAX - 1)
#  define INT_MAX       2147483647

/* Maximum value an `unsigned int' can hold.  (Minimum is 0.)  */
#  define UINT_MAX      4294967295U

64ビットOSのLinux(ubuntu)を使用しているのですが、charは1バイト、intは4バイト(2の32乗)の領域を確保しています。ただ先にも書いたように変数cはchar型でも問題なく動きました。EOFの値が何なのかを調べることは可能で、実際に計算させるか、stdio.hにEOFが定義されているのでそれを見るか、です。

実際に計算させるには以下のソースをコンパイルして実行します。


#include <stdio.h>

int main(void)
{
	printf("EOF is %d\n", EOF);
}

結果は「EOF is -1」と表示されます。stdio.hを見てみるとEOFの定義があります。以下は抜粋です。


/* End of file character.
   Some things throughout the library rely on this being -1.  */
#ifndef EOF
# define EOF (-1)
#endif

結果を言うと、EOFは「-1」だからcharでも問題ないわけですね。ただ、EOFは「-1」と覚えてしまうのは危険です。というのもLinux環境の場合、OSがEOFを検知した際にプログラムに「-1」を返すだけであって、他の機種やOSの場合は異なる可能性があるからです。ですから、EOFが収まるであろう十分大きな型であるintを使っているということでしょう。なお「-1」というのはASCII文字コード上には表れない値なので、OSがEOFの印として使っているのでしょう。

ちなみに、記号定数「NULL」についても書いておきます。NULLは通常0(数字のゼロ)なのですが、これは数値ではなくポインタアドレスの位置であって、位置がゼロ(すなわちアドレスがない)ということになります。数字と考えるよりアドレスがあるかないかの印と思ったほうが良いです。なので、NULLを数値のゼロと思ってポインタでない変数の条件式(等号、不等号とか)で使うのはやめましょう。