首頁 > 軟體

C語言技巧提升之回撥函數的掌握

2022-12-15 14:02:23

一、函數指標

在講回撥函數之前,我們需要了解函數指標。

我們都知道,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 <stdio.h>
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 = %dnb = %dnmax = %dn", 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 <stdio.h>
#include <stdlib.h>
 
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: %dn",x);
}
void hisFun(int x)
{
    printf("hisFun: %dn",x);
}
void herFun(int x)
{
    printf("herFun: %dn",x);
}

輸出:

4.函數指標作為函數返回型別

有了上面的基礎,要寫出返回型別為函數指標的函數應該不難了,下面這個例子就是返回型別為函數指標的函數:

void (* func5(int, int, float ))(int, int)
{
    ...
}

在這裡, func5 以 (int, int, float) 為引數,其返回型別為 void (*)(int, int) 。

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()    ///< 回撥函數
{
    // TODO
    return 0;
}
int main()     ///<  主函數
{
    // TODO
    Library(Callback);  ///< 庫函數通過函數指標進行回撥
    // TODO
    return 0;
}

回撥似乎只是函數間的呼叫,和普通函數呼叫沒啥區別。

但仔細看,可以發現兩者之間的一個關鍵的不同:在回撥中,主程式把回撥函數像引數一樣傳入庫函數。

這樣一來,只要我們改變傳進庫函數的引數,就可以實現不同的功能,這樣有沒有覺得很靈活?並且當庫函數很複雜或者不可見的時候利用回撥函數就顯得十分優秀。

3.怎麼使用回撥函數

int Callback_1(int a)   ///< 回撥函數1
{
    printf("Hello, this is Callback_1: a = %d ", a);
    return 0;
}
 
int Callback_2(int b)  ///< 回撥函數2
{
    printf("Hello, this is Callback_2: b = %d ", b);
    return 0;
}
 
int Callback_3(int c)   ///< 回撥函數3
{
    printf("Hello, this is Callback_3: c = %d ", c);
    return 0;
}
 
int Handle(int x, int (*Callback)(int)) ///< 注意這裡用到的函數指標定義
{
    Callback(x);
}
 
int main()
{
    Handle(4, Callback_1);
    Handle(5, Callback_2);
    Handle(6, Callback_3);
    return 0;
}

如上述程式碼:可以看到,Handle()函數裡面的引數是一個指標,在main()函數裡呼叫Handle()函數的時候,給它傳入了函數Callback_1()/Callback_2()/Callback_3()的函數名,這時候的函數名就是對應函數的指標,也就是說,回撥函數其實就是函數指標的一種用法。

4.下面是一個四則運算的簡單回撥函數例子

#include <stdio.h>
#include <stdlib.h>
 
/****************************************
 * 函數指標結構體
 ***************************************/
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 = &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 = %fn", (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 = %fn", 
            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上面的框架,只需要修改回撥函數內部的具體實現,或者增加、減少回撥函數,就可以很簡潔快速的實現模組聯網。

到此這篇關於C語言技巧提升之回撥函數的掌握的文章就介紹到這了,更多相關C語言回撥函數內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com