我們在使用C語言實現相對復雜的軟件開發時,經常會碰到使用回調函數的問題。但是回調函數的理解和使用卻不是一件簡單的事,在本篇我們根據我們個人的理解和應用經驗對回調函數做簡要的分析。
1、什么是回調函數
既然談到了回調函數,首先我們就要搞清楚什么是回調函數。在討論回調函數之前,我們需要說明另一個概念,那就是函數指針。什么是函數指針呢?說的淺顯一點,函數指針就是指向函數的指針,說白了也是一種指針,只是它指向的不是整型,字符型等數據量,而是指向函數。在C中,每個函數在編譯后都是存儲在內存中,並且每個函數都有一個入口地址,根據這個地址,我們便可以訪問並使用這個函數。函數指針就是指向這個入口地址,從而調用這個函數。
同樣回調函數就是一個通過函數指針調用的函數。如果我們把函數的指針(指向函數入口地址)作為參數傳遞給另一個函數,而接收這個參數的函數在其運行過程中,反過來使用這個指針調用其所指向的函數,我們就把這個被通過函數指針調用的函數稱之為回調函數。
從上述描述我們可以知道,回調函數有別於一般意義上的函數調用方式。它一般不是由該函數的實現方直接調用,而是由已經存在的其它對象間接調用它。而且回調函數的調用是調用方所需要的,但是其具體實現卻是非常靈活的,我們可以根據需要來實現它,只要調用的格式相符,我們不需要去考慮調用他的對象的具體內容。
2、為何使用回調函數
前面我們簡單介紹了回調函數,那我們為什么需要使用回調函數呢?既然是用它,當然是有使用的理由。接下來我們簡單的討論一下使用回調函數的優勢所在。
首先,可以使上層的應用更完整,但又不需要考慮底層的實現細節。比如我們設計了一個通訊應用,但在設計時我並不能確定底層接口,或者說不想局限於某一接口。那么我們可以將接口部分的實現留在具體使用中,所以采用回調函數的方式就非常方便。
其次,可以使應用更加靈活,這是顯而易見的。比如我們設計一個通訊協議棧,這個協議棧在什么平台使用並不局限,我們使用回調的方式具體實現平台相關部分,而協議棧的內核這可以使用於多種平台。
再者,可以把調用者與被調用者分開,這樣調用者不關心誰是被調用者,也不關心他的具體實現。使得軟件的設計更加獨立,方便與協作或者移植。其實細說起來還有很多,在此僅列舉上述幾點。
3、如何使用回調函數
我們已經簡單的介紹了什么事回調函數以及為什么要使用它,接下來我們說說怎么使用它。對於使用方式千差萬別,而且每個使用者都有相應的心得,在這里我們之宗解一下我們平時常用的幾種方式。
3.1、以函數參數的形式使用
在大多數情況下,我們可能都是將函數指針作為參數傳遞給調用者來實現回調。比如我們聲明如下函數:
void function1(int var1,int var2)
void function2(void *fc(int,int),float a,int b)
調用時咋使用function2(function1,a,b)就可以了。當然還有另一個函數與function1的聲明形式一致,也一樣可以做為參數傳遞給function2函數。
這種方式最好理解,而且函數名不受限制,只要聲明形式一致就可以了。我們在外設驅動的調用上會使用這一形式。
3.2、以弱化定義的方式使用
所謂弱化函數就是調用者以_weak定義一個沒有操作或者默認操作的函數,該函數允許定義與其名稱和形式完全一樣的函數。若使用者重新定義了該函數則會調用新函數,否則使用_weak修飾的默認函數。在STM32的HAL庫中使用了很多這樣的函數,比如各種msp函數。
首先需要有一個以_weak修飾的函數聲明:
__weak void SetSingleCoil(uint16_t coilAddress,bool coilValue)
而在使用時定義一個與其同名且形式一樣的函數:
void SetSingleCoil(uint16_t coilAddress,bool coilValue),具體個功能有使用者更具需要設定。如上述這個函數就是我們在調用Modbus協議棧時實現的,每次都不一樣,根據需求而定。
這種方式使用雖然方便,但有一個局限就是必須與原函數聲明一致,且只能有一個。
3.3、以函數注冊的方式使用
有時候我們會對一些對象進行封裝,同是將操作函數的函數指針也封裝在內,這樣我們可以在使用對象是直接調用其操作。這以方式組要應用於對一些復雜的外設對象的操作。如:網卡對象等,在WIZnet以及LwIP等協議棧中都是以這種方式將網卡密切相關的特定操作以函數指針的方式封裝於對象中。
當然我們在開發一些外設的驅動時也可以使用這種方式。如我們開發一個外設驅動,該設備即可使用I2C接口也可使用SPI接口,我們要多次使用該設備,但每次,每個人使用那種接口是不確定的,而我們又想復用這部分驅動,但不是每次都改它,就將其作為一個對象封裝起來。
定義一個結構類型,包括包括對象的主要屬性和基本操作接口:
1 /*定義BMP280操作對象*/ 2 3 typedef struct { 4 5 uint8_t chipID; //芯片ID 6 7 struct Bmp280_Calib_Param caliPara; //校准參數 8 9 struct Bmp280_Config config; //配置寄存器 10 11 struct Bmp280_Ctrl_Meas ctrlMeas; //測量控制寄存器 12 13 void (*Read)(uint8_t regAddress,uint8_t *rData,uint16_t rSize); //讀數據操作指針 14 15 void (*Write)(uint8_t regAddress,uint8_t command); //謝數據操作指針 16 17 void (*Delay)(volatile uint32_t nTime); //延時操作指針 18 19 }BMP280Device;
在使用時,我們只需聲明某一特定對象,並注冊相應的函數就可以使用,調用者並不關心具體接口實現。
3.4、以函數指針類型的方式使用
以聲明函數指針類型的方式其實是與函數參數很類式的,也可用於形參聲明,而且更簡潔。但它最主要的優勢在於我們可以使用其處理多個回調函數條件調用的問題。
據比如我們在處理Modbus協議時我們在處理不同功能嗎的消息時,需要采用不同的處理方式,就可以采用這種方式:
定義一個枚舉,同時定義一個函數指針數組:
1 void (*HandleSlaveRespond[])(uint8_t *,uint16_t,uint16_t)= 2 3 {HandleReadCoilStatusRespond, 4 5 HandleReadInputStatusRespond, 6 7 HandleReadHoldingRegisterRespond, 8 9 HandleReadInputRegisterRespond};
這要我們通過功能碼的枚舉來調用不同的回調函數就非常簡潔了:
HandleSlaveRespond[fuctionCode](recievedMessage,startAddress,quantity);
當然,我們只是討論一種方法,因為使用switch語句一樣可以達到效果,但是其代碼量卻是相差很遠。
4、總結
此篇我們介紹了回調函數及其使用方式,但我們所掌握的不過冰山之一角。並且具體怎么使用它是一個見仁見智的論題,用好了自然是給程序增色,但若是隨意使用反倒游客能會有問題。總而言之,回調函數是一種靈活而有強大的功能,但最終的效果還要看使用者。
歡迎關注:

