點(diǎn)擊上方“C語言與CPP編程”,選擇“關(guān)注/置頂/星標(biāo)公眾號”
干貨福利,第一時間送達(dá)!

? ?你好,我是飛宇。
在講回調(diào)函數(shù)之前,我們需要了解函數(shù)指針。
我們都知道,C語言的靈魂是指針,我們經(jīng)常使用整型指針,字符串指針,結(jié)構(gòu)體指針等
int?*p1;
char?*p2;
STRUCT?*p3;?//STRUCT為我們定義的結(jié)構(gòu)體
但是好像我們一般很少使用函數(shù)指針,我們一般使用函數(shù)都是直接使用函數(shù)調(diào)用。
下面我們來了解一下函數(shù)指針的概念和使用方法。
函數(shù)指針是指向函數(shù)的指針變量。
通常我們說的指針變量是指向一個整型、字符型或數(shù)組等變量,而函數(shù)指針是指向函數(shù)。
函數(shù)指針可以像一般函數(shù)一樣,用于調(diào)用函數(shù)、傳遞參數(shù)。
函數(shù)指針的定義方式為:
函數(shù)返回值類型 (* 指針變量名) (函數(shù)參數(shù)列表);
“函數(shù)返回值類型”表示該指針變量可以指向具有什么返回值類型的函數(shù);“函數(shù)參數(shù)列表”表示該指針變量可以指向具有什么參數(shù)列表的函數(shù)。這個參數(shù)列表中只需要寫函數(shù)的參數(shù)類型即可。
我們看到,函數(shù)指針的定義就是將“函數(shù)聲明”中的“函數(shù)名”改成“(指針變量名)”。但是這里需要注意的是:“(指針變量名)”兩端的括號不能省略,括號改變了運(yùn)算符的優(yōu)先級。如果省略了括號,就不是定義函數(shù)指針而是一個函數(shù)聲明了,即聲明了一個返回值類型為指針型的函數(shù)。
那么怎么判斷一個指針變量是指向變量的指針變量還是指向函數(shù)的指針變量呢?首先看變量名前面有沒有“”,如果有“”說明是指針變量;其次看變量名的后面有沒有帶有形參類型的圓括號,如果有就是指向函數(shù)的指針變量,即函數(shù)指針,如果沒有就是指向變量的指針變量。
最后需要注意的是,指向函數(shù)的指針變量沒有 ++ 和 – 運(yùn)算。
一般為了方便使用,我們會選擇
typedef 函數(shù)返回值類型 (* 指針變量名) (函數(shù)參數(shù)列表);
比如
typedef?int?(*Fun1)(int);//聲明也可寫成int (*Fun1)(int x),但習(xí)慣上一般不這樣。
typedef?int?(*Fun2)(int,?int);//參數(shù)為兩個整型,返回值為整型
typedef?void?(*Fun3)(void);//無參數(shù)和返回值
typedef?void*?(*Fun4)(void*);//參數(shù)和返回值都為void*指針
給大家舉一個例子:
int?Func(int?x);???/*聲明一個函數(shù)*/
int?(*p)?(int?x);??/*定義一個函數(shù)指針*/
p?=?Func;??????????/*將Func函數(shù)的首地址賦給指針變量p*/
p?=?&Func;??????????/*將Func函數(shù)的首地址賦給指針變量p*/
賦值時函數(shù) Func 不帶括號,也不帶參數(shù)。由于函數(shù)名 Func 代表函數(shù)的首地址,因此經(jīng)過賦值以后,指針變量 p 就指向函數(shù) Func() 代碼的首地址了。
下面來寫一個程序,看了這個程序你們就明白函數(shù)指針怎么使用了:
#include?
int?Max(int,?int);??//函數(shù)聲明
int?main(void)
{
????int(*p)(int,?int);??//定義一個函數(shù)指針
????int?a,?b,?c;
????p?=?Max;??//把函數(shù)Max賦給指針變量p,?使p指向Max函數(shù)
????printf("please?enter?a?and?b:");
????scanf("%d%d",?&a,?&b);
????c?=?(*p)(a,?b);??//通過函數(shù)指針調(diào)用Max函數(shù)
????printf("a?=?%d\nb?=?%d\nmax?=?%d\n",?a,?b,?c);
????return?0;
}
int?Max(int?x,?int?y)??//定義Max函數(shù)
{
????int?z;
????if?(x?>?y)
????{
????????z?=?x;
????}
????else
????{
????????z?=?y;
????}
????return?z;
}
特別注意的是,因為函數(shù)名本身就可以表示該函數(shù)地址(指針),因此在獲取函數(shù)指針時,可以直接用函數(shù)名,也可以取函數(shù)的地址。
p?=?Max可以改成?p?=?&Max
c?=?(*p)(a,?b)?可以改成?c?=?p(a,?b)
既然函數(shù)指針變量是一個變量,當(dāng)然也可以作為某個函數(shù)的參數(shù)來使用的。
示例:
#include?
#include?
typedef?void(*FunType)(int);
//前加一個typedef關(guān)鍵字,這樣就定義一個名為FunType函數(shù)指針類型,而不是一個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);//傳入函數(shù)指針常量,作為回調(diào)函數(shù)
????callFun(hisFun,200);
????callFun(herFun,300);
????return?0;
}
void?callFun(FunType?fp,int?x)
{
????fp(x);//通過fp的指針執(zhí)行傳遞進(jìn)來的函數(shù),注意fp所指的函數(shù)有一個參數(shù)
}
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);
}
輸出:

