1. 程式人生 > >C語言學習及應用筆記之七:C語言中的回撥函式及使用方式

C語言學習及應用筆記之七:C語言中的回撥函式及使用方式

  我們在使用C語言實現相對複雜的軟體開發時,經常會碰到使用回撥函式的問題。但是回撥函式的理解和使用卻不是一件簡單的事,在本篇我們根據我們個人的理解和應用經驗對回撥函式做簡要的分析。

1、什麼是回撥函式

  既然談到了回撥函式,首先我們就要搞清楚什麼是回撥函式。在討論回撥函式之前,我們需要說明另一個概念,那就是函式指標。什麼是函式指標呢?說的淺顯一點,函式指標就是指向函式的指標,說白了也是一種指標,只是它指向的不是整型,字元型等資料量,而是指向函式。在C中,每個函式在編譯後都是儲存在記憶體中,並且每個函式都有一個入口地址,根據這個地址,我們便可以訪問並使用這個函式。函式指標就是指向這個入口

地址,從而呼叫這個函式。

  同樣回撥函式就是一個通過函式指標呼叫的函式。如果我們把函式的指標(指向函式入口地址)作為引數傳遞給另一個函式,而接收這個引數的函式在其執行過程中,反過來使用這個指標呼叫其所指向的函式,我們就把這個被通過函式指標呼叫的函式稱之為回撥函式。

  從上述描述我們可以知道,回撥函式有別於一般意義上的函式呼叫方式。它一般不是由該函式的實現方直接呼叫,而是由已經存在的其它物件間接呼叫它。而且回撥函式的呼叫是呼叫方所需要的,但是其具體實現卻是非常靈活的,我們可以根據需要來實現它,只要呼叫的格式相符,我們不需要去考慮呼叫他的物件的具體內容。

2、為何使用回撥函式

  前面我們簡單介紹了回撥函式,那我們為什麼需要使用回撥函式呢?既然是用它,當然是有使用的理由。接下來我們簡單的討論一下使用回撥函式的優勢所在。

  首先,可以使上層的應用更完整,但又不需要考慮底層的實現細節。比如我們設計了一個通訊應用,但在設計時我並不能確定底層介面,或者說不想侷限於某一介面。那麼我們可以將介面部分的實現留在具體使用中,所以採用回撥函式的方式就非常方便。

  其次,可以使應用更加靈活,這是顯而易見的。比如我們設計一個通訊協議棧,這個協議棧在什麼平臺使用並不侷限,我們使用回撥的方式具體實現平臺相關部分,而協議棧的核心這可以使用於多種平臺。

  再者,可以把呼叫者與被呼叫者分開,這樣呼叫者不關心誰是被呼叫者,也不關心他的具體實現。使得軟體的設計更加獨立,方便與協作或者移植。其實細說起來還有很多,在此僅列舉上述幾點。

3、如何使用回撥函式

  我們已經簡單的介紹了什麼事回撥函式以及為什麼要使用它,接下來我們說說怎麼使用它。對於使用方式千差萬別,而且每個使用者都有相應的心得,在這裡我們之宗解一下我們平時常用的幾種方式。

3.1、以函式引數的形式使用

  在大多數情況下,我們可能都是將函式指標作為引數傳遞給呼叫者來實現回撥。比如我們宣告如下函式:

  void function1int var1int var2

  void function2void *fcintint),float aint b

  呼叫時咋使用function2function1ab)就可以了。當然還有另一個函式與function1的宣告形式一致,也一樣可以做為引數傳遞給function2函式。

  這種方式最好理解,而且函式名不受限制,只要宣告形式一致就可以了。我們在外設驅動的呼叫上會使用這一形式。

3.2、以弱化定義的方式使用

  所謂弱化函式就是呼叫者以_weak定義一個沒有操作或者預設操作的函式,該函式允許定義與其名稱和形式完全一樣的函式。若使用者重新定義了該函式則會呼叫新函式,否則使用_weak修飾的預設函式。在STM32HAL庫中使用了很多這樣的函式,比如各種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、總結

  此篇我們介紹了回撥函式及其使用方式,但我們所掌握的不過冰山之一角。並且具體怎麼使用它是一個見仁見智的論題,用好了自然是給程式增色,但若是隨意使用反倒遊客能會有問題。總而言之,回撥函式是一種靈活而有強大的功能,但最終的效果還要看使用者。

歡迎關注: