【C言語】関数へのポインタとコールバック関数の違いと使い方について

C言語/C++

C言語を使っていると何が違うのか?と考えることがあります。
タイトルにも書いてある以下の2つです。

・関数(への)ポイント
・コールバック関数

この記事では、上記2つについて使い方とメモ書きです。
関数へのポインタはC言語を利用している人以外はあまり聞き馴染みのない言葉かもしれません。
もう一つのコールバック関数については聞いたことある、知っていると言う方も多いはずです。
ですが実際にいつ使うのか?どのように使うのか?となる人も多いハズです。
以下で順を追って確認していきます。

動作確認を行うための環境は、ubuntuのgccを使用しています。

関数へのポインタ

まずは、関数へのポインタについてです。
これについてはC言語を使う人以外はあなり聞き馴染みがないかと思います。

補足

関数へのポインタ・・・関数ポインタとも呼ばれたりします。本記事では同意義とします。

たとえば、以下のような関数があるとします。

int func (int num);  

上記関数がある時、関数funcへのポインタを格納するポインタ変数は以下のように宣言できます。

int (*func_p) (int);

これが関数へのポインタです。
非常にわりかりにくい構文ですよね。と文句を言いたくなりますがとりあえず関数へのポインタを使用したサンプルです。

#include <stdio.h>

/*引数に1を足して返却する関数*/
int func_add(int number) {
    return number + 1; 
}
/*引数に1を引いて返却する関数*/
int func_sub(int number) {
    return number - 1;
}

int main() {
    int (*func_p) (int);
    
    //func_addをポインタ変数にセット、実行
    func_p = func_add;
    int numA = func_p(2);
    printf("func_add : %d\n", numA);

    //func_subをポインタ変数にセット、実行
    func_p = func_sub;
    int numB = func_p(2);
    printf("func_sub : %d\n", numB);
}

コールバック関数

コールバック関数とは、関数と呼び出す時に引数として渡される関数のことをいいます。

コールバック関数とは - IT用語辞典
コールバック関数とは、コンピュータプログラム中で、ある関数を呼び出す際に引数などとして引き渡される別の関数のこと。呼び出し側の用意した関数を、呼び出し先のコードが「呼び出し返す」(callback)ように実行される。プログラムにおける関数は、別のコードから呼び出して実行することができるコードのまとまりで、呼び出し側は処...
#include <stdio.h>
#include <string.h>

/*引数に1を足して返却するコールバック関数*/
int cb_add(int number) {
    return number + 1; 
}
/*引数に1を引いて返却するコールバック関数*/
int cb_sub(int number) {
    return number - 1;
}

//コールバック関数を呼び出す関数
void func_cb(int num, int (*cb)(int)) {
    printf("result:%d\n", cb(num));
}
int main() {
    int (*func_p) (int);
    char* s = "sub";
    
    func_p = NULL;
    if (strcmp("add", s) == 0) {
        func_p = cb_add;
        func_cb(5, func_p);
    }
    else if (strcmp("sub", s) == 0) {
        func_p = cb_sub;
        func_cb(5, func_p);
    }
    else {
        printf("unknown command\n");
    }
}

関数へのポインタとコールバック関数の違いは?

これについては私的には大きな違いはなくほぼ同意義として使われていると思います。
※あくまで私的な意見です。
私のこれまでの経験から明確に2つの言葉が使い分けられている印象はありません。


どうしても分けたいと考えるのであれば、
C言語の場合には関数のポインタをどのように使うかによって「関数へのポインタ」なのか「コールバック関数」と呼ぶのか分類できるのではないかと考えます。

関数へのポインタ

ポインタ変数に関数のアドレスがセットされて以下のように使用されている場合には、関数へのポインタと言う。

int (*func_p) (int);
//func_addをポインタ変数にセット、実行
func_p = func_add;
int numA = func_p(2);
コールバック関数

関数へのポインタがラップされて使われているようなイメージ。
関数へのポインタがある関数の引数として利用されているような場合にはコールバック関数という。

