みなさんこんにちは!
今回はC言語を学ぶ上で難関の一つでもある『関数ポインタ』について解説していきます。
関数ポインタって、どんなポインタなの?
『関数ポインタ』というくらいなので、あのポインタの一種であることは容易に想像できますよね!
私たちが普通に使っているポインタは、
多くの場合は、int型やchar型のアドレスを指すポインタや、自分で定義した構造体のアドレスを指すポインタでした。
つまり、変数のアドレスを指していたものだったのですが
関数ポインタは、関数の先頭アドレス(正確には関数のコンパイル結果の機械語列の先頭アドレス)を指すポインタのことを言います。
概念としてはそこまで難しくないのですよね。
しかし、C言語においては、
関数ポインタの記述する際に『かっこ()』が多様されるために、複雑に見えてしまうので、
一般的に多くの人たちから、難しいものだと思われています。
実際に関数ポインタを使ってみよう
関数ポインタをどのように利用するのかを理解するために、以下のソースコードを実際に実行してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
#include <stdio.h> int add(int a, int b) { return a + b; } int sub(int a, int b) { return a - b; } int mul(int a, int b) { return a * b; } void printDemo(char *words) { printf("%s\n", words); return; } int main(void) { /* 関数ポインタ1 */ int (*func1)(int, int); func1 = add; printf("関数ポインタ1: %d + %d = %d\n", 2, 5, func1(2, 5)); func1 = sub; printf("関数ポインタ1: %d - %d = %d\n", 2, 5, func1(2, 5)); /* 関数ポインタ2 */ void (*func2)(char*); func2 = printDemo; func2("関数ポインタ2: りんご"); /* 関数ポインタの配列 */ int (*funcArr[3])(int, int); funcArr[0] = add; funcArr[1] = sub; funcArr[2] = mul; printf("関数ポインタの配列: %d + %d = %d\n", 3, 10, funcArr[0](3, 10)); printf("関数ポインタの配列: %d - %d = %d\n", 3, 10, funcArr[1](3, 10)); printf("関数ポインタの配列: %d * %d = %d\n", 3, 10, funcArr[2](3, 10)); } |
このコードを実行すると、
1 2 3 4 5 6 7 8 9 10 |
$ gcc funcPointer.c $ ./a.out 関数ポインタ1: 2 + 5 = 7 関数ポインタ1: 2 - 5 = -3 関数ポインタ2: りんご 関数ポインタの配列: 3 + 10 = 13 関数ポインタの配列: 3 - 10 = -7 関数ポインタの配列: 3 * 10 = 30 |
という結果が表示されるかと思います。
関数ポインタは、
関数の先頭アドレスを指すポインタですが、指している関数の返り値と引数の型に合わせてあげる必要があります。
具体的にはソースコードにもありますが、
add(),sub(),mul()のようなint型の引数を二つもち、返り値がint型の関数を指すポインタを格納する変数は、
int (*func1)(int, int);
のように定義します。
また、printDemo()のようなchar*型の引数を一つもち、返り値がvoid型の関数を指すポインタを格納する変数は、
void (*func2)(char *);
のように定義します。
ソースコードの最後の方にも記述してあるのですが、関数ポインタも、他の変数と同様に配列を作ることも可能です。
関数ポインタを使うメリット・デメリットは?
関数ポインタを使うメリットやデメリットには、どのようなものがあるのでしょうか?
メリットとしては、
これまでは関数名を指定してfunc()のようにして関数を直接的にしか呼び出せなかったのですが
関数ポインタを使用することによって間接的に呼び出せるようになるということがあります。
間接的に呼び出せるので、関数ポインタの配列を作ってfor文やwhile文でいろんな関数を呼び出したり
コールバック関数やプロセス間通信で重要な役割を果たすsignal()などでも、関数ポインタが活用されています。
一方でデメリットとしては、
C言語においては関数ポインタを記述すると複雑すぎてプログラムの可読性が下がってしまうことです。
例としてsignal()の仕様を見てみましょう。
1 2 3 4 |
#include <signal.h> void (*signal(int sig, void (*handler)(int)))(int); |
どうでしょうか?
C言語に相当慣れている人でないと、一瞬で理解するのは難しいと思います。
signal()を日本語で説明しても、
signal()は、引数がint型、返り値がvoid型の関数へのポインタを返す関数で、
引数として、int型と、int型を引数として受け取りvoid型を返り値として返す関数へのポインタを返す。
というふうに非常に読み難い表現になってしまいます。
これが関数ポインタのデメリットです。
ついでに、『signal()について詳しく知りたい!』という方は、以下の記事も読んでみてください。
関数ポインタの記述方法をさらに詳しく解説
関数ポインタを使うデメリットとして『複雑な記述になり可読性が下がる』ということを挙げましたが、
実際問題として、signal()などの重要なシステムコールなどでも、関数ポインタが使われています。
なので自分がコーディングするときに関数ポインタを使わなかったとしても、
関数ポインタを利用している重要な関数を使う機会はたくさんあると思うので、読み方だけでも理解しておくといいでしょう。
1 2 3 |
void (*signal(int sig, void (*func)(int)))(int) |
これはsignal()というライブラリ関数の一つですが、
使われている『かっこ()』が
・関数の引数をまとめる() ← 『*』よりも結合の優先順位が高い
・グループ化をするための() ←『*』よりも結合の優先順位が低い
の2種類に使い分けられています。
なので、
『int *func(int)』はint型のポインタを返す関数という意味で、
『int (*func)(int)』はint型を返す関数のポインタという意味になります。
関数ポインタの記述方法をすぐに振り返れるように、
早わかり表も作ってみました。
int func() | intを返す関数 |
---|---|
int *func() | intのポインタを返す関数 |
int (*func)() | intを返す関数のポインタ |
int (*arr)[] | intの配列のポインタ |
int *arr[] | intのポインタの配列 |
int *(*func)() | intのポインタを返す関数のポインタ |
int (*func())() | intを返す関数のポインタを返す関数 |
int (*func())[] | intの配列のポインタを返す関数 |
int (*arr[])() | intを返す関数のポインタの配列 |
int (*(*func[])())[] | intの配列のポインタを返す関数のポインタの配列 |
関数ポインタについて分からなくなったり読み方を忘れたりした場合には、ぜひこの表を活用してください。
読み方にはいくつがあるのですが、オススメの読み方は外側から読む方法です(コンパイラは実際に左側からコードを解釈していくから)
『int (*(*func[])())[]』を例に説明してみると、
int A[] | int型の配列 |
---|---|
int (*B)[] | int型の配列を指すポインタ |
int (*C())[] | int型の配列を指すポインタを返す関数 |
int (*(*D)())[] | int型の配列を指すポインタを返す関数を指すポインタ |
int (*(*E[])())[] | int型の配列を指すポインタを返す関数を指すポインタの配列 |
というふうにして解釈していきます。
この読み方なら、コードを見るときに左から自然に読んでも、読み返すこと無く解釈することができます。
関数ポインタの複雑さは『typedef』で解消できる
ここまで、関数ポインタについて解説してきましたが
デメリットである『複雑さ』は、typedefという機能を使うことである程度、解消することができます。
以下のtypedefを使ったプログラムを実行してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include <stdio.h> typedef int (*newType)[]; /* typedefを使った表現 */ newType func1() { int arr1[5] = {1, 2, 3}; return &arr1; } /* typedefを使わない表現 */ int (*func2())[] { int arr2[5] = {100, 200, 300}; return &arr2; } int main() { printf("typedefあり: %d %d %d\n", (*func1())[0], (*func1())[1], (*func1())[2]); printf("typedefなし: %d %d %d\n", (*func2())[0], (*func2())[1], (*func2())[2]); return 0; } |
すると、このような結果になると思います。
1 2 3 4 |
typedefあり: 1 2 3 typedefなし: 100 200 300 |
この機能のおかげで、
『int (*A)[]』というint型の配列のポインタをnewTypeとして新しく定義してあげれば、
これまでは
『int (*func2())[] {}』というようにして記述していた関数が、
『newType func1() {}』という、誰でも簡単に理解できる記述ができるようになります。
まとめ
今回は関数ポインタについて解説してきましたが、いかがだったでしょうか?
C言語の難関であるポインタと、関数ポインタの複雑な記述方法が合わさって、あ
はじめは理解するのに時間がかかるかもしれませんが
落ち着いて、しっかり仕組みを理解すれば誰でも分かるようになります。
最後まで読んでいただきありがとうございました。