C語(yǔ)言進(jìn)階之回調函數詳解
在講回調函數之前,我們需要了解函數指針。
本文引用地址:http://dyxdggzs.com/article/202308/449836.htm我們都知道,C語(yǔ)言的靈魂是指針,我們經(jīng)常使用整型指針,字符串指針,結構體指針等。
int *p1;
char *p2;
STRUCT *p3; // STRUCT為我們定義的結構體
但是好像我們一般很少使用函數指針,我們一般使用函數都是直接使用函數調用。
下面我們來(lái)了解一下函數指針的概念和使用方法。
1. 概念
函數指針是指向函數的指針變量。
通常我們說(shuō)的指針變量是指向一個(gè)整型、字符型或數組等變量,而函數指針是指向函數。
函數指針可以像一般函數一樣,用于調用函數、傳遞參數。
函數指針的定義方式為:
函數返回值類(lèi)型 (* 指針變量名) (函數參數列表);
“函數返回值類(lèi)型”表示該指針變量可以指向具有什么返回值類(lèi)型的函數;“函數參數列表”表示該指針變量可以指向具有什么參數列表的函數。這個(gè)參數列表中只需要寫(xiě)函數的參數類(lèi)型即可。
我們看到,函數指針的定義就是將“函數聲明”中的“函數名”改成“(指針變量名)”。但是這里需要注意的是:“(指針變量名)”兩端的括號不能省略,括號改變了運算符的優(yōu)先級。如果省略了括號,就不是定義函數指針而是一個(gè)函數聲明了,即聲明了一個(gè)返回值類(lèi)型為指針型的函數。
那么怎么判斷一個(gè)指針變量是指向變量的指針變量還是指向函數的指針變量呢?首先看變量名前面有沒(méi)有“”,如果有“”說(shuō)明是指針變量;其次看變量名的后面有沒(méi)有帶有形參類(lèi)型的圓括號,如果有就是指向函數的指針變量,即函數指針,如果沒(méi)有就是指向變量的指針變量。
最后需要注意的是,指向函數的指針變量沒(méi)有 ++ 和 -- 運算。
一般為了方便使用,我們會(huì )選擇:
typedef 函數返回值類(lèi)型 (* 指針變量名) (函數參數列表);
比如:
typedef int (*Fun1)(int); //聲明也可寫(xiě)成int (*Fun1)(int x),但習慣上一般不這樣。
typedef int (*Fun2)(int, int); //參數為兩個(gè)整型,返回值為整型
typedef void (*Fun3)(void); //無(wú)參數和返回值
typedef void* (*Fun4)(void*); //參數和返回值都為void*指針
2. 如何用函數指針調用函數
給大家舉一個(gè)例子:
int Func(int x); /*聲明一個(gè)函數*/
int (*p) (int x); /*定義一個(gè)函數指針*/
p = Func; /*將Func函數的首地址賦給指針變量p*/
p = &Func; /*將Func函數的首地址賦給指針變量p*/
賦值時(shí)函數 Func 不帶括號,也不帶參數。由于函數名 Func 代表函數的首地址,因此經(jīng)過(guò)賦值以后,指針變量 p 就指向函數 Func() 代碼的首地址了。
下面來(lái)寫(xiě)一個(gè)程序,看了這個(gè)程序你們就明白函數指針怎么使用了:
#include
int Max(int, int); //函數聲明
int main(void)
{
int(*p)(int, int); //定義一個(gè)函數指針
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); //通過(guò)函數指針調用Max函數
printf("a = %dnb = %dnmax = %dn", a, b, c);
return 0;
}
int Max(int x, int y) //定義Max函數
{
int z;
if (x > y)
{
z = x;
}
else
{
z = y;
}
return z;
}
特別注意的是,因為函數名本身就可以表示該函數地址(指針),因此在獲取函數指針時(shí),可以直接用函數名,也可以取函數的地址。
p = Max 可以改成 p = &Max
c = (*p)(a, b) 可以改成 c = p(a, b)
3. 函數指針作為某個(gè)函數的參數
既然函數指針變量是一個(gè)變量,當然也可以作為某個(gè)函數的參數來(lái)使用的。示例:
#include
#include
//前加一個(gè)typedef關(guān)鍵字,這樣就定義一個(gè)名為FunType函數指針類(lèi)型,而不是一個(gè)FunType變量。
//形式同 typedef int* PINT;
typedef void(*FunType)(int);
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);//通過(guò)fp的指針執行傳遞進(jìn)來(lái)的函數,注意fp所指的函數有一個(gè)參數
}
void myFun(int x)
{
printf("myFun: %dn",x);
}
void hisFun(int x)
{
printf("hisFun: %dn",x);
}
void herFun(int x)
{
printf("herFun: %dn",x);
}
輸出:
4. 函數指針作為函數返回類(lèi)型
有了上面的基礎,要寫(xiě)出返回類(lèi)型為函數指針的函數應該不難了,下面這個(gè)例子就是返回類(lèi)型為函數指針的函數:
void (* func5(int, int, float))(int, int)
{
...
}
在這里,func5
以(int, int, float)
為參數,其返回類(lèi)型為void (*)(int, int)
。在C語(yǔ)言中,變量或者函數的聲明也是一個(gè)大學(xué)問(wèn),想要了解更多關(guān)于聲明的話(huà)題,可以參考《C專(zhuān)家編程》(1-3章)。這本書(shū)的第三章花了整整一章的內容來(lái)講解如何讀懂C語(yǔ)言的聲明。
5. 函數指針數組
在開(kāi)始講解回調函數前,最后介紹一下函數指針數組。既然函數指針也是指針,那我們就可以用數組來(lái)存放函數指針。下面我們看一個(gè)函數指針數組的例子:
/* 方法 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];
上面兩種方法都可以用來(lái)定義函數指針數組,它們定義了一個(gè)元素個(gè)數為5,類(lèi)型是 * void (*)(int, int, float)
* 的函數指針數組。
6. 函數指針總結
函數指針常量 :Max;函數指針變量:p;
數名調用如果都得如 (*myFun)(10) 這樣,那書(shū)寫(xiě)與讀起來(lái)都是不方便和不習慣的。所以C語(yǔ)言的設計者們才會(huì )設計成又可允許 myFun(10) 這種形式地調用(這樣方便多了,并與數學(xué)中的函數形式一樣)。
在函數指針變量也可以存入一個(gè)數組內。數組的聲明方法:int (*fArray[10]) (int);
1. 什么是回調函數
我們先來(lái)看看百度百科是如何定義回調函數的:
回調函數就是一個(gè)通過(guò)函數指針調用的函數。如果你把函數的指針(地址)作為參數傳遞給另一個(gè)函數,當這個(gè)指針被用來(lái)調用其所指向的函數時(shí),我們就說(shuō)這是回調函數?;卣{函數不是由該函數的實(shí)現方直接調用,而是在特定的事件或條件發(fā)生時(shí)由另外的一方調用的,用于對該事件或條件進(jìn)行響應。
這段話(huà)比較長(cháng),也比較繞口。下面我通過(guò)一幅圖來(lái)說(shuō)明什么是回調:
假設我們要使用一個(gè)排序函數來(lái)對數組進(jìn)行排序,那么在主程序(Main program)中,我們先通過(guò)庫,選擇一個(gè)庫排序函數(Library function)。但排序算法有很多,有冒泡排序,選擇排序,快速排序,歸并排序。
同時(shí),我們也可能需要對特殊的對象進(jìn)行排序,比如特定的結構體等。庫函數會(huì )根據我們的需要選擇一種排序算法,然后調用實(shí)現該算法的函數來(lái)完成排序工作。這個(gè)被調用的排序函數就是回調函數(Callback function)。
結合這幅圖和上面對回調函數的解釋?zhuān)覀兛梢园l(fā)現,要實(shí)現回調函數,最關(guān)鍵的一點(diǎn)就是要將函數的指針傳遞給一個(gè)函數(上圖中是庫函數),然后這個(gè)函數就可以通過(guò)這個(gè)指針來(lái)調用回調函數了。注意,回調函數并不是C語(yǔ)言特有的,幾乎任何語(yǔ)言都有回調函數。在C語(yǔ)言中,我們通過(guò)使用函數指針來(lái)實(shí)現回調函數。
把一段可執行的代碼像參數傳遞那樣傳給其他代碼,而這段代碼會(huì )在某個(gè)時(shí)刻被調用執行,這就叫做回調。
如果代碼立即被執行就稱(chēng)為同步回調,如果過(guò)后再執行,則稱(chēng)之為異步回調。
回調函數就是一個(gè)通過(guò)函數指針調用的函數。如果你把函數的指針(地址)作為參數傳遞給另一個(gè)函數,當這個(gè)指針被用來(lái)調用其所指向的函數時(shí),我們就說(shuō)這是回調函數。
回調函數不是由該函數的實(shí)現方直接調用,而是在特定的事件或條件發(fā)生時(shí)由另外的一方調用的,用于對該事件或條件進(jìn)行響應。
2. 為什么要用回調函數?
因為可以把調用者與被調用者分開(kāi),所以調用者不關(guān)心誰(shuí)是被調用者。它只需知道存在一個(gè)具有特定原型和限制條件的被調用函數。
簡(jiǎn)而言之,回調函數就是允許用戶(hù)把需要調用的方法的指針作為參數傳遞給一個(gè)函數,以便該函數在處理相似事件的時(shí)候可以靈活的使用不同的方法。
int Callback() // /< 回調函數
{
// TODO
return 0;
}
int main() // /< 主函數
{
// TODO
Library(Callback); // /< 庫函數通過(guò)函數指針進(jìn)行回調
// TODO
return 0;
}
回調似乎只是函數間的調用,和普通函數調用沒(méi)啥區別。
但仔細看,可以發(fā)現兩者之間的一個(gè)關(guān)鍵的不同:在回調中,主程序把回調函數像參數一樣傳入庫函數。
這樣一來(lái),只要我們改變傳進(jìn)庫函數的參數,就可以實(shí)現不同的功能,這樣有沒(méi)有覺(jué)得很靈活?并且當庫函數很復雜或者不可見(jiàn)的時(shí)候利用回調函數就顯得十分優(yōu)秀。
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() 函數里面的參數是一個(gè)指針,在 main() 函數里調用 Handle() 函數的時(shí)候,給它傳入了函數 Callback_1()/Callback_2()/Callback_3() 的函數名,這時(shí)候的函數名就是對應函數的指針,也就是說(shuō),回調函數其實(shí)就是函數指針的一種用法。
4. 下面是一個(gè)四則運算的簡(jiǎn)單回調函數例子:
#include
#include
/****************************************
* 函數指針結構體
***************************************/
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 = %fn", (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 = %fn",
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;
}
5. 回調函數實(shí)例(很有用)
一個(gè) GPRS 模塊聯(lián)網(wǎng)的小項目,使用過(guò)的同學(xué)大概知道 2G、4G、NB 等模塊要想實(shí)現無(wú)線(xiàn)聯(lián)網(wǎng)功能都需要經(jīng)歷模塊上電初始化、注冊網(wǎng)絡(luò )、查詢(xún)網(wǎng)絡(luò )信息質(zhì)量、連接服務(wù)器等步驟,這里的的例子就是,利用一個(gè)狀態(tài)機函數(根據不同狀態(tài)依次調用不同實(shí)現方法的函數),通過(guò)回調函數的方式依次調用不同的函數,實(shí)現模塊聯(lián)網(wǎng)功能,如下:
/********* 工作狀態(tài)處理 *********/
typedef struct
{
uint8_t mStatus;
uint8_t (* Funtion)(void); //函數指針的形式
} M26_WorkStatus_TypeDef; //M26的工作狀態(tài)集合調用函數
/**********************************************
** >M26工作狀態(tài)集合函數
***********************************************/
M26_WorkStatus_TypeDef M26_WorkStatus_Tab[] =
{
{GPRS_NETWORK_CLOSE, M26_PWRKEY_Off }, //模塊關(guān)機
{GPRS_NETWORK_OPEN, M26_PWRKEY_On }, //模塊開(kāi)機
{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模塊工作狀態(tài)機,依次調用里面的12個(gè)函數
***********************************************/
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;
}
所以,如果有人想做個(gè) NB 模塊聯(lián)網(wǎng)項目,可以 copy 上面的框架,只需要修改回調函數內部的具體實(shí)現,或者增加、減少回調函數,就可以很簡(jiǎn)潔快速的實(shí)現模塊聯(lián)網(wǎng)。
原文:https://blog.csdn.net/qq_41854911/article/details/121058935
文章來(lái)源于網(wǎng)絡(luò ),版權歸原作者所有,如有侵權,請聯(lián)系刪除。
評論