上一節主要討論了C語言中的函數指針在「運行時」代碼選擇中的應用,這其實是一個小技巧,僅需在需要切換代碼的時候重新確定函數指針的指向,之後的代碼就幾乎不用動了。
粗略來說,只需一次 if 判斷,就可以將所有C語言代碼涉及到的代碼切換完成。這樣的代碼風格顯然有利於程式設計師維護,也能提升C語言程序的運行效率。
事實上,C語言函數指針的用途遠不止於此
在本專欄更早的章節中,我們曾討論C語言函數的參數也可以是指針型的,「指針型」中的指針當然包括函數指針,也就是說,C語言函數的參數可以也是一個「函數」,只不過這個「函數」是通過函數指針傳遞的。請看下面這個例子:
#include <stdio.h>void myprint(){printf("myprint\n");}void fun( void(*f)() ){ f();}int main(){ fun(&myprint);return0;}
從上面這段C語言代碼中,可以看出 fun() 函數接收一個參數,該參數是一個函數指針,指向返回值為空的函數。在 main() 中調用 fun() 時,將 myprint() 傳遞給它了。編譯並執行這段C語言代碼,得到如下輸出:
# gcc t.c# ./a.out myprint在 fun() 中調用的 myprint() 就是所謂的「回調函數」。顯然,回調函數就是一個通過函數指針調用的函數,回調函數不是由實現方直接調用,而是通過函數指針,在特定條件發生時,由另外一方調用,用於對該條件響應。
上面的例子很簡單,fun() 無條件調用 f 了,但是應明白,如果需要的話,程式設計師能夠輕易為 f 的添加調用條件。
容易產生迷惑的點
在上述例子中,main() 函數中的 fun() 在接收函數指針時,fun(&myprint) 中的 & 符號可以不寫。而且有些程式設計師在調用 f 時,為了顯式的說明它是一個函數指針,常常寫作:
(*f)();但是也有程式設計師像本例一樣,將函數指針當作普通函數使用:
f();這似乎很不可思議,但是這些寫法都可以正常工作,怎麼回事呢?C語言的函數指針怎麼會如此混亂不堪呢?
其實這主要是因為在C語言中,函數名,&函數名,以及 * 函數名在內存中的值是相等的,編寫下面這樣的C語言代碼:
printf("%p, %p, %p\n", &myprint, myprint, *myprint);編譯並執行,得到如下輸出:
0x40057d, 0x40057d, 0x40057d顯然,三者是相等的。所以究竟使用何種方式,主要取決於程式設計師自己的習慣了。
回調函數的意義
從上例可以看出,fun() 並不關心自己接收到的函數 f 以何種方式提供何種功能,這樣一來,fun() 的一些功能就很靈活了。現在設想這種情況:
fun() 在處理數據時,需要用到排序算法,但是 fun() 的主要功能並不是排序,所以不打算在 fun() 中嵌入排序相關的C語言代碼。
在這種情況下,回調函數就比較有用了,程式設計師可以在別處實現排序算法函數,再將該函數的地址以函數指針參數的形式傳遞給 fun() 就可以了。
程式設計師甚至可以在別處實現若干個不同的排序算法函數(如冒泡排序、快速排序、shell排序、shake排序等等),根據實際情況,決定使用何種排序。
為什麼不直接調用函數呢?感到迷惑的讀者可以再看看上一節。
回調還可用於通知機制。例如,有時要在A程序中設置一個計時器,每到一定時間,A程序會得到相應的通知,但通知機制的實現者對A程序一無所知。那麼,就需一個具有特定原型的函數指針進行回調,通知A程序事件已經發生。
回調函數的參數
上面的例子演示的 myprint() 沒有參數,如果需要給回調函數傳遞參數,該怎麼實現呢?請看下面的C語言代碼:
void myprint(int a, double b){printf("myprint recieve nums: %d, %0.2f\n", a, b);}void fun( void(*f)(), int a, double b ){ f(a, b);}顯然,可以在 fun() 中指定傳遞給 myprint() 的參數。如果需要傳遞給 myprint() 的參數比較多,則可以使用本專欄第21節提到的小技巧:藉助指針和結構體:
struct param{char a;int b;double c; ...char str[128];};void myprint(void *data){struct param *p = (struct param*)data;printf("myprint recieve nums: %d, %0.2f...\n", p->a, p->b);}void fun( void(*f)(), void *data ){ f(data);}
顯然,藉助於C語言的指針和結構體語法,程式設計師可以僅使用一個參數,傳遞任意多的參數。事實上,一些比較成熟的庫函數也是這麼幹的,例如:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);小結
本節主要討論了C語言中的回調函數,應該能夠發現,其實回調函數也是藉助於C語言的指針語法實現的。
另外,在文章最後還討論了回調函數傳遞參數的方法,可以看出,藉助指針和結構體語法,程式設計師能夠輕易的傳遞任意多的複雜參數。歸根結底,這些重要內容都離不開C語言中的指針,所以說指針是C語言的靈魂一點也不誇張。
歡迎在評論區一起討論,質疑。文章都是手打原創,每天最淺顯的介紹C語言、linux等嵌入式開發,喜歡我的文章就關注一波吧,可以看到最新更新和之前的文章哦。