徹底図解!C言語の関数ポインタについて解説

下の記事で主に変数のアドレスを指すポインタについて説明しました。

ポインタ解説ページのアイキャッチ徹底図解!C言語ポインタを初心者向けに分かりやすく解説

関数も変数同様にメモリ上に配置されるためアドレスを持っており、ポインタで指すことができます。関数を指すポインタは関数ポインタと呼ばれます。

ここでは関数ポインタの考え方と簡単な使い方を紹介していきたいと思います。

関数ポインタ

まずは関数ポインタがどのようなものであるかを解説していきます。

関数ポインタとは

関数ポインタとはその名の通り、関数を指すポインタです。

関数も変数等と同様にプログラム実行時にメモリ上に展開され、メモリ上に存在することになります。

関数がメモリに展開される様子

ポインタとはメモリ上のアドレスを指すものであり、メモリ上に存在する関数も、変数同様にポインタで指すことができます。

ポインタが関数を指す様子

この関数を指すポインタが関数ポインタです。

関数ポインタの宣言

関数ポインタも他の変数同様に変数宣言を行います。関数ポインタの変数宣言は下記のように行います。括弧の位置が特殊ですので注意してください。

戻り値の型 (*変数名)(引数1の型, 引数2の型);

戻り値の型や引数の数は指すポインタに応じて変更する必要があります。

例えば下記のような関数であれば、

int functionA(int a, char b, int *c){
    /* 処理 */
}

戻り値の型が「int」、引数1の型が「int」、引数2の型が「char」、引数3の型が「int*」ですので、この関数を指す関数ポインタは下記のように変数宣言を行います。

int (*funcPtr)(int, char, int*);

関数ポインタへのアドレス格納

関数ポインタへのアドレス格納をするためには、単純に関数ポインタへ関数名を代入すれば良いです。これにより関数ポインタに関数へのアドレスが格納され、関数ポインタがその関数を指すことになります。

funcPtr = functionA;

関数ポインタの型に合った関数(同じ戻り値かつ同じ引数の型の関数)であれば他の関数も指すことが可能です。

関数ポインタによる関数の実行

関数ポインタによる関数の実行は、通常の関数実行と同様にして行うことが可能です。ただし、関数名には関数ポインタの変数名を使用することに注意してください。

int (*funcPtr)(int, char, int*);
int x = 10;
char y = 'y';
int ret;

funcPtr = functionA;

ret = funcPtr(x, y, &x);

関数ポインタを用いたプログラム例

関数ポインタを用いた簡単なプログラムを作って見ました。こちらを見ながら説明していきましょう。

#include <stdio.h>

int add(int, int);
int sub(int, int);
int mul(int, int);
int div(int, int);

int add(int input1, int input2){
  printf("execute %d + %d\n", input1,input2);
  return input1 + input2;
}

int sub(int input1, int input2){
  printf("execute %d - %d\n",input1,input2);
  return input1 - input2;
}

int mul(int input1, int input2){
  printf("execute %d * %d\n",input1,input2);
  return input1 * input2;
}

int div(int input1, int input2){
  printf("execute %d / %d\n",input1,input2);
  return input1 / input2;
}

int main(void){
  int i;
  int in1;
  int in2;
  int ans;
  int (*func)(int, int);
  int (*funcarray[4])(int, int);

  printf("2つの数字を入力してください\n");
  printf("まず一つ目:");
  scanf("%d", &in1);
  printf("次に二つ目:");
  scanf("%d", &in2);

  func = add;
  ans = func(in1, in2);
  printf("answer is %d\n", ans);

  funcarray[0] = add;
  funcarray[1] = sub;
  funcarray[2] = mul;
  funcarray[3] = div;

  for(i = 0; i < 4; i++){
    ans = funcarray[i](in1, in2);
    printf("answer is %d\n", ans);
  }

  return 0;
}

