C語言陷阱與技巧25節,常說的「回調函數」是什麼?為何要用它?

2020-12-16 IT劉小虎

上一節主要討論了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等嵌入式開發,喜歡我的文章就關注一波吧,可以看到最新更新和之前的文章哦。

相關焦點

  • C語言函數指針之回調函數
    1 什麼是回調函數?首先什麼是「回調」呢?如果你把函數的指針(地址)作為參數傳遞給另一個函數,當這個指針被用來調用其所指向的函數時,我們就說這是回調函數。回調函數不是由該函數的實現方直接調用,而是在特定的事件或條件發生時由另外的一方調用的,用於對該事件或條件進行響應。2 為什麼要用回調函數?
  • 深入淺出剖析C語言函數指針與回調函數
    回調函數就是一個通過函數指針調用的函數。如果你把函數的指針(地址)作為參數傳遞給另一個函數,當這個指針被用來調用其所指向的函數時,我們就說這是回調函數。回調函數不是由該函數的實現方直接調用,而是在特定的事件或條件發生時由另外的一方調用的,用於對該事件或條件進行響應。
  • 深入淺出剖析C語言函數指針與回調函數(一)
    今天我們要搞明白的一個概念叫回調函數。什麼是回調函數?
  • C語言陷阱與技巧第18節,函數式宏定義的「缺陷」,沒有參數類型檢查...
    在之前的文章裡,我們曾討論C語言程序開發中 define 宏定義的「陷阱」之一就是可能會產生多次「副作用」,這也是C語言中函數式宏定義與真正函數的主要區別之一。顯然,define 宏定義的這種「陷阱」會導致程序存在隱患,而且這種隱患造成的危害不亞於「野指針」。
  • C語言陷阱與技巧31節,都說void*指針是「萬能指針」,它萬能在哪
    在C語言程序開發中,一些比較成熟的庫函數常常會被使用。畢竟,如果手邊就有不錯的「輪子」可以用,沒有程式設計師願意再花費精力憑空造一個輪子出來。因為 void 類型是一個特殊的類型,常被稱作「空類型」,C語言中沒有 void 類型的變量,所以在遇到 void 指針時,編譯器根本不知道如何解釋接下來的內存,甚至編譯器都不知道接下來多少內存屬於它。
  • C/C++編程筆記:如何理解C語言中的回調函數,零基礎也看得懂
    在c語言中,回調是使用函數指針來實現的。後者把g聲明為一個數組,數組的元素類型是一個函數指針,它所指向的函數接受兩個參數,分別是一個整型值和浮點型值,並返回一個整型指針。需要注意的是,簡單聲明一個函數指針並不意味著它馬上就可以使用。和其他指針一樣,對函數指針執行間接訪問之前必須把它初始化為指向某個函數。下面的代碼段說明了一種初始化函數指針的方法。
  • C語言陷阱與技巧第29節,很多程式設計師不知道,C語言也能「繼承」父類
    之前兩節探討了C語言進行「面向對象」編程的可能性。現在已經了解,結合C語言的指針和結構體語法,基本能夠實現對象語法最核心的部分,即成員函數和成員變量。另外,上一節討論了如何利用指針,將公開的成員變量,封裝成 private(私有)變量,由此也可以看出C語言指針語法的強大。
  • 為何C語言函數調用要堆棧,而彙編卻不需要?
    最近,看了很多關於uboot的分析,其中就有說要為C語言的運行,就要準備好堆棧。而在Uboot的start.S彙編代碼中,關於系統初始化,也看到有堆棧指針初始化這個動作。但是,從來只是看到有人說系統初始化要初始化堆棧,即正確給堆棧指針sp賦值,但是卻從來沒有看到有人解釋,為何要初始化堆棧。
  • C++類與回調函數
    ,它的定義回調函數就是一個通過函數指針調用的函數。在C++中的一個重要概念就是類,所以我們一般想讓類的成員函數作為回調函數(如果直接用非類的成員函數作為回調函數,其實就和C語言中的方法一樣),但是想實現這樣的功能,還是存在一些限制的。
  • C語言陷阱與技巧第15節,為什麼每調用一次函數,就需要一次if判斷...
    在C語言程序開發中,調用一個有返回值的函數時,一般要對函數的返回值做判斷,以確定函數是否按照預期執行。如果被調用函數沒有按照預期執行,最好加上相應的錯誤處理代碼,否則最終編譯得到的C語言程序穩定性就不夠好,遇到一點點意外,可能就不會正常工作了。沒有判斷C語言函數的返回值,會有什麼問題?
  • C語言陷阱與技巧第26節,一文弄懂「函數指針數組」,為什麼不直接...
    通過前面兩節的討論,相信讀者已經發現C語言中函數指針的靈活與強大了。毫不誇張的說,C語言的指針語法,有時甚至讓C語言看起來像具備了「新特性」似的。將指針當作一種普通數據類型不過C語言指針的靈活與強大,也導致很多初學者認為指針是一個很難的概念,因此在遇到指針時,常常會覺得「緊張」。
  • C語言陷阱與技巧第17節,有個條件很少發生,還要不要寫if判斷?
    不過,程序是死板的,它需要接收外界(比如人)輸入的指令,才知道要做什麼。例如,你打開瀏覽器,你得點擊我的文章才能看到這些內容。頭條不會在你沒有輸入時,自動的把我的文章顯示到手機。這麼看來,「程序是各種條件判斷,加上相應的邏輯處理」這句話並沒有錯。程序會根據條件的不同,做出不同的響應。
  • C語言陷阱與技巧第2節,使用inline函數可以提升程序效率,但是讓...
    打開 Linux 內核原始碼,會發現內核在定義C語言函數時,有很多都帶有 「inline」關鍵字,請看下圖,那麼這個關鍵字有什麼作用呢?inline 關鍵字的作用在C語言程序開發中,inline 一般用於定義函數,inline 函數也被稱作「內聯函數」,C99 和 GNU C 均支持內聯函數。那麼在C語言中,內聯函數和普通函數有什麼不同呢?
  • C語言陷阱與技巧20節,自定義「編譯時」assert方法,在代碼編譯階段...
    在C語言程序開發中,程式設計師寫代碼時應該考慮的「面面俱到」,這樣才能寫出功能穩定的程序。例如,在實現 open() 函數時,先完成它的功能固然是重要的,但是程式設計師還需要考慮各種「意外」,比如下面這種情況。
  • C語言陷阱與技巧第8節,輸出適當的信息,有利於定位錯誤和異常代碼
    將任務拆分成子模塊後,每個子模塊常常被封裝成一個C語言函數,所以,最後的「堆積木」其實就是調用各個C語言函數。不過,每一個子模塊都有可能得到正常結果,也有可能得到異常結果,這通常用C語言函數的返回值區分。在「堆積木」階段調用各個函數時,應該根據被調用函數的返回值做不同的處理。
  • ...函數式宏定義不能用普通函數代替嗎?為什麼要使用do{}while(0...
    不過,C語言中的宏定義不提供參數類型檢查的確也是一個缺點,它可能會導致程序的不安全,讀者不應忽視這一點。因此如果不是必須要使用 define 宏定義才能解決問題,應該儘可能的使用函數,若是希望能夠得到較高效率的代碼,可以使用 inline 函數。
  • 深度剖析C語言的main函數
    g++3.2 中如果 main 函數的返回值不是 int 類型,就根本通不過編譯。而 gcc3.2 則會發出警告。所以,為了程序擁有很好的可移植性,一定要用 int main ()。/a.out && echo "hello world" #helloc 語言hello world可以看出,正如我們所期望的一樣,main函數返回0,代表函數正常退出,執行成功;返回非0,代表函數出先異常,執行失敗。首先說明的是,可能有些人認為main函數是不可傳入參數的,但是實際上這是錯誤的。
  • C語言陷阱與技巧第12節,重要數據怎麼保存?如何判斷數據是否損壞?
    C語言中的結構體是非常有用的複合數據類型,正是有了結構體,C語言在描述複雜問題時才能夠得心應手。事實上,當初 Dennis Ritchie 開發C語言用於替換 B 語言,其中一個主要原因就是 B 語言不支持「結構體」式數據結構。
  • 詳解C語言gets()函數與它的替代者fgets()函數
    在c語言中讀取字符串有多種方法,比如scanf() 配合%s使用,但是這種方法只能獲取一個單詞,即遇到空格等空字符就會返回。如果要讀取一行字符串,比如:I love BIT這種情況,scanf()就無能為力了。這時我們最先想到的是用gets()讀取.gets()函數從標準輸入(鍵盤)讀入一行數據,所謂讀取一行,就是遇到換行符就返回。
  • C語言程序中,有些函數的參數是結構體指針型,為什麼要這麼用?
    在C語言程序開發中,遇到複雜問題需要描述時,最常使用的就是結構體了。事實上,如果某個函數的參數比較多,並且這些參數被使用的頻率比較高,為了C語言代碼的簡潔,也常將這些參數封裝為結構體。為了C語言代碼的簡潔「重複的C語言代碼」如果函數的參數比較多,很容易產生「重複C語言代碼」,例如:int get_video