什么是回調(diào)函數(shù)
我們先來看看百度百科是如何定義回調(diào)函數(shù)的:
回調(diào)函數(shù)就是一個通過函數(shù)指針調(diào)用的函數(shù)。如果你把函數(shù)的指針(地址)作為參數(shù)傳遞給另一個函數(shù),當(dāng)這個指針被用來調(diào)用其所指向的函數(shù)時,我們就說這是回調(diào)函數(shù)?;卣{(diào)函數(shù)不是由該函數(shù)的實現(xiàn)方直接調(diào)用,而是在特定的事件或條件發(fā)生時由另外的一方調(diào)用的,用于對該事件或條件進行響應(yīng)。
這段話比較長,也比較繞口。下面我通過一幅圖來說明什么是回調(diào):

假設(shè)我們要使用一個排序函數(shù)來對數(shù)組進行排序,那么在主程序(Main program)中,我們先通過庫,選擇一個庫排序函數(shù)(Library function)。但排序算法有很多,有冒泡排序,選擇排序,快速排序,歸并排序。同時,我們也可能需要對特殊的對象進行排序,比如特定的結(jié)構(gòu)體等。庫函數(shù)會根據(jù)我們的需要選擇一種排序算法,然后調(diào)用實現(xiàn)該算法的函數(shù)來完成排序工作。這個被調(diào)用的排序函數(shù)就是回調(diào)函數(shù)(Callback function)。
結(jié)合這幅圖和上面對回調(diào)函數(shù)的解釋,我們可以發(fā)現(xiàn),要實現(xiàn)回調(diào)函數(shù),最關(guān)鍵的一點就是要將函數(shù)的指針傳遞給一個函數(shù)(上圖中是庫函數(shù)),然后這個函數(shù)就可以通過這個指針來調(diào)用回調(diào)函數(shù)了。注意,回調(diào)函數(shù)并不是C語言特有的,幾乎任何語言都有回調(diào)函數(shù)。在C語言中,我們通過使用函數(shù)指針來實現(xiàn)回調(diào)函數(shù)。那函數(shù)指針是什么?不著急,下面我們就先來看看什么是函數(shù)指針。
什么是函數(shù)指針
函數(shù)指針也是一種指針,只是它指向的不是整型,字符型而是函數(shù)。在C中,每個函數(shù)在編譯后都是存儲在內(nèi)存中,并且每個函數(shù)都有一個入口地址,根據(jù)這個地址,我們便可以訪問并使用這個函數(shù)。函數(shù)指針就是通過指向這個函數(shù)的入口,從而調(diào)用這個函數(shù)。
函數(shù)指針的使用
函數(shù)指針的定義
函數(shù)指針雖然也是指針,但它的定義方式卻和其他指針看上去很不一樣,我們來看看它是如何定義的:
/* 方法1 */
void (*p_func)(int, int, float) = NULL;
/* 方法2 */
typedef void (*tp_func)(int, int, float);
tp_func p_func = NULL;
這兩種方式都是定義了一個指向返回值為 void 類型,參數(shù)為 (int, int, float) 的函數(shù)指針。第二種方法是為了讓函數(shù)指針更容易理解,尤其是在復(fù)雜的環(huán)境下;而對于一般的函數(shù)指針,直接用第一種方法就行了。
如果之前沒見過函數(shù)指針,可能會覺得函數(shù)指針的定義比較怪,為什么不是 void ()(int, int, float) *p_func 而是 void (*p_func)(int, int, float) 這種形式?這個問題我也不知道,也沒必要糾結(jié),花點時間理解下它與普通指針的區(qū)別,實在不行就先記住它的形式。
函數(shù)指針的賦值
在定義完函數(shù)指針后,我們就需要給它賦值了我們有兩種方式對函數(shù)指針進行賦值:
void (*p_func)(int, int, float) = NULL;
p_func = &func1;
p_func = func2;
上面兩種方法都是合法的,對于第二種方法,編譯器會隱式地將 func_2 由 void ()(int, int, float) 類型轉(zhuǎn)換成 void (*)(int, int, float) 類型,因此,這兩種方法都行。想要了解更詳細的說明,可以看看下面這個stackoverflow的鏈接。
使用函數(shù)指針調(diào)用函數(shù)
因為函數(shù)指針也是指針,因此可以使用常規(guī)的帶 * 的方法來調(diào)用函數(shù)。和函數(shù)指針的賦值一樣,我們也可以使用兩種方法:
/* 方法1 */
int val1 = p_func(1,2,3.0);
/* 方法2 */
int val2 = (*p_func)(1,2,3.0);
方法1和我們平時直接調(diào)用函數(shù)是一樣的,方法2則是用了 * 對函數(shù)指針取值,從而實現(xiàn)對函數(shù)的調(diào)用。
將函數(shù)指針作為參數(shù)傳給函數(shù)
函數(shù)指針和普通指針一樣,我們可以將它作為函數(shù)的參數(shù)傳遞給函數(shù),下面我們看看如何實現(xiàn)函數(shù)指針的傳參:
/* func3 將函數(shù)指針 p_func 作為其形參 */
void func3(int a, int b, float c, void (*p_func)(int, int, float))
{
(*p_func)(a, b, c);
}
/* func4 調(diào)用函數(shù)func3 */
void func4()
{
func3(1, 2, 3.0, func_1);
/* 或者 func3(1, 2, 3.0, &func_1); */
}
函數(shù)指針作為函數(shù)返回類型
有了上面的基礎(chǔ),要寫出返回類型為函數(shù)指針的函數(shù)應(yīng)該不難了,下面這個例子就是返回類型為函數(shù)指針的函數(shù):
void (* func5(int, int, float ))(int, int)
{
...
}
在這里, func5 以 (int, int, float) 為參數(shù),其返回類型為 void (*)(int, int) 。在C語言中,變量或者函數(shù)的聲明也是一個大學(xué)問,想要了解更多關(guān)于聲明的話題,可以參考我之前的文章 - C專家編程》讀書筆記(1-3章)。這本書的第三章花了整整一章的內(nèi)容來講解如何讀懂C語言的聲明。
函數(shù)指針數(shù)組
在開始講解回調(diào)函數(shù)前,最后介紹一下函數(shù)指針數(shù)組。既然函數(shù)指針也是指針,那我們就可以用數(shù)組來存放函數(shù)指針。下面我們看一個函數(shù)指針數(shù)組的例子:
/* 方法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];
上面兩種方法都可以用來定義函數(shù)指針數(shù)組,它們定義了一個元素個數(shù)為5,類型是 void (*)(int, int, float) 的函數(shù)指針數(shù)組。
回調(diào)函數(shù)
我們前面談的都是函數(shù)指針,現(xiàn)在我們回到正題,來看看回調(diào)函數(shù)到底是怎樣實現(xiàn)的。下面是一個四則運算的簡單回調(diào)函數(shù)例子:
#include <stdio.h>
#include <stdlib.h>
/****************************************
* 函數(shù)指針結(jié)構(gòu)體
***************************************/
typedef struct _OP {
float (*p_add)(float, float);
float (*p_sub)(float, float);
float (*p_mul)(float, float);
float (*p_div)(float, float);
} OP;
/****************************************
* 加減乘除函數(shù)
***************************************/
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;
}
/****************************************
* 初始化函數(shù)指針
***************************************/
void init_op(OP *op)
{
op->p_add = ADD;
op->p_sub = SUB;
op->p_mul = &MUL;
op->p_div = &DIV;
}
/****************************************
* 庫函數(shù)
***************************************/
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);
/* 直接使用函數(shù)指針調(diào)用函數(shù) */
printf("ADD = %f, SUB = %f, MUL = %f, DIV = %f\n", (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));
/* 調(diào)用回調(diào)函數(shù) */
printf("ADD = %f, SUB = %f, MUL = %f, DIV = %f\n",
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;
}
這個例子有點長,我一步步地來講解如何使用回調(diào)函數(shù)。
第一步
要完成加減乘除,我們需要定義四個函數(shù)分別實現(xiàn)加減乘除的運算功能,這幾個函數(shù)就是:
/****************************************
* 加減乘除函數(shù)
***************************************/
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;
}
第二步
我們需要定義四個函數(shù)指針分別指向這四個函數(shù):
/****************************************
* 函數(shù)指針結(jié)構(gòu)體
***************************************/
typedef struct _OP {
float (*p_add)(float, float);
float (*p_sub)(float, float);
float (*p_mul)(float, float);
float (*p_div)(float, float);
} OP;
/****************************************
* 初始化函數(shù)指針
***************************************/
void init_op(OP *op)
{
op->p_add = ADD;
op->p_sub = SUB;
op->p_mul = &MUL;
op->p_div = &DIV;
}
第三步
我們需要創(chuàng)建一個“庫函數(shù)”,這個函數(shù)以函數(shù)指針為參數(shù),通過它來調(diào)用不同的函數(shù):
/****************************************
* 庫函數(shù)
***************************************/
float add_sub_mul_div(float a, float b, float (*op_func)(float, float))
{
return (*op_func)(a, b);
}
第四步
當(dāng)這幾部都完成后,我們就可以開始調(diào)用回調(diào)函數(shù)了:
/* 調(diào)用回調(diào)函數(shù) */
printf("ADD = %f, SUB = %f, MUL = %f, DIV = %f\n",
add_sub_mul_div(1.3, 2.2, op->p_add),
add_sub_mul_div(1.3, 2.2, op->p_sub),
add_sub_mul_div(1.3, 2.2, MUL),
add_sub_mul_div(1.3, 2.2, DIV));
簡單的四部便可以實現(xiàn)回調(diào)函數(shù)。在這四步中,我們甚至可以省略第二步,直接將函數(shù)名傳入“庫函數(shù)”,比如上面的乘法和除法運算?;卣{(diào)函數(shù)的核心就是函數(shù)指針,只要搞懂了函數(shù)指針再學(xué)回調(diào)函數(shù),那真是手到擒來了。
總結(jié)
本文主要講了如何使用函數(shù)指針和回調(diào)函數(shù)。回調(diào)函數(shù)的核心就是函數(shù)指針,因此我花了大量篇幅講解函數(shù)指針。對于回調(diào)函數(shù)的實現(xiàn),我給出了一個例子,希望這個例子能給你幫助?;卣{(diào)函數(shù)很重要,如果連它都不會,C語言真不算入門了。當(dāng)然了,即使會了它,也不要驕傲,因為C語言還有太多的東西需要我們?nèi)W(xué)習(xí)、實踐。
如果覺得本文對你有幫助,請多多點贊支持,謝謝!