下の記事で主に変数のアドレスを指すポインタについて説明しました。
【C言語】ポインタを初心者向けに分かりやすく解説関数も変数同様にメモリ上に配置されるためアドレスを持っており、ポインタで指すことができます。関数を指すポインタは関数ポインタと呼ばれます。
ここでは関数ポインタの考え方と簡単な使い方を紹介していきたいと思います。
Contents
関数ポインタ
まずは関数ポインタがどのようなものであるかを解説していきます。
関数ポインタとは
関数ポインタとはその名の通り、関数を指すポインタです。
関数も変数等と同様にプログラム実行時にメモリ上に展開され、メモリ上に存在することになります。
ポインタとはメモリ上のアドレスを指すものであり、メモリ上に存在する関数も、変数同様にポインタで指すことができます。
この関数を指すポインタが関数ポインタです。
スポンサーリンク
関数ポインタの宣言
関数ポインタも他の変数同様に変数宣言を行います。関数ポインタの変数宣言は下記のように行います。括弧の位置が特殊ですので注意してください。
戻り値の型 (*変数名)(引数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 func_a(int, char, int*);
int func_b(int, char, int*);
int func_c(int, char, int*);
一応下記のように戻り値や引数の型が異なる関数も指させることはできますが、動作が保証されませんので注意しましょう。
void func_d(char, int*);
int func_e(void);
int func_f(int);
例えば 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 = add;
これにより関数ポインタ func
は add
を指すことになります。
関数ポインタの配列である funcarray
には下記でそれぞれの関数のアドレスを格納しています。
funcarray[0] = add;
funcarray[1] = sub;
funcarray[2] = mul;
funcarray[3] = div;
これにより各関数を 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言語でも構造体と関数ポインタを用いれば構造体をクラスのような使い方ができるようになり、なんちゃってオブジェクト指向的なプログラムも書けるようになります。
#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);
#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;
}
#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言語では変数だけでなく関数もポインタで指すことが可能です。これは関数もメモリ上に展開されるためです。
関数ポインタは定義の仕方が独特で覚えにくいのでその点は注意してください。
オススメの参考書(PR)
C言語一通り勉強したけど「ポインタがよく分からない」「ポインタの理解があやふや」「もっとC言語の理解を深めたい」という方には、下記の「C言語ポインタ完全制覇」がオススメです!
この本の主な内容は下記の通りで、通常の参考書では50ページくらいで解説するポインタを、この本では約 "360ページ" 使って幅広く・深く解説しています。
- C言語でのメモリの使い方
- 配列とポインタの関係性
- ポインタのよくある使い方
- ポインタの効果的な使い方
一通りC言語を学んだだけだと "理解があやふやになってしまいがち" "疑問に思いがち" な内容に対する明確な解説が多いため、特にポインタやC言語の理解があやふやという方にはオススメの本です。
また、C言語においてポインタはまさに "肝" となる機能ですので、ポインタについてより深く学ぶことでC言語全体の理解を深めることにもつながります。
ポインタ・C言語についてより深く理解するための本としては現状1番のオススメの本です。
ただし、他の入門書等で "一通りC言語を学んでいる" 方向けの解説になっているので、"C言語を始めるにあたっての最初の入門書" として利用すると難易度が高いので注意してください。
入門用のオススメ参考書は下記ページで紹介していますので、こちらも是非参考にしていただければと思います。
https://daeudaeu.com/c_reference_book/