下記の4つの関数を作成しており、この関数を関数ポインタで指して実行するプログラムになっています。

  • add
  • sub
  • mul
  • div

この4つの関数は全て戻り値と引数は下記になっています。

  • 戻り値:int
  • 引数1の型:int
  • 引数2の型:int

したがって、上記の4つの関数は下記の型の関数ポインタにより指すことが可能です。

int (*変数名)(int, int);

このプログラムにおいては add, sub, div, mul の4つの関数を指す関数ポインタとして下記の2つを変数宣言しています。

int (*func)(int, int);
int (*funcarray[4])(int, int);

func は単純な関数ポインタですが、funcarray は関数ポインタの配列となっています。このように変数名に “[ ]” を付けることで、通常の変数同様に配列として扱うことが可能です。

func には下記で add 関数のアドレスを格納しています。

func = add;

これにより関数ポインタ func は add を指すことになります。

funcがadd関数を指す様子

関数ポインタの配列である funcarray には下記でそれぞれの関数のアドレスを格納しています。

funcarray[0] = add;
funcarray[1] = sub;
funcarray[2] = mul;
funcarray[3] = div;

これにより各関数を funcarray が指すことになります。

funcarrayが各関数を指す様子

さらにこの格納したアドレスの関数を下記でコールして実行しています。

for(i = 0; i < 4; i++){
    ans = funcarray[i](in1, in2);
    printf("answer is %d\n", ans);
}

こんな感じで配列の添字を変更することで実行される関数を切り替えることができます。普通なら実行するたびに if 文を使う必要がありますが、それが不要になるので関数実行部分のプログラムが少しスッキリします。

スポンサーリンク

関数ポインタを使う場面

続いて関数ポインタをどのような場面で使用するかについて解説していきます。

コールバック関数

関数のアドレスを渡し、渡した先でその関数を実行してもらうようなことも可能です。こういう使い方をする関数をコールバック関数と呼びます。コールバック関数が特に重宝するのがマルチスレッドなどの並列プログラミング時です。

他のスレッドにコールバック関数の関数ポインタを渡し、処理が終わったタイミングにそのコールバック関数を実行してもらうことで処理が終わったことを通知してもらうようなことが可能です。

簡単なコールバック関数の例

ただマルチスレッドのプログラムを書くと長くなるので、ここでは簡単なコールバック関数の例で説明したいと思います。

コールバック関数の使用例となるプログラムは下記になります。

#include <stdio.h>

int add(int x, int y, int z){
    return x + y + z;
}

int func(int (*callback)(int, int, int), int x){
    return callback(x, x, x);
}

int main(void){
   int ret;

   ret = func(add, 100);

   printf("ret = %d\n", ret);

   return 0;
}

ポイントは関数 func の引数の一つ目です。

int func(int (*callback)(int, int, int), int x)

この引数は関数ポインタの型になります。これにより func 関数実行時に、第一引数として、戻り値が「int」で引数1が「int」、引数2が「int」、引数3が「int」の関数を指定することが可能になります。

そして func 関数実行時に第一引数に add 関数を指定することにより、func 関数の中で関数ポインタから add 関数を実行することが可能になっています。

func 関数に渡す関数ポインタを状況等に応じて異なる関数のものにするようにすれば、func 関数で様々な処理を実行させることができるようになります。

シグナルハンドラを用いたコールバック関数の例

また、シグナルハンドラを用いてコールバック関数の動きを確認することもできます。シグナルハンドルを用いたプログラムは下記になります。

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

int a = 0;

void signalHandler(int signum){
  a = 100;
}

int main(void){

  int i;
  void* ret;
  ret = signal(SIGINT, signalHandler);
  if(ret == SIG_ERR) {
    return -1;
  }

  for(i = 0; i < 10; i++){
    printf("a = %d\n", a);
    usleep(100000);
  }

  return 0;
}

下記はsignal関数と呼ばれる関数を実行している箇所になります。

  ret = signal(SIGINT, signalHandler);

