一、函數指針
在講回調函數之前,我們需要了解函數指針。
我們都知道,C語言的靈魂是指針,我們經常使用整型指針,字符串指針,結構體指針等
?
int?*p1; char?*p2; STRUCT?*p3;?//STRUCT為我們定義的結構體
?
但是好像我們一般很少使用函數指針,我們一般使用函數都是直接使用函數調用。
下面我們來了解一下函數指針的概念和使用方法。
1.概念
函數指針是指向函數的指針變量。
通常我們說的指針變量是指向一個整型、字符型或數組等變量,而函數指針是指向函數。
函數指針可以像一般函數一樣,用于調用函數、傳遞參數。
函數指針的定義方式為:
函數返回值類型 (* 指針變量名) (函數參數列表);
“函數返回值類型”表示該指針變量可以指向具有什么返回值類型的函數;“函數參數列表”表示該指針變量可以指向具有什么參數列表的函數。這個參數列表中只需要寫函數的參數類型即可。
我們看到,函數指針的定義就是將“函數聲明”中的“函數名”改成“(指針變量名)”。但是這里需要注意的是:“(指針變量名)”兩端的括號不能省略,括號改變了運算符的優先級。如果省略了括號,就不是定義函數指針而是一個函數聲明了,即聲明了一個返回值類型為指針型的函數。
那么怎么判斷一個指針變量是指向變量的指針變量還是指向函數的指針變量呢?首先看變量名前面有沒有“”,如果有“”說明是指針變量;其次看變量名的后面有沒有帶有形參類型的圓括號,如果有就是指向函數的指針變量,即函數指針,如果沒有就是指向變量的指針變量。
最后需要注意的是,指向函數的指針變量沒有 ++ 和 – 運算。
一般為了方便使用,我們會選擇
typedef 函數返回值類型 (* 指針變量名) (函數參數列表);
比如
?
typedef?int?(*Fun1)(int);//聲明也可寫成int?(*Fun1)(int?x),但習慣上一般不這樣。 typedef?int?(*Fun2)(int,?int);//參數為兩個整型,返回值為整型 typedef?void?(*Fun3)(void);//無參數和返回值 typedef?void*?(*Fun4)(void*);//參數和返回值都為void*指針
?
2,如何用函數指針調用函數
給大家舉一個例子:
?
int?Func(int?x);???/*聲明一個函數*/ int?(*p)?(int?x);??/*定義一個函數指針*/ p?=?Func;??????????/*將Func函數的首地址賦給指針變量p*/ p?=?&Func;??????????/*將Func函數的首地址賦給指針變量p*/
?
賦值時函數 Func 不帶括號,也不帶參數。由于函數名 Func 代表函數的首地址,因此經過賦值以后,指針變量 p 就指向函數 Func() 代碼的首地址了。
下面來寫一個程序,看了這個程序你們就明白函數指針怎么使用了:
?
#include?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 b?=?%d max?=?%d ",?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?#include? 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 ",x); } void?hisFun(int?x) { ????printf("hisFun:?%d ",x); } void?herFun(int?x) { ????printf("herFun:?%d ",x); }
?
輸出:
4.函數指針作為函數返回類型
有了上面的基礎,要寫出返回類型為函數指針的函數應該不難了,下面這個例子就是返回類型為函數指針的函數:
?
void?(*?func5(int,?int,?float?))(int,?int) { ????... }
?
在這里, func5 以 (int, int, float) 為參數,其返回類型為 void (*)(int, int) 。在C語言中,變量或者函數的聲明也是一個大學問,想要了解更多關于聲明的話題,可以參考我之前的文章 - C專家編程》讀書筆記(1-3章)。這本書的第三章花了整整一章的內容來講解如何讀懂C語言的聲明。
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 );
二、回調函數
1.什么是回調函數
我們先來看看百度百科是如何定義回調函數的:
回調函數就是一個通過函數指針調用的函數。如果你把函數的指針(地址)作為參數傳遞給另一個函數,當這個指針被用來調用其所指向的函數時,我們就說這是回調函數。回調函數不是由該函數的實現方直接調用,而是在特定的事件或條件發生時由另外的一方調用的,用于對該事件或條件進行響應。
這段話比較長,也比較繞口。下面我通過一幅圖來說明什么是回調:
假設我們要使用一個排序函數來對數組進行排序,那么在主程序(Main program)中,我們先通過庫,選擇一個庫排序函數(Library function)。但排序算法有很多,有冒泡排序,選擇排序,快速排序,歸并排序。同時,我們也可能需要對特殊的對象進行排序,比如特定的結構體等。庫函數會根據我們的需要選擇一種排序算法,然后調用實現該算法的函數來完成排序工作。這個被調用的排序函數就是回調函數(Callback function)。
結合這幅圖和上面對回調函數的解釋,我們可以發現,要實現回調函數,最關鍵的一點就是要將函數的指針傳遞給一個函數(上圖中是庫函數),然后這個函數就可以通過這個指針來調用回調函數了。注意,回調函數并不是C語言特有的,幾乎任何語言都有回調函數。在C語言中,我們通過使用函數指針來實現回調函數。
我的理解是:把一段可執行的代碼像參數傳遞那樣傳給其他代碼,而這段代碼會在某個時刻被調用執行,這就叫做回調。
如果代碼立即被執行就稱為同步回調,如果過后再執行,則稱之為異步回調。
回調函數就是一個通過函數指針調用的函數。如果你把函數的指針(地址)作為參數傳遞給另一個函數,當這個指針被用來調用其所指向的函數時,我們就說這是回調函數。
回調函數不是由該函數的實現方直接調用,而是在特定的事件或條件發生時由另外的一方調用的,用于對該事件或條件進行響應。
2 為什么要用回調函數?
因為可以把調用者與被調用者分開,所以調用者不關心誰是被調用者。它只需知道存在一個具有特定原型和限制條件的被調用函數。
簡而言之,回調函數就是允許用戶把需要調用的方法的指針作為參數傳遞給一個函數,以便該函數在處理相似事件的時候可以靈活的使用不同的方法。
int?Callback()????///?
回調似乎只是函數間的調用,和普通函數調用沒啥區別。
但仔細看,可以發現兩者之間的一個關鍵的不同:在回調中,主程序把回調函數像參數一樣傳入庫函數。
這樣一來,只要我們改變傳進庫函數的參數,就可以實現不同的功能,這樣有沒有覺得很靈活?并且當庫函數很復雜或者不可見的時候利用回調函數就顯得十分優秀。
3 怎么使用回調函數?
?
int?Callback_1(int?a)???///?
如上述代碼:可以看到,Handle()函數里面的參數是一個指針,在main()函數里調用Handle()函數的時候,給它傳入了函數Callback_1()/Callback_2()/Callback_3()的函數名,這時候的函數名就是對應函數的指針,也就是說,回調函數其實就是函數指針的一種用法。
4.下面是一個四則運算的簡單回調函數例子:
?
#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?=?÷ } /**************************************** ?*?庫函數 ?***************************************/ 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 ",?(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 ",? ????????????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. 回調函數實例(很有用)
一個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模塊聯網項目,可以copy上面的框架,只需要修改回調函數內部的具體實現,或者增加、減少回調函數,就可以很簡潔快速的實現模塊聯網。
審核編輯:湯梓紅
評論
查看更多