ユーニックス総合研究所

  • home
  • archives
  • c-function-pointer

C言語の関数ポインタの使い方

  • 作成日: 2021-10-26
  • 更新日: 2023-12-26
  • カテゴリ: C言語

C言語の関数ポインタの使い方

C言語には関数のアドレスを保存する関数ポインタというポインタがあります。

関数ポインタはポインタの一種ですが、書き方が少し独特です。
これはC言語の文法の影響です。

関数ポインタはコールバックや構造体の関数の設定などで使われます。
関数ポインタを知っておくとC言語で書ける処理が広がります。

この記事では関数ポインタについて具体的に解説していきます。
まとめると↓を見ていきます。

  • 関数ポインタとは?
  • 関数ポインタの使い道
  • 関数ポインタの注意点
  • 関数ポインタの宣言方法
  • 関数ポインタに関数のアドレスを代入する
  • 関数ポインタから関数を呼び出す
  • 関数ポインタ型をtypedefする
  • 構造体に関数ポインタを保存する
  • 関数の引数に関数ポインタを渡す
  • 関数ポインタに不正な値が入っていたら?

関連記事
プログラミングのポインタをわかりやすく解説【C言語】
C言語のポインタのメリットとは?コピーしますかメモリを共有しますか
C言語のポインタのポインタを解説

関数ポインタとは?

関数ポインタとは関数のアドレスを保存するポインタです。

C言語の関数にはアドレスがあります。
そのためそのアドレスはポインタに保存することができます。
この時に使われるのが関数ポインタです。

関数ポインタは関数の引数に渡すコールバック関数や、あるいは構造体に宣言する関数として使われることがあります。
たとえばC言語の標準ライブラリであるqsortは値の比較にコールバック関数を使います。
ちなみにコールバック関数とは、関数の引数に渡される関数のことを言います。
このコールバック関数は関数内で使われます。

関数ポインタを使うと関数のアドレスを保存することができます。
そして保存した関数のアドレスから関数を参照することが可能です。

関数ポインタの注意点

関数ポインタの注意点としては↓があります。

  • 記述が複雑

まず1つ目が関数ポインタは記述が複雑という点です。
関数ポインタの書き方は、C言語の文法の中でもかなり複雑なほうです。
慣れてしまうとなんてことはないのですが、慣れるまでちょっと大変かもしれません。

関数ポインタの宣言方法

関数ポインタはどのように宣言するのでしょうか?
関数ポインタの宣言の文法は?
具体的に見ていきます。

宣言の文法

関数ポインタの宣言は↓のような文法になっています。

返り値の型 (*関数ポインタ名)(引数の型);  

↑の場合、「関数ポインタ名」がポインタの変数名になります。
このように関数ポインタは独特な記法になっています。
関数のポインタなので、型の情報として返り値の型と引数の型が必要です。
そのため↑のような宣言になります。

関数ポインタの宣言

関数ポインタの宣言は↓のようにします。

int main(void) {  
    void (*funcptr)(int);  
    return 0;  
}  

↑の場合、funcptrが関数ポインタになります。
funcptrvoid型の返り値を返し、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()funcfuncptrのアドレスを出力すると、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言語の関数ポインタは覚えておくと役に立つシーンがけっこうあります。
記述は複雑ですが慣れてしまいましょう。

🦝 < 関数ポインタで関数呼び出し

🐭 < 何が呼ばれるかはお楽しみ