在C語言中,函數也是一種類型,可以定義指向函數的指針。我們知道,指針變數的內存單元存放一個地址值,而函數指針存放的就是函數的入口地址(位於.text
段)。下面看一個簡單的例子:
例 23.3. 函數指針
#include <stdio.h> void say_hello(const char *str) { printf("Hello %s\n", str); } int main(void) { void (*f)(const char *) = say_hello; f("Guys"); return 0; }
分析一下變數f
的類型聲明void (*f)(const char *)
,f
首先跟*
號結合在一起,因此是一個指針。(*f)
外面是一個函數原型的格式,參數是const char *
,返回值是void
,所以f
是指向這種函數的指針。而say_hello
的參數是const char *
,返回值是void
,正好是這種函數,因此f
可以指向say_hello
。注意,say_hello
是一種函數類型,而函數類型和數組類型類似,做右值使用時自動轉換成函數指針類型,所以可以直接賦給f
,當然也可以寫成void (*f)(const char *) = &say_hello;
,把函數say_hello
先取地址再賦給f
,就不需要自動類型轉換了。
可以直接通過函數指針調用函數,如上面的f("Guys")
,也可以先用*f
取出它所指的函數類型,再調用函數,即(*f)("Guys")
。可以這麼理解:函數調用運算符()
要求操作數是函數指針,所以f("Guys")
是最直接的寫法,而say_hello("Guys")
或(*f)("Guys")
則是把函數類型自動轉換成函數指針然後做函數調用。
下面再舉幾個例子區分函數類型和函數指針類型。首先定義函數類型F:
typedef int F(void);
這種類型的函數不帶參數,返回值是int
。那麼可以這樣聲明f
和g
:
F f, g;
相當於聲明:
int f(void); int g(void);
下面這個函數聲明是錯誤的:
F h(void);
因為函數可以返回void
類型、標量類型、結構體、聯合體,但不能返回函數類型,也不能返回數組類型。而下面這個函數聲明是正確的:
F *e(void);
函數e
返回一個F *
類型的函數指針。如果給e
多套幾層括號仍然表示同樣的意思:
F *((e))(void);
但如果把*
號也套在括號裡就不一樣了:
int (*fp)(void);
這樣聲明了一個函數指針,而不是聲明一個函數。fp
也可以這樣聲明:
F *fp;
通過函數指針調用函數和直接調用函數相比有什麼好處呢?我們研究一個例子。回顧第 3 節 “數據類型標誌”的習題1,由於結構體中多了一個類型欄位,需要重新實現real_part
、img_part
、magnitude
、angle
這些函數,你當時是怎麼實現的?大概是這樣吧:
double real_part(struct complex_struct z) { if (z.t == RECTANGULAR) return z.a; else return z.a * cos(z.b); }
現在類型欄位有兩種取值,RECTANGULAR
和POLAR
,每個函數都要if ... else ...
,如果類型欄位有三種取值呢?每個函數都要if ... else if ... else
,或者switch ... case ...
。這樣維護代碼是不夠理想的,現在我用函數指針給出一種實現:
double rect_real_part(struct complex_struct z) { return z.a; } double rect_img_part(struct complex_struct z) { return z.b; } double rect_magnitude(struct complex_struct z) { return sqrt(z.a * z.a + z.b * z.b); } double rect_angle(struct complex_struct z) { double PI = acos(-1.0); if (z.a > 0) return atan(z.b / z.a); else return atan(z.b / z.a) + PI; } double pol_real_part(struct complex_struct z) { return z.a * cos(z.b); } double pol_img_part(struct complex_struct z) { return z.a * sin(z.b); } double pol_magnitude(struct complex_struct z) { return z.a; } double pol_angle(struct complex_struct z) { return z.b; } double (*real_part_tbl[])(struct complex_struct) = { rect_real_part, pol_real_part }; double (*img_part_tbl[])(struct complex_struct) = { rect_img_part, pol_img_part }; double (*magnitude_tbl[])(struct complex_struct) = { rect_magnitude, pol_magnitude }; double (*angle_tbl[])(struct complex_struct) = { rect_angle, pol_angle }; #define real_part(z) real_part_tbl[z.t](z) #define img_part(z) img_part_tbl[z.t](z) #define magnitude(z) magnitude_tbl[z.t](z) #define angle(z) angle_tbl[z.t](z)
當調用real_part(z)
時,用類型欄位z.t
做索引,從指針數組real_part_tbl
中取出相應的函數指針來調用,也可以達到if ... else ...
的效果,但相比之下這種實現更好,每個函數都只做一件事情,而不必用if ... else ...
兼顧好幾件事情,比如rect_real_part
和pol_real_part
各做各的,互相獨立,而不必把它們的代碼都耦合到一個函數中。“低耦合,高內聚”(Low Coupling, High Cohesion)是程序設計的一條基本原則,這樣可以更好地復用現有代碼,使代碼更容易維護。如果類型欄位z.t
又多了一種取值,只需要添加一組新的函數,修改函數指針數組,原有的函數仍然可以不加改動地復用。