C言語

【C言語 関数】関数ポインタとは?【発展編】

c-language-function-pointer.png

<この記事について>

  • この記事を読むことで、関数ポインタがどんなものなのかと使い方について理解することができます。
  • 結論としては、関数ポインタは関数のアドレスを参照して関数を実行することができます。主にロールバックや一部の標準関数の引数に使われます。

こんにちは!こんばんは!

ちゃとらです(・ω・)/

関数ポインタって聞いたことがあるでしょうか?

その名の通り関数のアドレスを代入できるポインタ変数であり、変数ポインタと同じような使い方です。

ちゃとら

関数ポインタについて分かりやすく解説していきます!

関数ポインタとは

説明

関数ポインタとは、ポインタ変数の一種であり、関数が保存されているアドレスを格納することができます。

関数のアドレスを参照することで、関数を呼び出すことが可能です。

しかし、関数ポインタの宣言方法は少し独特です。

なので、サンプルコードを基に具体的な解説をしていきます。

サンプルコード

まずは、関数ポインタを使って関数(showString)を動かすプログラムです。

#include <stdio.h>

void showString(void)
{
	printf("ABCDEFG\n");
}

int main(void)
{
	void(*fp)(void);  // 関数ポインタfpの宣言
	fp = showString;  // fpにshowString関数のアドレスを代入

	fp();  // 参照先の関数を実行

	return 0;
}
実行結果

ABCDEFG

10行目、11行目で関数ポインタを宣言しshowString関数のアドレスを代入しています。

ここでの注目点は関数ポインタの宣言方法についてです。

// 関数ポインタの宣言方法
戻り値のデータ型 (*変数名)(引数のデータ型...)

関数ポインタを宣言するには、戻り値・引数のデータ型を設定しなければなりません。また、関数ポインタには宣言した戻り値・引数のデータ型が合致する関数のアドレスしか入れることができません。

13行目で関数ポインタfpに格納されている関数のアドレスを参照してshowString関数を動かしています。

イメージは以下のようになります。

関数ポインタの参照先の関数を実行

関数ポインタを使うことで次のようなこともできます。

#include <stdio.h>

void showString(void)
{
	printf("ABCDEFG\n");
}

int add(int a, int b)
{
	return a + b;
}

int mul(int a, int b)
{
	return a * b;
}

int main(void)
{
	void(*fp1)(void) = showString;  // fp1にshowString関数のアドレスを代入
	int(*fp2)(int, int) = add;		// fp2にadd関数のアドレスを代入
	int n1 = 100, n2 = 200;
	int ans;

	fp1();              // showString関数を実行
	ans = fp2(n1, n2);  // add関数を実行し結果をansに代入
	printf("%dと%dの和:%d\n", n1, n2, ans);

	fp2 = mul;          // fp2にmul関数のアドレスを代入
	ans = fp2(n1, n2);  // mul関数を実行し結果をansに代入
	printf("%dと%dの積:%d\n", n1, n2, ans);
	
	return 0;
}
実行結果

ABCDEFG
100と200の和:300
100と200の積:20000

ここで注目してほしいのは、21行目でfp2にadd関数のアドレスを代入し26行目で実行していますが、29行目と30行目でfp2にmul関数のアドレスを代入し実行していることです。

このように、関数ポインタの戻り値・引数のデータ型が関数と合致するならば、他の関数のアドレスも代入可能となります。

関数ポインタの使い道

とあるモブ

まぁ、何となく関数ポインタがどんなものなのか分かったけど、

どんな時に使うの?

ちゃとら

主に、コールバックや一部の標準関数の呼び出しですね!

関数ポインタの用途は様々ですが、代表的な使い方を2つ紹介していきます!

コールバック

コールバックとは、電話のかけ直しが言葉の由来となっていて、関数の引数に関数ポインタを設定することでその関数内で他の関数を実行することが可能となります。

つまり、自作関数内で他の自作関数を実行させることができます。

