C言語の関数ポインタの使い方
目次
- C言語の関数ポインタの使い方
- 関数ポインタとは?
- 関数ポインタの注意点
- 関数ポインタの宣言方法
- 関数ポインタに関数のアドレスを代入する
- 関数ポインタから関数を呼び出す
- 関数ポインタ型をtypedefする
- 構造体に関数ポインタを保存する
- 関数の引数に関数ポインタを渡す
- 関数ポインタに不正な値が入っていたら?
- おわりに
C言語の関数ポインタの使い方
C言語には関数のアドレスを保存する関数ポインタというポインタがあります。
関数ポインタはポインタの一種ですが、書き方が少し独特です。
これはC言語の文法の影響です。
関数ポインタはコールバックや構造体の関数の設定などで使われます。
関数ポインタを知っておくとC言語で書ける処理が広がります。
この記事では関数ポインタについて具体的に解説していきます。
まとめると↓を見ていきます。
関数ポインタとは?
関数ポインタの使い道
関数ポインタの注意点
関数ポインタの宣言方法
関数ポインタに関数のアドレスを代入する
関数ポインタから関数を呼び出す
関数ポインタ型をtypedefする
構造体に関数ポインタを保存する
関数の引数に関数ポインタを渡す
関数ポインタに不正な値が入っていたら?
関連記事
プログラミングのポインタをわかりやすく解説【C言語】
C言語のポインタのメリットとは?コピーしますかメモリを共有しますか
C言語のポインタのポインタを解説
関数ポインタとは?
関数ポインタとは関数のアドレスを保存するポインタです。
C言語の関数にはアドレスがあります。
そのためそのアドレスはポインタに保存することができます。
この時に使われるのが関数ポインタです。
関数ポインタは関数の引数に渡すコールバック関数や、あるいは構造体に宣言する関数として使われることがあります。
たとえばC言語の標準ライブラリであるqsort
は値の比較にコールバック関数を使います。
ちなみにコールバック関数とは、関数の引数に渡される関数のことを言います。
このコールバック関数は関数内で使われます。
関数ポインタを使うと関数のアドレスを保存することができます。
そして保存した関数のアドレスから関数を参照することが可能です。
関数ポインタの注意点
関数ポインタの注意点としては↓があります。
記述が複雑
まず1つ目が関数ポインタは記述が複雑という点です。
関数ポインタの書き方は、C言語の文法の中でもかなり複雑なほうです。
慣れてしまうとなんてことはないのですが、慣れるまでちょっと大変かもしれません。
関数ポインタの宣言方法
関数ポインタはどのように宣言するのでしょうか?
関数ポインタの宣言の文法は?
具体的に見ていきます。
宣言の文法
関数ポインタの宣言は↓のような文法になっています。
返り値の型 (*関数ポインタ名)(引数の型);
↑の場合、「関数ポインタ名」がポインタの変数名になります。
このように関数ポインタは独特な記法になっています。
関数のポインタなので、型の情報として返り値の型と引数の型が必要です。
そのため↑のような宣言になります。
関数ポインタの宣言
関数ポインタの宣言は↓のようにします。
int main(void) { void (*funcptr)(int); return 0; }
↑の場合、funcptr
が関数ポインタになります。
funcptr
はvoid
型の返り値を返し、int
型の引数を1つ取る関数の関数ポインタです。
宣言したfuncptr
には関数のアドレスを代入することができます。
関数ポインタに関数のアドレスを代入する
先ほどの関数ポインタfuncptr
に関数のアドレスを代入してみます。
#include <stdio.h> void func(int a) { } int main(void) { void (*funcptr)(int) = func; printf("func = %p\n", func); // func = 0x4004b2 printf("funcptr = %p\n", funcptr); // funcptr = 0x4004b2 return 0; }
↑のように宣言と同時にfuncptr
を初期化するには、関数をfuncptr
に代入します。
printf()
でfunc
とfuncptr
のアドレスを出力すると、2つのアドレスは一致しているのがわかります。
代入する関数func
と関数ポインタfuncptr
の返り値と引数の型は一致している必要があります。
一致していない場合はコンパイラが警告を出力することがあります。
関数ポインタから関数を呼び出す
先ほどの関数funcptr
から関数を呼び出してみます。
#include <stdio.h> void func(int a) { printf("a = %d\n", a); } int main(void) { void (*funcptr)(int) = func; funcptr(10); // a = 10 return 0; }
↑の場合、関数ポインタfuncptr
には関数func()
のアドレスが入っています。
funcptr
にカッコを付けて引数10
を渡して呼び出すと、func
が実行されます。
func()
内のprintf()
が実行されてfunc()
の引数a
の値(つまり10
)が出力されます。
このように関数ポインタを使うと、間接的に関数を呼び出すことができます。
関数ポインタからは返り値も受けることができて、関数ポインタに引数を渡すこともできます。
関数ポインタ型をtypedefする
関数ポインタは慣れると便利ですが、関数ポインタの宣言のためにいちいち返り値や引数の型を書いたりするのはめんどくさいです。
そのため関数ポインタはよくtypedef
と組み合わされて使われます。
typedef
を使うと関数ポインタ型を宣言することが可能です。
関数ポインタ型をtypedef
するには↓のようにします。
#include <stdio.h> // 関数ポインタ型「FuncPtr」の定義 typedef void (*FuncPtr)(int); void func(int a) { printf("a = %d\n", a); } int main(void) { FuncPtr funcptr = func; // 関数ポインタがスッキリ書ける funcptr(10); // a = 10 return 0; }
↑の場合、FuncPtr
が関数ポインタ型になります。
この関数ポインタのtypedef
の方法も独特な記法ですが、慣れるしかありません。
定義したFuncPtr
型で関数ポインタfuncptr
を定義しています。
このように関数ポインタ型を使うと関数ポインタをスッキリ書くことができます。
関数の引数が長くなったり、返り値の型に構造体などを使っていると、関数ポインタの宣言は長くなってしまいます。
しかし関数ポインタ型を宣言すれば、そういっためんどうな記述をしなくて済みます。
関数ポインタを何度も使う場合はtypedef
したほうがいいでしょう。
構造体に関数ポインタを保存する
構造体に関数ポインタを宣言する方法です。
#include <stdio.h> struct Calculator { int (*add)(int, int); }; int add(int a, int b) { return a + b; } int main(void) { struct Calculator calculator = { .add=add, }; int result = calculator.add(1, 2); printf("%d\n", result); // 3 return 0; }
↑のように構造体Calculator
の中に関数ポインタを書くことができます。
構造体変数(calculator
)の初期化のさいに関数ポインタに関数を代入しておきます。
そうすると構造体変数の持つ関数ポインタから関数を呼び出すことができます。
クラスのメソッドのように構造体の関数を呼び出す
この方法を使うとC言語でクラスとメソッドのようなものを設計できます。
具体的には↓のようにやります。
#include <stdio.h> struct Animal { const char *name; void (*walk)(struct Animal *); }; void animal_walk(struct Animal *animal) { printf("%s walking\n", animal->name); } int main(void) { struct Animal animal = { .name="Tama", .walk=animal_walk, }; animal.walk(&animal); // Tama walking return 0; }
構造体Animal
のメンバname
がメンバ変数に当たります。
そしてメンバwalk
はクラスで言うところのメソッドになります。
walk
の第1引数に構造体変数のポインタを渡すようにします。
そうするとクラスとクラスのメソッドのような実装が可能になります。
↑の例で言うと、animal_walk()
の第1引数animal
は、C++で言うところのthis
, Pythonで言うところのself
になります。
つまりクラスのオブジェクトを指すポインタです。
関数の引数に関数ポインタを渡す
関数の引数に関数ポインタを渡す方法です。
↓のようにやります。
#include <stdio.h> void func(int a) { printf("a = %d\n", a); } void test(void (*funcptr)(int a)) { funcptr(10); // a = 10 } int main(void) { test(func); return 0; }
↑の場合、test()
は引数に関数ポインタfuncptr
を取ります。
test()
は内部でfuncptr
を参照して呼び出しています。
main
関数内ではtest()
にfunc()
のアドレスを渡しています。
こうすることでtest()
の引数の関数ポインタfuncptr
に、func()
のアドレスが代入されます。
test()
内でfuncptr
を参照して呼び出すと、func()
が呼び出されます。
結果、出力は「a = 10
」になります。
この設計はC言語の標準ライブラリqsort
などに見られる設計です。
test()
のfuncptr
はコールバック関数と呼ばれます。
関数ポインタに不正な値が入っていたら?
関数ポインタに不正なアドレスが入っていたらどうなるのでしょうか?
不正なアドレスが入ったまま呼び出すと?
実際にやってみます。
関数ポインタとNULLポインタ
関数ポインタにNULLポインタを入れて呼び出してみます。
#include <stdio.h> int main(void) { void (*funcptr)(void) = NULL; // NULLポインタを関数ポインタに代入 funcptr(); // Segmentation fault return 0; }
結果は「Segmentation fault
」になりました。
つまりプログラムがクラッシュしています。
このようにNULLポインタを代入している関数ポインタを参照すると、プログラムがクラッシュする場合があります。
関数ポインタと不正なアドレス
では不正なアドレスはどうでしょうか?
存在しないアドレスを関数ポインタに代入してみます。
int main(void) { void (*funcptr)(void) = 1234; funcptr(); // Segmentation fault return 0; }
結果は同じく「Segmentation fault
」になりました。
GCCコンパイラの場合はコンパイルで警告が出力されますが、エラーにはなりません。
このように存在しないアドレスを関数ポインタに入れて参照した場合はプログラムがクラッシュすることがあります。
おわりに
今回はC言語の関数ポインタについて詳しく見てみました。
C言語の関数ポインタは覚えておくと役に立つシーンがけっこうあります。
記述は複雑ですが慣れてしまいましょう。
(^ _ ^) | 関数ポインタで関数呼び出し |
(・ v ・) | 何が呼ばれるかはお楽しみ |