関数ポインタの基本

概要

最終更新日:2020/02/28

関数ポインタの基本的な内容、使い方について書いた記事です。
この記事は以下の内容を知りたい方に向けて書いています。
  • 関数ポインタとは何か知りたい
  • 関数ポインタの使い方を知りたい
  • 関数ポインタのメリットを知りたい
  • コールバック関数とは何か知りたい



関数ポインタとは

関数ポインタとは関数のアドレスを保存できるポインタ型の事で、
この型を使用した変数は関数のアドレスを保存することができます。
関数のアドレスが保存された変数は通常関数のように実行することができるので、
その変数に様々な関数を保存すれば一つの変数でそれらの関数を実行できます。

関数のアドレス

関数もアドレスを持っているということの説明をします。
まず、変数の復習になりますが、メモリ領域に保存場所を確保して
そこに値を保存しています。
このメモリ領域には変数の保存場所とは別に関数情報の保存場所も存在します。
これらの保存場所は厳重に管理されているので、間違って変数の値が関数情報の場所に
関数情報が変数の場所に保存されることはありません。

c_0053

上の図のように赤枠のメモリ領域の中に変数や関数のメモリ領域が存在します。

関数のアドレス表示

関数がアドレスを持っていることを証明するために
printfを使用して出力していたいと思います。

#include <stdio.h>

int main(void)
{
	printf("main = %p\n", main);

	return 0;
}

実行結果:
	main関数のアドレスが表示される

上のコードのように「%p」や「%d」で関数を指定すれば
その関数情報が保存されている場所のアドレスを出力することができます。

宣言

関数ポインタも型の一つなので「int*」や「float*」のような型の書式があります。

// 書式例
戻り値の型 (*)(引数情報)

// 具体例
// 戻り値なし、引数なしの関数型の変数pfunc
void (* pfunc)();

書式例、具体例から関数ポインタはかなり特殊な宣言方法ということが分かります。
宣言の書式は本来の関数宣言の関数名の部分をポインタ型である「*」にして
「 () 」で括りますが、この*を括る「 () 」を忘れないでください。
そして、変数名の部分は通常のポインタ型と同様に*のすぐ後ろに置きます。
慣れるまでは一旦型を書いて、その後で*の後ろに名前を書けばいいと思います。

// 慣れるまでの流れ
// 戻り値なし、引数なしの関数型で変数をpfuncを宣言する
// ①.書式通りに型を書く
void (*)();

// ②.*の後ろに名前pfuncをつける
void (* pfunc)();

// 使用例
void PrintHelloWorld(void)
{
	printf("HelloWorld\n");
}

int main(void)
{
	// 関数ポインタを宣言
	void (* pfunc)();

	pfunc = PrintHelloWorld;
	printf("PrintHelloWorldのアドレス = %p", PrintHelloWorld);
	printf("pfuncのアドレス = %p", *pfunc);
	pfunc();

	return 0;
}

実行結果:
	PrintHelloWorldとpfuncのアドレスは同じ値が出力される

コードの結果から、関数ポインタ変数に関数のアドレスが
きちんと保存されていることがわかります。

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

関数ポインタは保存されている関数のアドレスを使用して間接的に実行できます。

// 書式例
関数ポインタ変数名(引数情報);

古いC言語の仕様では「(*関数ポインタ変数名)(引数情報)」となっていましたが、
現在では()と*は不要となっています。

// 具体例
#include <stdio.h>

void PrintNum(int num)
{
	printf("num = %d\n", num);
}

int main(void)
{
	void (* pfunc)(int);

	pfunc = PrintNum;

	(* pfunc)(1);	// 旧仕様の書式
	pfunc(2);	// 新仕様の書式

	return 0;
}

実行結果:
	num = 1
	num = 2

pfuncにPrintNum関数のアドレスを保存して、関数として実行しています。
その結果からきちんとPrintNumが実行されていることが分かります。

関数ポインタのメリット

関数ポインタのメリットは直接関数名を書いて呼び出さなければいけなかった所で
間接的に呼び出すことが可能になるという点です。
これによって関数を呼び出す側のコードがシンプルになり可読性が上がります#include <stdio.h>
#include <memory.h>

enum
{
	Init,	// 初期化
	Run,	// 実行
	Delete,	// 終了
	End,
};

typedef struct
{
	int Step;
	int Counter;
} Scene;

typedef void (*GAME_FUNC)(Scene*);

void InitScene(Scene* game_info)
{
	// 初期化処理
	game_info->Step = Run;
	printf("初期化\n");
}

