こんにちは、ナナです。
ポインタ最終章として、最後に特殊なポインタを紹介しましょう。それが「関数ポインタ」です。「関数」と「ポインタ」、この2つが如何に関連するのか。
その謎を解き明かしていきます。
本記事では次の疑問点を解消する内容となっています。
では、関数ポインタを学んでいきましょう。
ポインタの全貌を学びたい方は『C言語 ポインタを使いこなせ【身に付けるための9の極意】』の記事から順に読むことをお勧めします。
関数ポインタの意味と定義方法
わかってらっしゃるじゃないの。ポインタ機能に関する最後のトリは、わたくしが飾りますわ!
さぁ、「関数ポインタ」とやらが何者なのかをお教えなさいっ!
じゃあ、「関数ポインタ」の役割と定義方法から順に解説するよ。
「関数ポインタ」もポインタ変数の一種です。
本格的なソフトウェアシステムを構築すると、どこかしらでこの「関数ポインタ」が登場します。関数ポインタが何者なのかを学びましょう。
関数ポインタとは「関数を指し示すポインタ」のこと
ここまで解説してきた「ポインタ変数」とは、「変数」を参照しているポインタでした。
それに対して「関数ポインタ」とは、
「関数」を参照している「ポインタ」
のことです。
皆さんはここまでプログラムの中で、変数をたくさん定義してきたことでしょう。その変数とはメモリ上に実体が存在しているんでしたね。
ここで皆さんに質問です!
皆さんが定義した「関数」とは、プログラムが動く時に、いったいどこに存在するのでしょうか?
おわかりですね、答えは「メモリ」です。
関数も変数も、プログラムが動作するときには、「メモリ」に存在しているのです。
つまり、「関数ポインタ」とは
メモリ上に存在する「関数を参照しているポインタ変数」
なのです。
ポインタ変数が「関数」を見ている、それが「関数ポインタ」です。
関数ポインタ変数の定義方法を知ろう
関数ポインタも変数であるため、使用するためには変数定義が必要となります。
しかし、関数ポインタの定義は独特でややこしい書き方が必要となります。定義例を示します。
// 関数ポインタの変数定義
long (* pfunc)(int , char);
この関数ポインタの変数定義は一例でしかありませんが、皆さん何が何を示しているかわかりますか?
C言語の中で、もっとも複雑な変数定義を求められるのが「関数ポインタ」です。この書き方はかなり特殊なため、最初は皆さん戸惑いますよ。
関数ポインタ変数の定義の解釈とは
関数ポインタ変数の定義を分解して、それぞれが何を示しているかを考察してみましょう。
各部品の役割は次のものとなります。
部品①
変数につけるラベル名を示す。皆さんが自由に名前を与えることができる。
部品②
部品①に対してのデータ型を示す。関数ポインタは「ポインタ型」のため「*」を指定する。
部品③
関数ポインタが参照する先の関数が定義する「戻り値のデータ型」を示す。
部品④
関数ポインタが参照する先の関数が定義する「引数のデータ型」を示す。
「ポインタ変数」と「関数ポインタ変数」の違いは部品③④です。
関数ポインタの参照先は「関数」ですから、参照先の関数ならではの情報を③④で表現しています。
その情報が、関数の「戻り値」と「引数」のデータ型ということなんです。
関数ポインタの変数定義には、参照させる先の関数が定義する「戻り値」と「引数」のデータ型を関連付ける必要があります!
関数ポインタを使うと何ができるのか
「関数ポインタ」の変数定義はわかりましたわ。次は、わたくしへのメリットをお教えなさいっ!「関数ポインタ」を使うと何ができますの?
「関数ポインタ」は今までのポインタ変数とは役割が根本的に違うんだよ。じゃあ、何ができるのかを学んでいこうね。
「ポインタ変数」は参照先のメモリに対し、数値を読み書きすることができました。では、関数ポインタができることとは、いったい何なのでしょう?
実は関数ポインタの存在意義とは、参照先のメモリを読み書きすることが目的ではありません。
「関数ポインタ」を使うと、
ポインタが参照している関数を呼び出すことができる
のです。
関数ポインタを使った関数呼び出しのサンプルプログラム
次のプログラムはfunc関数を通常の呼び出し方と、関数ポインタから呼び出した2つの方法を示したものです。
#include <stdio.h>
// 2つの引数を足し算し画面に表示する関数
long func(long num1, long num2)
{
// 足し算した結果を画面に表示
printf("%d\n", num1 + num2);
return num1 + num2;
}
int main(void)
{
// 関数ポインタの変数定義
long (*pfunc)(long, long);
// ①:直接的な関数呼び出し
func(100, 200);
// 照準設定:pfunc --> func関数
pfunc = func;
// ②:pfuncから関数呼び出し
pfunc(100, 200);
return 0;
}
このプログラムの結果は次のものになります。
300
300
①と②の関数呼び出しにより、全く同じ実行結果が得られました。
このように関数ポインタを使うと、参照先の関数を呼び出すことができるのです。
関数ポインタを利用して、ポインタが参照している関数を呼び出すことができる!これが関数ポインタができることなんです。
関数ポインタを理解するための利用するイメージ
関数ポインタとは
「関数を変数に保存して、持ち運んだ先でいつでも利用できる」
そんなイメージが関数ポインタです。
この持ち運んで利用するというイメージに対して、
そもそも、関数を持ち運ぶ必要なんてあるの?
関数を直接呼べばいいじゃないの?なんでわざわざ関数ポインタなんてものから呼び出すの?なんで?
と思われる方もいるでしょう。
結局は
「なぜ、そんなことをする必要があるの?」
これが関数ポインタの解説の難しいところです。
代表的な関数ポインタの使いどころは「コールバック」と呼ばれる仕組みを作るときに必要になります。
コールバックに関しては、ソフトウェアシステム構築の知識がある程度必要となります。現時点で解説をしてもなかなかピンと来る段階ではないので、別の機会に解説いたします。
関数ポインタの型の厳密さに注意
関数ポインタの定義には、たくさんデータ型が登場しますわっ!どこに何を書けばよいのかしら?わかりやすく解説なさいっ!
関数ポインタの変数定義には、参照先の関数のデータ型を反映する必要があるよ。この組み合わせは無限にあるから使う際には注意が必要だよ。
関数ポインタは関数を指し示すポインタですが、型に関する制約が非常に強い特徴があります。
関数における型とは次のものです。
これら全ての情報がセットになって、1つの関数の型を示します。
引数の数が変われば別の型となり、戻り値の型が異なれば別の型となります。
つまり、関数の型とは
「戻り値」と「引数」の組み合わせ次第で、何通りもの組み合わせがある
ということです。
関数ポインタ変数を定義するときには、これら無数の型の組み合わせの中から1つだけを選択する必要があります。
#include <stdio.h>
// 型が関数ポインタと一致している関数
long funcMatch(int n1, char n2)
{
return 0;
}
// 型が関数ポインタと一致しない関数
long funcUnMatch(int n1, char n2, long n3)
{
return 0;
}
int main(void)
{
// 関数ポインタの変数定義
long(*pfunc)(int, char);
// 型が一致するため代入OK
pfunc = funcMatch;
// 型が一致しないためエラー/警告
pfunc = funcUnMatch;
return 0;
}
このように関数であれば、なんでも関数ポインタで参照できるわけではありません。
引数の数や型が1つ違うだけでも、関数ポインタへの代入処理でビルドエラーや警告が出力されます。
関数ポインタは、型に対して非常に厳しい変数であるということです。使用する際は「戻り値」と「引数」の構成をしっかりと合わせることに注意しましょう。
関数ポインタと参照先の関数の「戻り値」と「引数」のデータ型は完全一致が求められるんです。ひとつでも違いがあってはダメですよ。
関数ポインタから関数が呼べるその仕組みとは
「関数ポインタから関数を呼ぶことができる」ってことに違和感しか感じないわっ!この奇妙な現象を、あぁたはどう説明しますの?
超常現象でお茶を濁すわけじゃないわよねっ!
関数ポインタを学び始めたばかりの人は、「関数ポインタ」から関数を呼び出すということに、違和感を感じるかもしれないね。
でもね、これって実は理にかなってるんですよ。それを教えましょう。
関数ポインタから関数を呼び出すことができます。この超常現象の仕組みを解明しましょう。
関数名とメモリ番地の関係とは
ここまでのプログラムをよく見ている人は気づいたでしょう。次のように関数ポインタに番地を設定する際には「関数名」が右辺として指定されています。
// 関数ポインタの変数定義
long (*pfunc)(long, long);
// 照準設定:pfunc --> func関数
pfunc = func;
関数ポインタであるpfunc変数は、「ポインタ型」なので番地を入れるための変数です。
つまり、この代入が成立するということは、
「関数名」は関数の番地を示している
ということになります。
配列名が配列の先頭番地を示しているのと似ていますね。
関数名を単体で記述した場合は「関数のメモリ番地」を示しています。このルールはしっかりと覚えておきましょう。
なぜ、関数ポインタは関数を呼び出せるのか?
関数ポインタを利用すると、なぜ関数が呼び出せるのでしょうか?
次のように関数ポインタpfuncに()を付けることで、確かに関数が呼び出せています。
// 照準設定:pfunc --> func関数
pfunc = func;
// 関数ポインタpfuncからfunc関数の呼び出し
pfunc(100, 200);
しかし、関数ポインタに()を付けると関数が呼び出せるってどういうことなんでしょう。
そもそも、関数を呼び出すときは、関数名 +()の形式で呼び出すことができましたね。
必要に応じて引数も指定する必要がありますが、基本的な呼び出し方は関数名に()を付けることです。
// 直接的な関数呼び出し
func(100, 200);
実は、「関数名に()を付けると呼び出せる」というのは正確な表現ではありません。
実際は、「関数の番地に()を付けると呼び出すことができる」のです。
関数名を使った関数呼び出し
関数ポインタを使った関数呼び出し
関数名であろうと関数ポインタであろうと、()の前は「関数の番地」なのです。
これが関数ポインタから関数が呼び出すことができる理由なんです。
関数のメモリ番地に()を付けることで、関数は呼び出されている!
これが原則です。関数名でも関数ポインタでもこの原則に従っているのです。
関数ポインタの変数定義を簡単にする方法
関数ポインタの変数定義はわかりづらいわねっ!なんなの、この奇妙な書き方わ、信じられませんわっ。あぁた、なんとかなさい‼
関数ポインタの変数って定義しづらいよね。それはね、みんな感じているんですよ。こんな時には「あれ」を使おうね、前に出てきた「あれ」だよ。
関数ポインタの変数定義は本当にわかりづらいんです。ぱっと見で、どれが変数名なのか見分けがつきづらいですね。
// 関数ポインタの変数定義例
char (*pfunc1)(void);
long (*pfunc2)(long, long);
「使いづらい型を使いやすくする」といえば、皆さんお判りでしょうか?
複雑な型名に、あだ名をつける機能がありましたね!答えは続きをご覧ください。
関数ポインタの変数が登場する複雑なシーン
次のプログラムは処理内容に意味はありませんが、動作させることは可能なものです。
#include <stdio.h>
// 関数ポインタを引数に持つ関数定義
void sub(long(* pfunc)(int, short), int num)
{
return;
}
int main(void)
{
// 関数ポインタ
long (*pfunc)(int, short) = NULL;
// 関数ポインタを引数で関数に渡す
sub(pfunc, 100);
return 0;
}
皆さん、このsub関数の引数のわかりづらさは尋常ではありません。いったい何が関数名で何が引数なのかさっぱりわかりません。
void sub(long(* pfunc)(int, short), int num)
このような関数ポインタの定義を見ると、「もっと簡単に書ければよいのに・・・」と思うのです。
関数ポインタの変数定義はわかりづらい、そう感じることは正常な感覚です。
関数ポインタの型は「typedef」で定義せよ
構造体でよくお世話になる「typedef」ですが、実は関数ポインタとも関りが深いのです。
「もっと簡単に関数ポインタの変数を作りたい」、その願いを叶えてくれるのが「typedef」です。
typedefの役割は既存の型に新しい型名を名付けることができる機能でした。
このややこしい関数ポインタの型を別の名前に変えてしまえばよいのです。
// 関数ポインタ型を別名でtypedefする
typedef long(* FP_FUNC)(int, short);
このようにFP_FUNCという型名で、新しく関数ポインタ型を作ることができます。
型名は皆さんが自由に決めることができます。このように定義することで先ほどのプログラムがどのように変貌するか見ていただきましょう。
#include <stdio.h>
// 関数ポインタ型を別名でtypedefする
typedef long(* FP_FUNC)(int, short);
void sub(FP_FUNC pfunc, int num)
{
return;
}
int main(void)
{
FP_FUNC pfunc = NULL;
sub(pfunc, 100);
return 0;
}
皆さん如何でしょう、ものすごく読みやすくなったと感じませんか?
// typedef前
void sub(long(* pfunc)(int, short), int num)
// typedef後
void sub(FP_FUNC pfunc, int num)
いやらしい関数ポインタの定義が、普段の変数定義のように行うことができるようになりました。sub関数の引数定義も、さっぱりしているのがわかりますね。
このように関数ポインタはtypedefを使って、新しいデータ型を名付けてあげるのが一般的です。
「typedef」機能に関して詳しく知りたい方は『C言語 typedefを使った型定義【活用場面のランキング発表】』の記事を読むとよいでしょう。
関数ポインタを使いたいと思ったら、関数ポインタのデータ型をtypdefで定義しましょう。プログラムの可読性がぐっと向上します。
課題:関数ポインタを学べたかを確認しよう
もしも、プログラムが上手く動かなくて困ったときは、答えを見るのではなく「デバッガ」の使い方を学びましょう。
この記事を見ると問題の解決技術が身に付きます。困ったときのオススメ記事です!
課題1
課題内容
次のプログラムのhello関数を関数ポインタを使用して呼び出せ。関数ポインタはtypedefによる型定義を行ってから使用すること。
main.c
#include <stdio.h>
void hello(void)
{
printf("Hello\n");
}
int main(void)
{
// 関数ポインタ変数を定義
// 関数ポインタに関数を設定
// 関数ポインタから関数を呼び出す
return 0;
}
出力期待結果
Hello
main.c
#include <stdio.h>
// 関数ポインタ定義
typedef void (* FP_HELLO)(void);
void hello(void)
{
printf("Hello\n");
}
int main(void)
{
// 関数ポインタ変数を定義
FP_HELLO pFunc = NULL;
// 関数ポインタに関数を設定
pFunc = hello;
// 関数ポインタから関数を呼び出す
pFunc();
return 0;
}
関数ポインタのtypedefの型は、呼び出したい関数の戻り値と引数のデータ型と合わせる必要がありますよ。
関数ポインタの変数定義・番地の設定方法・関数の呼び出し方、これらの基礎をしっかりと身に付けましょう。
課題2
課題内容
次のプログラムは四則演算を関数にしたものである。
main.c
#include <stdio.h>
void addition(long num1, long num2)
{
printf("%d + %d => %d\n", num1, num2, num1 + num2);
return;
}
void subtraction(long num1, long num2)
{
printf("%d - %d => %d\n", num1, num2, num1 - num2);
return;
}
void multiplication(long num1, long num2)
{
printf("%d * %d => %d\n", num1, num2, num1 * num2);
return;
}
void division(long num1, long num2)
{
printf("%d / %d => %d\n", num1, num2, num1 / num2);
return;
}
int main(void)
{
// 関数ポインタの配列
FP_CALCULATION pfunc[] = {
addition, // 加算
subtraction, // 減算
multiplication, // 乗算
division, // 徐算
};
int i;
for (i = 0; i < sizeof(pfunc) / sizeof(pfunc[0]); i++)
{
// 関数ポインタの配列から四則演算を実施
// 第1引数は150、第2引数は30とする
}
return 0;
}
このプログラムにtypedefを使用し関数ポインタ型のFP_CALCULATIONを定義せよ。また、関数ポインタの配列を利用し四則演算の各関数を実行するようにせよ。
出力期待結果
150 + 30 => 180
150 - 30 => 120
150 * 30 => 4500
150 / 30 => 5
main.c
#include <stdio.h>
// 関数ポインタの定義
typedef void (* FP_CALCULATION)(long, long);
void addition(long num1, long num2)
{
printf("%d + %d => %d\n", num1, num2, num1 + num2);
return;
}
void subtraction(long num1, long num2)
{
printf("%d - %d => %d\n", num1, num2, num1 - num2);
return;
}
void multiplication(long num1, long num2)
{
printf("%d * %d => %d\n", num1, num2, num1 * num2);
return;
}
void division(long num1, long num2)
{
printf("%d / %d => %d\n", num1, num2, num1 / num2);
return;
}
int main(void)
{
// 関数ポインタの配列
FP_CALCULATION pfunc[] = {
addition, // 加算
subtraction, // 減算
multiplication, // 乗算
division, // 徐算
};
int i;
for (i = 0; i < sizeof(pfunc) / sizeof(pfunc[0]); i++)
{
// 関数ポインタの配列から四則演算を実施
// 第1引数は150、第2引数は30とする
pfunc[i](150, 30);
}
return 0;
}
戻り値の型と引数の型の構成が同一であれば、関数ポインタに番地を設定することができます。
配列と関数ポインタを組み合わせることで、統一的な関数呼び出しを可能にしていますね。たくさんの武器を組み合わせることで、より強力な武器を作り出すことができます。
こういったテクニックも知っておくとよいでしょう。