以下のプログラムで、実際にコールバックさせてみます。

#include <stdio.h>

void showString(void)
{
	printf("showStringの実行開始\n");
	printf("ABCDEFG\n");
	printf("showStringの実行終了\n");
}

void func(void(*fp)(void))
{
	printf("funcの実行開始\n");
	fp();  // 引数で受け取った関数アドレスを参照して関数を実行
	printf("funcの実行終了\n");
}

int main(void)
{
	func(showString);  // showString関数のアドレスを渡してfunc関数を実行

	return 0;
}
実行結果

funcの実行開始
showStringの実行開始
ABCDEFG
showStringの実行終了
funcの実行終了

19行目でfunc関数にshowString関数のアドレスを渡して実行しています。

なので、func関数の始まりを実行→showString関数の実行→func関数の終わりを実行という流れの処理となっています。

このソースコードでは単純な使い方のコールバックですが、もちろん応用した使い方もできます。

コールバックを使用することによって、状況に応じて関数内で動かす他の関数を変えたり、多くの関数を動かすことができます。

ちゃとら

意外と便利かも!

一部の標準関数の呼び出し

C言語に用意されている標準関数の中には、引数に関数ポインタ(関数アドレス)を渡して呼び出すことのできるものがいくつかあります。

代表的なものだと、atexit関数やsignal関数などです。

以下のプログラムでは、atexit関数を使って関数を登録し、main関数の処理が終わったら最後に登録された関数から順番に呼び出されます。

#include <stdio.h>
#include <stdlib.h>

void showString1(void)
{
	printf("ABCDEFG\n");
}

void showString2(void)
{
	printf("HTJKLMN\n");
}

int main(void)
{
	void(*fp)(void);
	int result;

	fp = showString2;
	result = atexit(fp);  // showString2関数を登録して結果をresultに代入
	// 関数の登録判定
	if (result == 0)
	{
		printf("関数の登録に成功しました\n");
	}
	else
	{
		printf("関数の登録に失敗しました\n");
	}

	fp = showString1;
	result = atexit(fp);  // showString1関数を登録して結果をresultに代入
	// 関数の登録判定
	if (result == 0)
	{
		printf("関数の登録に成功しました\n");
	}
	else
	{
		printf("関数の登録に失敗しました\n");
	}

	return 0;
}

// main関数の処理が終了したら
// showString2 → showString1の順番で関数が実行される 
実行結果

関数の登録に成功しました
関数の登録に成功しました
ABCDEFG
HTJKLMN

atexit関数の説明は以下の通りです。

// atexit関数の形式
int atexit(void(*func)(void));
atexit関数の解説
  • 引数として渡された関数ポインタの中身の関数を登録し、登録成功ならば1を返し、登録失敗ならば0を返す。
  • main関数の終了やexit関数が呼び出されてプログラムが終了したら、最後に登録された関数から順番に呼び出される。
  • コンパイラによって登録できる関数の制限が変わるが、最低でも32個の関数を登録することが可能。

atexit関数は最後に登録された関数から順番に関数を呼び出します。

なので、main関数の処理が終了したらshowString2→showString1の順番で関数が呼び出されています。

注意点としてatexit関数の引数には、戻り値・引数が共にvoid型の関数アドレスしか入らないことです。

まとめ

関数ポインタについて

  1. 関数ポインタとは、関数のアドレスが格納可能なポインタ変数の一種。格納されている関数のアドレス参照して関数を動かすことが可能。
  2. 宣言方法 → 戻り値のデータ型(*変数名)(引数のデータ型…)
    しかし、戻り値・引数のデータ型に合致した関数のアドレスしか入らない。
  3. 主に、コールバックや一部の標準関数の引数に使用される。
ちゃとら

変数のアドレスを代入して参照することができれば、

同じように関数のアドレスを代入して参照することができます!

今回はここまでです。

ちゃとら(・ω・)/