void RunScene(Scene* game_info)
{
	// 実行処理
	game_info->Counter++;

	if (game_info->Counter > 30)
	{
		game_info->Step = Delete;
	}
	printf("実行\n");
}

void DeleteScene(Scene* game_info)
{
	// 終了処理
	game_info->Step = End;
	printf("終了\n");
}

int main(void)
{
	//void (* pfunc[])(Scene*) = {
	GAME_FUNC pfunc[] = {
		InitScene,
		RunScene,
		DeleteScene,
		NULL
	};

	Scene gi = {
		Init,
		0,
	};

	while (pfunc[gi.Step] != NULL)
	{
		pfunc[gi.Step](&gi);
	}

	return 0;
}

実行結果:
	初期化
	実行 * 30
	終了

上の例では、関数ポインタの配列にInitScene、RunScene、DeleteSceneの
三つの関数を保存しています。
本来はこの三つの関数を使い分けるにはifやswitchで関数の指定が必要がありますが、
関数ポインタの配列を使用した方法では、whileとStepの値で関数の切り替えを行うので、
ifやswitchを使用した場合よりも呼び出し側の構造がシンプルになります。

コールバック関数

コールバック関数とは関数の引数に渡す関数のことです。
コールバック関数は、実行側の関数で使用する関数を使い分けたい時や
実行側で何らかのイベントが発生した際にそのイベントに対応する処理を
指定する目的で使用されます。

#include <stdio.h>

// 敵構造体
typedef struct
{
	int AiNo;	// AI番号
} Enemy;

// ゲージ構造体
typedef struct
{
	float Current;	// ゲージの現在の値
	float Min;		// 最小値
	float Max;		// 最大値
} Gauge;

// Player構造体
typedef struct
{
	int Hp;	// Hp
} Player;

// コールバックに使用する関数の保存用配列
void (*g_NoticeList[2])(int);

// Player、Gauge、Enemyのグローバル変数
Player g_Player;
Gauge g_Gauge;
Enemy g_Enemy;

// プレイヤーの更新関数(mainで実行)
void UpdatePlayer()
{
	static int timer = 0;
	timer++;
	printf("time = %d\n", timer);

	if (timer > 5 &&
		g_Player.Hp > 0)
	{
		timer = 0;
		g_Player.Hp -= 10;

		// 関数ポインタでHpが変化したことを通知する
		printf("\n###### 通知開始 #####\n");
		for (int i = 0; i < 2; i++)
		{
			g_NoticeList[i](g_Player.Hp);
		}
		printf("###### 通知終了 #####\n\n");
	}
}

// 敵更新関数(mainで実行)
void UpdateEnemy()
{
	printf("敵AIはNo.%dを実行中\n", g_Enemy.AiNo);
}

// 敵AI変更関数(コールバックで使われる関数)
void ChangeEnemyAi(int player_hp)
{
	int last_ai_no = g_Enemy.AiNo;

	if (player_hp >= 50)
	{
		g_Enemy.AiNo = 1;
	}
	else
	{
		g_Enemy.AiNo = 2;
	}

	if (last_ai_no != g_Enemy.AiNo)
	{
		printf("AIを%dから%dに変更しました\n", last_ai_no, g_Enemy.AiNo);
	}
}

// ゲージの現在値変更関数(コールバックで使われる関数)
void ChangeGaugeCurrentPoint(int player_hp)
{
	float length = g_Gauge.Max - g_Gauge.Min;
	g_Gauge.Current = player_hp;
	printf("ゲージ更新 Hpはあと%.2f%%\n", g_Gauge.Current / length * 100.0f);
}

int main(void)
{
	// 各変数の初期化
	int player_max_hp = 100;
	g_Player.Hp = player_max_hp;
	g_Gauge.Current = g_Gauge.Min = 0.0f;
	g_Gauge.Max = (float)player_max_hp;
	g_Enemy.AiNo = 0;

	// コールバック関数として関数を保存
	g_NoticeList[0] = ChangeEnemyAi;
	g_NoticeList[1] = ChangeGaugeCurrentPoint;

	while (g_Player.Hp > 0)
	{
		UpdatePlayer();
		UpdateEnemy();
	}

	return 0;
}

このコードではPlayerのHpに変化が起こった際にコールバックを使用して
EnemyとGaugeに対して更新を通知しています。
コールバックを使用すれば登録されている関数がEmenyでもGaugeでも関係なく
for文などで一気に実行できるので、新しい通知先が必要になったとしても、
配列に新しい関数を追加するだけで良いので、実行側に変化を加えなくてすみます。