1. 関数ポインタ
コールバック関数について話す前に、関数ポインターを理解する必要があります。
C 言語の本質はポインタであることは誰もが知っており、整数ポインタ、文字列ポインタ、構造体ポインタなどをよく使用します。
int *p1;
char *p2;
STRUCT *p3; //STRUCT为我们定义的结构体
しかし、関数ポインタを使用することはほとんどないようで、通常は関数呼び出しを直接使用します。
関数ポインターの概念と使用法を見てみましょう。
1.1 コンセプト
関数ポインタは、関数を指すポインタ変数です。
通常、ここで説明するポインタ変数は整数、文字、または配列変数を指しますが、関数ポインタは関数を指します。
関数ポインタを使用すると、通常の関数と同様に関数を呼び出し、パラメータを渡すことができます。
関数ポインタは次のように定義されます。
関数の戻り値の型 (* ポインタ変数名) (関数パラメータのリスト);
「関数の戻り値の型」はポインタ変数が指すことができる戻り値の型を示します; 「関数パラメータのリスト」はポインタ変数が指すことができるパラメータを示します。機能。このパラメータ リストには、関数のパラメータの型を記述するだけです。
関数ポインタの定義は、「関数宣言」の「関数名」を「(ポインタ変数名)」に変更することであることがわかります。ただし、ここで注意が必要なのは、「(ポインタ変数名)」の両端の括弧は演算子の優先順位を変えるため省略できないことです。括弧を省略した場合は、関数ポインタの定義ではなく関数宣言、つまり戻り値の型がポインタである関数が宣言されたことになります。
では、ポインタ変数が変数を指すポインタ変数であるか、関数を指すポインタ変数であるかを判断するにはどうすればよいでしょうか? まず、変数名の前に「」があるかどうかを確認します。「」がある場合はポインタ変数です。次に、変数名の後に仮引数の型を示す括弧があるかどうかを確認します。ある場合は、は関数を指すポインタ変数、つまり関数ポインタで、ない場合は変数を指すポインタ変数です。
最後に注意すべき点は、関数へのポインター変数には ++ および – 演算がないことです。
通常、使いやすさを考慮して、
typedef 関数の戻り値の型 (* ポインター変数名) (関数パラメーターのリスト);
例:
typedef int (*Fun1)(int);//声明也可写成int (*Fun1)(int x),但习惯上一般不这样。
typedef int (*Fun2)(int, int);//参数为两个整型,返回值为整型
typedef void (*Fun3)(void);//无参数和返回值
typedef void* (*Fun4)(void*);//参数和返回值都为void*指针
1.2 関数ポインタを使用して関数を呼び出す方法
例を挙げてみましょう。
int Func(int x); /*声明一个函数*/
int (*p) (int x); /*定义一个函数指针*/
p = Func; /*将Func函数的首地址赋给指针变量p*/
p = &Func; /*将Func函数的首地址赋给指针变量p*/
関数 Func は、値を割り当てるときに括弧やパラメーターを受け取りません。関数名 Func は関数の最初のアドレスを表すため、代入後、ポインタ変数 p は関数 Func() コードの最初のアドレスを指します。
プログラムを書いてみましょう。このプログラムを読むと、関数ポインタの使い方が理解できるようになります。
#include <stdio.h>
int Max(int, int); //函数声明
int main(void)
{
int(*p)(int, int); //定义一个函数指针
int a, b, c;
p = Max; //把函数Max赋给指针变量p, 使p指向Max函数
printf("please enter a and b:");
scanf("%d%d", &a, &b);
c = (*p)(a, b); //通过函数指针调用Max函数
printf("a = %d\nb = %d\nmax = %d\n", a, b, c);
return 0;
}
int Max(int x, int y) //定义Max函数
{
int z;
if (x > y)
{
z = x;
}
else
{
z = y;
}
return z;
}
関数名自体が関数アドレス (ポインタ) を表すことができるため、関数ポインタを取得するときに、関数名を直接使用することも、関数のアドレスを取得することもできる点に注意してください。
p = Max は p = &Max に変更できます
c = (*p)(a, b) は c = p(a, b) に変更できます3.
関数ポインタは、ある関数のパラメータとして使用されます。
関数ポインタ変数は変数ですが、もちろん関数のパラメータとしても使用できます。
例:
include <stdio.h>
#include <stdlib.h>
typedef void(*FunType)(int);
//前加一个typedef关键字,这样就定义一个名为FunType函数指针类型,而不是一个FunType变量。
//形式同 typedef int* PINT;
void myFun(int x);
void hisFun(int x);
void herFun(int x);
void callFun(FunType fp,int x);
int main()
{
callFun(myFun,100);//传入函数指针常量,作为回调函数
callFun(hisFun,200);
callFun(herFun,300);
return 0;
}
void callFun(FunType fp,int x)
{
fp(x);//通过fp的指针执行传递进来的函数,注意fp所指的函数有一个参数
}
void myFun(int x)
{
printf("myFun: %d\n",x);
}
void hisFun(int x)
{
printf("hisFun: %d\n",x);
}
void herFun(int x)
{
printf("herFun: %d\n",x);
}
出力:
4. 関数の戻り値の型としての関数ポインタ
上記の基礎を備えているため、戻り値の型が関数ポインタである関数を作成することは難しくありません。次の例は、戻り値の型が関数ポインタである関数です。
void (* func5(int, int, float ))(int, int)
{
...
}
ここで、func5 は (int, int, float) をパラメータとして受け取り、戻り値の型は void (*)(int, int) です。C言語では、変数や関数の宣言も大学の科目となっています。宣言について詳しく知りたい場合は、前回の記事「Cエキスパートプログラミング」の読書メモ(第1章~第3章)を参照してください。本書の第 3 章では、C 言語の宣言の読み方について 1 章丸々を割いて説明しています。
5. 関数ポインタ配列
コールバック関数の説明を始める前に、最後に関数ポインター配列について紹介しましょう。関数ポインタもポインタであるため、配列を使用して関数ポインタを格納できます。関数ポインターの配列の例を見てみましょう。
/* 方法1 */
void (*func_array_1[5])(int, int, float);
/* 方法2 */
typedef void (*p_func_array)(int, int, float);
p_func_array func_array_2[5];
上記のメソッドはどちらも関数ポインタ配列の定義に使用でき、要素が 5 つで型が *void (*)(int, int, float)* の関数ポインタ配列を定義します。
6. 関数ポインタの概要
関数ポインタ定数: Max; 関数ポインタ変数: p;
番号呼び出しが (*myFun)(10) のような場合、書き込みや読み取りが不便で不慣れになります。だからこそ、C 言語の設計者は、myFun(10) をこの形式で呼び出せるように設計しました (これははるかに便利で、数学の関数形式と同じです)。
関数ポインター変数は配列に格納することもできます。配列宣言方法: int (*fArray[10]) (int);
2. コールバック関数
2.1 コールバック関数とは何ですか?
まず、Baidu Encyclopedia がコールバック関数をどのように定義しているかを見てみましょう。
コールバック関数は、関数ポインターを通じて呼び出される関数です。関数ポインタ (アドレス) をパラメータとして別の関数に渡し、このポインタがそれが指す関数を呼び出すために使用される場合、それをコールバック関数と呼びます。コールバック関数は、関数の実装者によって直接呼び出されるのではなく、特定のイベントまたは条件が発生したときに、そのイベントまたは条件に応答するために別のパーティによって呼び出されます。
この文章は比較的長くて複雑です。コールバックとは何かを図で説明します。
ソート関数を使用して配列をソートする場合、メイン プログラム (Main プログラム) で、まずライブラリを通じてライブラリ ソート関数 (Library 関数) を選択します。ただし、バブル ソート、選択ソート、クイック ソート、マージ ソートなど、多くのソート アルゴリズムがあります。同時に、特定の構造などの特別なオブジェクトを並べ替える必要がある場合もあります。ライブラリ関数はニーズに基づいて並べ替えアルゴリズムを選択し、そのアルゴリズムを実装する関数を呼び出して並べ替え作業を完了します。呼び出されるソート関数はコールバック関数です。
この図と上記のコールバック関数の説明を組み合わせると、コールバック関数を実装する最も重要なポイントは、関数ポインタを関数 (上の図ではライブラリ関数) に渡すことであることがわかります。その後、この関数は次のことを行うことができます。コールバック関数はこのポインタを通じて呼び出されます。コールバック関数は C 言語に固有のものではなく、ほとんどすべての言語にコールバック関数があることに注意してください。C言語では関数ポインタを利用してコールバック関数を実装します。
私の理解は次のとおりです。パラメータの受け渡しのように、実行可能コードの一部を他のコードに渡すと、このコードは特定の瞬間に呼び出されて実行されます。これはコールバックと呼ばれます。
コードがすぐに実行される場合は同期コールバックと呼ばれ、後で実行される場合は非同期コールバックと呼ばれます。
コールバック関数は、関数ポインターを通じて呼び出される関数です。関数ポインタ (アドレス) をパラメータとして別の関数に渡し、このポインタがそれが指す関数を呼び出すために使用される場合、それをコールバック関数と呼びます。
コールバック関数は、関数の実装者によって直接呼び出されるのではなく、特定のイベントまたは条件が発生したときに、そのイベントまたは条件に応答するために別のパーティによって呼び出されます。
2.2 なぜコールバック関数を使用するのですか?
発信者と着信者を分離できるため、発信者は着信者が誰であるかを気にしません。特定のプロトタイプと制約を持つ呼び出される関数が存在することを知る必要があるだけです。
つまり、コールバック関数を使用すると、呼び出されるメソッドのポインタをパラメータとして関数に渡すことができるため、関数は同様のイベントを処理するときに異なるメソッドを柔軟に使用できます。
int Callback() ///< 回调函数
{
// TODO
return 0;
}
int main() ///< 主函数
{
// TODO
Library(Callback); ///< 库函数通过函数指针进行回调
// TODO
return 0;
}
コールバックは単なる関数間の呼び出しであり、通常の関数呼び出しと何ら変わりません。
しかし、よく見てみると、この 2 つの重要な違いが見つかります。コールバックでは、メイン プログラムがコールバック関数をパラメータのようにライブラリ関数に渡します。
このように、ライブラリ関数に渡すパラメータを変更するだけで、さまざまな機能を実現できるのですが、とても柔軟だと思いませんか?また、ライブラリ関数が複雑な場合や目に見えない場合には、コールバック関数を使用することは非常に良いことです。
3 コールバック関数の使い方は?
int Callback_1(int a) ///< 回调函数1
{
printf("Hello, this is Callback_1: a = %d ", a);
return 0;
}
int Callback_2(int b) ///< 回调函数2
{
printf("Hello, this is Callback_2: b = %d ", b);
return 0;
}
int Callback_3(int c) ///< 回调函数3
{
printf("Hello, this is Callback_3: c = %d ", c);
return 0;
}
int Handle(int x, int (*Callback)(int)) ///< 注意这里用到的函数指针定义
{
Callback(x);
}
int main()
{
Handle(4, Callback_1);
Handle(5, Callback_2);
Handle(6, Callback_3);
return 0;
}
上記のコードに示すように、Handle() 関数のパラメータはポインタであることがわかります。main() 関数で Handle() 関数を呼び出すと、関数 Callback_1()/Callback_2()/Callback_3()このときの関数名は対応する関数のポインタ、つまりコールバック関数は実際には関数ポインタを利用したものになります。
4. 以下は、四則演算を使用した単純なコールバック関数の例です。
#include <stdio.h>
#include <stdlib.h>
/****************************************
* 函数指针结构体
***************************************/
typedef struct _OP {
float (*p_add)(float, float);
float (*p_sub)(float, float);
float (*p_mul)(float, float);
float (*p_div)(float, float);
} OP;
/****************************************
* 加减乘除函数
***************************************/
float ADD(float a, float b)
{
return a + b;
}
float SUB(float a, float b)
{
return a - b;
}
float MUL(float a, float b)
{
return a * b;
}
float DIV(float a, float b)
{
return a / b;
}
/****************************************
* 初始化函数指针
***************************************/
void init_op(OP *op)
{
op->p_add = ADD;
op->p_sub = SUB;
op->p_mul = &MUL;
op->p_div = &DIV;
}
/****************************************
* 库函数
***************************************/
float add_sub_mul_div(float a, float b, float (*op_func)(float, float))
{
return (*op_func)(a, b);
}
int main(int argc, char *argv[])
{
OP *op = (OP *)malloc(sizeof(OP));
init_op(op);
/* 直接使用函数指针调用函数 */
printf("ADD = %f, SUB = %f, MUL = %f, DIV = %f\n", (op->p_add)(1.3, 2.2), (*op->p_sub)(1.3, 2.2),
(op->p_mul)(1.3, 2.2), (*op->p_div)(1.3, 2.2));
/* 调用回调函数 */
printf("ADD = %f, SUB = %f, MUL = %f, DIV = %f\n",
add_sub_mul_div(1.3, 2.2, ADD),
add_sub_mul_div(1.3, 2.2, SUB),
add_sub_mul_div(1.3, 2.2, MUL),
add_sub_mul_div(1.3, 2.2, DIV));
return 0;
}
- コールバック関数インスタンス (非常に便利)
GPRS モジュール ネットワーキングのための小規模プロジェクトです。これを使用したことのある学生はおそらく、2G、4G、NB およびその他のモジュールのワイヤレス ネットワーキング機能を実装するには、モジュールの電源投入時の初期化、ネットワークの登録、ネットワーク情報品質クエリ、サーバー接続 ここでは、ステート マシン関数 (さまざまな状態に応じてさまざまな実装メソッドを順番に呼び出す関数) を使用し、コールバック関数を通じてさまざまな関数を順番に呼び出して、モジュール ネットワーキング関数を実装する例を示します。 、 次のように:
/********* 工作状态处理 *********/
typedef struct
{
uint8_t mStatus;
uint8_t (* Funtion)(void); //函数指针的形式
} M26_WorkStatus_TypeDef; //M26的工作状态集合调用函数
/**********************************************
** >M26工作状态集合函数
***********************************************/
M26_WorkStatus_TypeDef M26_WorkStatus_Tab[] =
{
{
GPRS_NETWORK_CLOSE, M26_PWRKEY_Off }, //模块关机
{
GPRS_NETWORK_OPEN, M26_PWRKEY_On }, //模块开机
{
GPRS_NETWORK_Start, M26_Work_Init }, //管脚初始化
{
GPRS_NETWORK_CONF, M26_NET_Config }, /AT指令配置
{
GPRS_NETWORK_LINK_CTC, M26_LINK_CTC }, //连接调度中心
{
GPRS_NETWORK_WAIT_CTC, M26_WAIT_CTC }, //等待调度中心回复
{
GPRS_NETWORK_LINK_FEM, M26_LINK_FEM }, //连接前置机
{
GPRS_NETWORK_WAIT_FEM, M26_WAIT_FEM }, //等待前置机回复
{
GPRS_NETWORK_COMM, M26_COMM }, //正常工作
{
GPRS_NETWORK_WAIT_Sig, M26_WAIT_Sig }, //等待信号回复
{
GPRS_NETWORK_GetSignal, M26_GetSignal }, //获取信号值
{
GPRS_NETWORK_RESTART, M26_RESET }, //模块重启
}
/**********************************************
** >M26模块工作状态机,依次调用里面的12个函数
***********************************************/
uint8_t M26_WorkStatus_Call(uint8_t Start)
{
uint8_t i = 0;
for(i = 0; i < 12; i++)
{
if(Start == M26_WorkStatus_Tab[i].mStatus)
{
return M26_WorkStatus_Tab[i].Funtion();
}
}
return 0;
}
したがって、NB モジュール ネットワーキング プロジェクトを作成したい場合は、上記のフレームワークをコピーして、コールバック関数内の特定の実装を変更するか、コールバック関数を追加または削減するだけで、モジュール ネットワーキングを非常に簡単かつ迅速に実装できます。 。