有了上面的基礎(chǔ),要寫出返回類型為函數(shù)指針的函數(shù)應(yīng)該不難了,下面這個例子就是返回類型為函數(shù)指針的函數(shù):
void?(*?func5(int,?int,?float?))(int,?int)
{
????...
}
在這里, func5 以 (int, int, float) 為參數(shù),其返回類型為 void (\*)(int, int) 。在C語言中,變量或者函數(shù)的聲明也是一個大學(xué)問,想要了解更多關(guān)于聲明的話題,可以參考我之前的文章 - C專家編程》讀書筆記(1-3章)。這本書的第三章花了整整一章的內(nèi)容來講解如何讀懂C語言的聲明。
在開始講解回調(diào)函數(shù)前,最后介紹一下函數(shù)指針數(shù)組。既然函數(shù)指針也是指針,那我們就可以用數(shù)組來存放函數(shù)指針。下面我們看一個函數(shù)指針數(shù)組的例子:
/*?方法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];
上面兩種方法都可以用來定義函數(shù)指針數(shù)組,它們定義了一個元素個數(shù)為5,類型是 *void (\*)(int, int, float)*的函數(shù)指針數(shù)組。
我們先來看看百度百科是如何定義回調(diào)函數(shù)的:
回調(diào)函數(shù)就是一個通過函數(shù)指針調(diào)用的函數(shù)。如果你把函數(shù)的指針(地址)作為參數(shù)傳遞給另一個函數(shù),當(dāng)這個指針被用來調(diào)用其所指向的函數(shù)時,我們就說這是回調(diào)函數(shù)。回調(diào)函數(shù)不是由該函數(shù)的實(shí)現(xiàn)方直接調(diào)用,而是在特定的事件或條件發(fā)生時由另外的一方調(diào)用的,用于對該事件或條件進(jìn)行響應(yīng)。
這段話比較長,也比較繞口。下面我通過一幅圖來說明什么是回調(diào):

假設(shè)我們要使用一個排序函數(shù)來對數(shù)組進(jìn)行排序,那么在主程序(Main program)中,我們先通過庫,選擇一個庫排序函數(shù)(Library function)。但排序算法有很多,有冒泡排序,選擇排序,快速排序,歸并排序。同時,我們也可能需要對特殊的對象進(jìn)行排序,比如特定的結(jié)構(gòu)體等。庫函數(shù)會根據(jù)我們的需要選擇一種排序算法,然后調(diào)用實(shí)現(xiàn)該算法的函數(shù)來完成排序工作。這個被調(diào)用的排序函數(shù)就是回調(diào)函數(shù)(Callback function)。
結(jié)合這幅圖和上面對回調(diào)函數(shù)的解釋,我們可以發(fā)現(xiàn),要實(shí)現(xiàn)回調(diào)函數(shù),最關(guān)鍵的一點(diǎn)就是要將函數(shù)的指針傳遞給一個函數(shù)(上圖中是庫函數(shù)),然后這個函數(shù)就可以通過這個指針來調(diào)用回調(diào)函數(shù)了。注意,回調(diào)函數(shù)并不是C語言特有的,幾乎任何語言都有回調(diào)函數(shù)。在C語言中,我們通過使用函數(shù)指針來實(shí)現(xiàn)回調(diào)函數(shù)。
我的理解是:把一段可執(zhí)行的代碼像參數(shù)傳遞那樣傳給其他代碼,而這段代碼會在某個時刻被調(diào)用執(zhí)行,這就叫做回調(diào)。
如果代碼立即被執(zhí)行就稱為同步回調(diào),如果過后再執(zhí)行,則稱之為異步回調(diào)。
回調(diào)函數(shù)就是一個通過函數(shù)指針調(diào)用的函數(shù)。如果你把函數(shù)的指針(地址)作為參數(shù)傳遞給另一個函數(shù),當(dāng)這個指針被用來調(diào)用其所指向的函數(shù)時,我們就說這是回調(diào)函數(shù)。
回調(diào)函數(shù)不是由該函數(shù)的實(shí)現(xiàn)方直接調(diào)用,而是在特定的事件或條件發(fā)生時由另外的一方調(diào)用的,用于對該事件或條件進(jìn)行響應(yīng)。
因為可以把調(diào)用者與被調(diào)用者分開,所以調(diào)用者不關(guān)心誰是被調(diào)用者。它只需知道存在一個具有特定原型和限制條件的被調(diào)用函數(shù)。
簡而言之,回調(diào)函數(shù)就是允許用戶把需要調(diào)用的方法的指針作為參數(shù)傳遞給一個函數(shù),以便該函數(shù)在處理相似事件的時候可以靈活的使用不同的方法。

int?Callback()????///
{
????//?TODO
????return?0;
}
int?main()?????///?主函數(shù)
{
????//?TODO
????Library(Callback);??///
????//?TODO
????return?0;
}
回調(diào)似乎只是函數(shù)間的調(diào)用,和普通函數(shù)調(diào)用沒啥區(qū)別。
但仔細(xì)看,可以發(fā)現(xiàn)兩者之間的一個關(guān)鍵的不同:在回調(diào)中,主程序把回調(diào)函數(shù)像參數(shù)一樣傳入庫函數(shù)。
這樣一來,只要我們改變傳進(jìn)庫函數(shù)的參數(shù),就可以實(shí)現(xiàn)不同的功能,這樣有沒有覺得很靈活?并且當(dāng)庫函數(shù)很復(fù)雜或者不可見的時候利用回調(diào)函數(shù)就顯得十分優(yōu)秀。
int?Callback_1(int?a)???///
{
????printf("Hello,?this?is?Callback_1:?a?=?%d?",?a);
????return?0;
}
int?Callback_2(int?b)??///
{
????printf("Hello,?this?is?Callback_2:?b?=?%d?",?b);
????return?0;
}
int?Callback_3(int?c)???///
{
????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()函數(shù)里面的參數(shù)是一個指針,在main()函數(shù)里調(diào)用Handle()函數(shù)的時候,給它傳入了函數(shù)Callback_1()/Callback_2()/Callback_3()的函數(shù)名,這時候的函數(shù)名就是對應(yīng)函數(shù)的指針,也就是說,回調(diào)函數(shù)其實(shí)就是函數(shù)指針的一種用法。
#include?
#include?
/****************************************
?*?函數(shù)指針結(jié)構(gòu)體
?***************************************/
typedef?struct?_OP?{
????float?(*p_add)(float,?float);?
????float?(*p_sub)(float,?float);?
????float?(*p_mul)(float,?float);?
????float?(*p_div)(float,?float);?
}?OP;?
/****************************************
?*?加減乘除函數(shù)
?***************************************/
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;
}
/****************************************
?*?初始化函數(shù)指針
?***************************************/
void?init_op(OP?*op)
{
????op->p_add?=?ADD;
????op->p_sub?=?SUB;
????op->p_mul?=?&MUL;
????op->p_div?=?÷
}
/****************************************
?*?庫函數(shù)
?***************************************/
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);
????
????/*?直接使用函數(shù)指針調(diào)用函數(shù)?*/?
????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));
?????
????/*?調(diào)用回調(diào)函數(shù)?*/?
????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模塊聯(lián)網(wǎng)的小項目,使用過的同學(xué)大概知道2G、4G、NB等模塊要想實(shí)現(xiàn)無線聯(lián)網(wǎng)功能都需要經(jīng)歷模塊上電初始化、注冊網(wǎng)絡(luò)、查詢網(wǎng)絡(luò)信息質(zhì)量、連接服務(wù)器等步驟,這里的的例子就是,利用一個狀態(tài)機(jī)函數(shù)(根據(jù)不同狀態(tài)依次調(diào)用不同實(shí)現(xiàn)方法的函數(shù)),通過回調(diào)函數(shù)的方式依次調(diào)用不同的函數(shù),實(shí)現(xiàn)模塊聯(lián)網(wǎng)功能,如下:
/*********??工作狀態(tài)處理??*********/
typedef?struct
{
?uint8_t?mStatus;
?uint8_t?(*?Funtion)(void);?//函數(shù)指針的形式
}?M26_WorkStatus_TypeDef;??//M26的工作狀態(tài)集合調(diào)用函數(shù)
/**********************************************
**?>M26工作狀態(tài)集合函數(shù)
***********************************************/
M26_WorkStatus_TypeDef?M26_WorkStatus_Tab[]?=
{????
????{GPRS_NETWORK_CLOSE,??M26_PWRKEY_Off??},?//模塊關(guān)機(jī)
????{GPRS_NETWORK_OPEN,??M26_PWRKEY_On??},?//模塊開機(jī)
????{GPRS_NETWORK_Start,???M26_Work_Init??},?//管腳初始化
????{GPRS_NETWORK_CONF,??M26_NET_Config??},?/AT指令配置
????{GPRS_NETWORK_LINK_CTC,??M26_LINK_CTC??},?//連接調(diào)度中心??
????{GPRS_NETWORK_WAIT_CTC,?M26_WAIT_CTC??},??//等待調(diào)度中心回復(fù)?
????{GPRS_NETWORK_LINK_FEM,?M26_LINK_FEM??},?//連接前置機(jī)
????{GPRS_NETWORK_WAIT_FEM,?M26_WAIT_FEM??},?//等待前置機(jī)回復(fù)
????{GPRS_NETWORK_COMM,??M26_COMM???},?//正常工作????
????{GPRS_NETWORK_WAIT_Sig,??M26_WAIT_Sig??},??//等待信號回復(fù)
????{GPRS_NETWORK_GetSignal,??M26_GetSignal??},?//獲取信號值
????{GPRS_NETWORK_RESTART,??M26_RESET???},?//模塊重啟
}
/**********************************************
**?>M26模塊工作狀態(tài)機(jī),依次調(diào)用里面的12個函數(shù)???
***********************************************/
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模塊聯(lián)網(wǎng)項目,可以copy上面的框架,只需要修改回調(diào)函數(shù)內(nèi)部的具體實(shí)現(xiàn),或者增加、減少回調(diào)函數(shù),就可以很簡潔快速的實(shí)現(xiàn)模塊聯(lián)網(wǎng)。
你好,我是飛宇,本碩均于某中流985 CS就讀,先后于百度搜索以及字節(jié)某電商部門擔(dān)任Linux C/C++后端研發(fā)工程師。
同時,我也是知乎博主@韓飛宇,日常分享C/C++、計算機(jī)學(xué)習(xí)經(jīng)驗、工作體會,歡迎點(diǎn)擊閱讀原文圍觀我分享的學(xué)習(xí)經(jīng)驗
我組建了一些社群一起交流,群里有大牛也有小白,如果你有意可以一起進(jìn)群交流。

歡迎你添加我的微信,我拉你進(jìn)技術(shù)交流群。此外,我也會經(jīng)常在微信上分享一些計算機(jī)學(xué)習(xí)經(jīng)驗以及工作體驗,還有一些內(nèi)推機(jī)會。

掃描上方二維碼,加我微信
我的私人微信

??點(diǎn)擊閱讀原文查看你錯過的寶藏