signal 関数は「第一引数で指定されたシグナルを受信した際に、第二引数で指定されたアドレスの関数を実行する」関数です。第二引数のところに関数ポインタが使われています。

シグナルの詳しい説明はここでは行いませんが、上記は Ctrl+C のキーボード入力を受信した際に、第二引数で指定されるアドレスの関数(つまり signalHandler 関数)を実行するというプログラムになります。signalHandler 関数は下記の通りで、Ctrl+C を受信した際に a の値を変更しています。

void signalHandler(int signum){
  a = 100;
}

実行結果は下記の通りになります。Ctrl+Cが押されたタイミングで signalHandler 関数が実行され、a の値が変わっていることに注目してください。

a = 0

a = 0

a = 0

a = 0

a = 0

^Ca = 100

a = 100

a = 100

a = 100

a = 100

クラスの作成

C++やJavaにはクラスというものが存在します。クラスはメンバ変数とメソッドを持っていますよね。Cでも構造体と関数ポインタを用いれば構造体をクラスのような使い方ができるようになり、なんちゃってオブジェクト指向的なプログラムも書けるようになります。

・MathClass.h

#include <stdio.h>

typedef struct{
  int input1;
  int input2;
  int (*calc)(void);
  void (*input)(int, int);
} CALCULATOR_T;

int add(void);
int sub(void);
int mul(void);
int div(void);
void input(int, int);
CALCULATOR_T *newCalc(int);
void delCalc(void);

・MathClass.c

#include "MathClass.h"

CALCULATOR_T calculator;

int add(void){
  printf("execute %d + %d\n", calculator.input1,calculator.input2);
  return calculator.input1 + calculator.input2;
}

int sub(void){
  printf("execute %d - %d\n",calculator.input1,calculator.input2);
  return calculator.input1 - calculator.input2;
}

int mul(void){
  printf("execute %d * %d\n",calculator.input1,calculator.input2);
  return calculator.input1 * calculator.input2;
}

int div(void){
  printf("execute %d / %d\n",calculator.input1,calculator.input2);
  return calculator.input1 / calculator.input2;
}

void input(int in1, int in2){
  calculator.input1 = in1;
  calculator.input2 = in2;
}

CALCULATOR_T *newCalc(int type){
  calculator.input = input;

  switch(type){
  case 1:
    calculator.calc = add;
    break;
  case 2:
    calculator.calc = sub;
    break;
  case 3:
    calculator.calc = mul;
    break;
  case 4:
    calculator.calc = div;
    break;
  default:
    return NULL;
  }
  return &calculator;
}

void delCalc(void){
  calculator.input = NULL;
  calculator.calc = NULL;
}

・main.c

#include "MathClass.h"

int main(void){
  int type;
  int in1;
  int in2;
  int ans;
  CALCULATOR_T *calculator;

  printf("実行したい演算は?(足し算:1,引き算:2,掛け算:3,割り算:4)");
  scanf("%d", &type);
  if(type > 4 || type < 1){
    printf("1から4を入力してください\n");
    return -1;
  }

  calculator = newCalc(type);

  printf("2つの数字を入力してください\n");
  printf("まず一つ目:");
  scanf("%d", &in1);
  printf("次に二つ目:");
  scanf("%d", &in2);

  calculator->input(in1, in2);
  ans = calculator->calc();

  printf("answer is %d\n", ans);

  delCalc();

  return 0;
}

構造体の calc と input をクラスのメソッドのようにして扱っています。また newCalc 関数をコンストラクタ、delCalc 関数をデストラクタ的に扱っています。calcのところはもう少し上手く作れば演算ごとのサブクラス的な使い方もできそうですね。

まとめ

C言語では変数だけでなく関数もポインタで指すことが可能です。これは関数もメモリ上に展開されるためです。

関数ポインタは定義の仕方が独特で覚えにくいのでその点は注意してください。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です