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*);

下記のような関数を指させることができます。

funcPtrが指すことのできる関数
int func_a(int, char, int*);
int func_b(int, char, int*);
int func_c(int, char, int*);

一応下記のように戻り値や引数の型が異なる関数も指させることはできますが、動作が保証されませんので注意しましょう。

funcPtrが指すことのできる関数
void func_d(char, int*);
int func_e(void);
int func_f(int);

例えば func_d を関数ポインタに指させたいのであれば、func_d と戻り値や引数の型を合わせた関数ポインタを変数宣言しましょう。

func_dを指す関数ポインタの宣言
void (*funcPtr)(char, int*);

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

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

関数ポインタからの関数実行の例
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への関数のアドレスセット
func = add;

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

funcがadd関数を指す様子

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

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 関数と呼ばれる関数を実行している箇所になります。

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;
}

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

まとめ

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

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

オススメの参考書(PR)

C言語一通り勉強したけど「ポインタがよく分からない」「ポインタの理解があやふや」「もっとC言語の理解を深めたい」という方には、下記の「C言語ポインタ完全制覇」がオススメです!

この本の主な内容は下記の通りで、通常の参考書では50ページくらいで解説するポインタを、この本では約 "360ページ" 使って幅広く・深く解説しています。

  • C言語でのメモリの使い方
  • 配列とポインタの関係性
  • ポインタのよくある使い方
  • ポインタの効果的な使い方

一通りC言語を学んだだけだと "理解があやふやになってしまいがち" "疑問に思いがち" な内容に対する明確な解説が多いため、特にポインタやC言語の理解があやふやという方にはオススメの本です。

また、C言語においてポインタはまさに "肝" となる機能ですので、ポインタについてより深く学ぶことでC言語全体の理解を深めることにもつながります。

ポインタ・C言語についてより深く理解するための本としては現状1番のオススメの本です。

ただし、他の入門書等で "一通りC言語を学んでいる" 方向けの解説になっているので、"C言語を始めるにあたっての最初の入門書" として利用すると難易度が高いので注意してください。

入門用のオススメ参考書は下記ページで紹介していますので、こちらも是非参考にしていただければと思います。

https://daeudaeu.com/c_reference_book/

同じカテゴリのページ一覧を表示