void func_cb(int num, int (*cb)(int)) {
    int ret = cb(num));
}

実践的な使い方

以前にmbedというものを使ってプログラミングしている際にコマンドラインから実行される関数へのポインタを見た記憶がありました。

mbed - Wikipedia


それと似たものを最近見て(なんとなく)思い出したので以下に書いてみます。
ぼんやりとしか覚えていないため、以下のものがそれかは定かではありませんが、、、

#include <stdio.h>
#include <unistd.h>
#include <string.h>

//
//typedef
//
//科目一覧
typedef enum {
    Jap = 3,
    Mat = 4,
    Eng = 5,
} Subject;

//生徒一覧
struct Student {
	int no;
	char* name;
	int total;
	int jap;
	int mat;
	int eng;
};

// グローバル変数
//科目
static Subject sub;
//点数
static int score;
//生徒
static struct Student student[] = {
    {	1, "yamada"	, 0, 0, 0, 0 },
    {	2, "fijita"	, 0, 0, 0, 0 },
    {	3, "tanaka"	, 0, 0, 0, 0 },
    {	4, "suzuki"	, 0, 0, 0, 0 },
};

//初期化メソッド
static void initScore_cb(struct Student* std) {
	std->jap = 0;
	std->mat = 0;
	std->eng = 0;
    std->total = 0;
    printf("No:%d name:%s init\n", std->no, std->name);
}

//セットメソッド
static void setScore_cb(struct Student* std) {
    switch(sub){
        case Jap:
            std->jap = score;
            break;
        case Mat:
            std->mat = score;
            break;
        case Eng:
            std->eng = score;
            break;
        default:
            return;
    }
    std->total = std->jap + std->mat + std->eng;
}

//結果メソッド
static void resultScore_cb(struct Student* std) {
    printf("-----------------------------------\n");
    printf("No:%d Name:%s TotalScore:%d\n", std->no, std->name, std->total);
}

//関数へのポインタ
typedef void (* command_func_t)(struct Student *std);

//コマンド一覧
static struct {
	char* cmd;
	command_func_t func;
	char *doc;
} command[] = {
	{ "score-init" 	, initScore_cb	,  "init set score test"    },
    { "score-set"	, setScore_cb	,  "set score"			    },
    { "score-result", resultScore_cb,  "result score"           },
	{ NULL          , NULL          , NULL                      }
};
// read command
static void readCommand(char* cmd, int no) {
	for (int i = 0; command[i].cmd; i++) {
		if (strcmp(command[i].cmd, cmd) == 0) {
            command[i].func(&student[no]);
			return;
		}
	}
}
// main
int main(int argc, char* argv[]) {
    //初期化
	readCommand("score-init", 0);
    readCommand("score-init", 1);
    readCommand("score-init", 2);
    readCommand("score-init", 3);

    //値をセット
    sub     = Jap;
    score   = 100; 
    readCommand("score-set", 3);
    sub     = Mat;
    score   = 88; 
    readCommand("score-set", 3);
    sub     = Eng;
    score   = 49; 
    readCommand("score-set", 3);
    sub     = Jap;
    score   = 27; 
    readCommand("score-set", 0);

    //結果
    readCommand("score-result", 0);
    readCommand("score-result", 1);
    readCommand("score-result", 2);
    readCommand("score-result", 3);
}
解説ポイント①

typedefを使用して「関数へのポインタ」の宣言する方法

void (*func_p) (struct Student* std);
void(*func_p)(&std[0]);

typedefを使用することで、以下のように宣言、使用可能です

typedef void (* command_func_t)(struct Student *std);
command_func(&std[0]);
解説ポイント②

3つの関数で関数へのポインタを使用
・static void initScore_cb(struct Student* std);
・static void setScore_cb(struct Student* std);
・static void resultScore_cb(struct Student* std);

上記をcommand配列の第2引数で宣言した関数へのポインタ(command_func_t func)で使用

readCommand()の第1引数の文字列で実行コマンドを分岐して第2引数にセットされている関数へのポインタを実行

コメント

タイトルとURLをコピーしました