8. 函數類型和函數指針類型

在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。那麼可以這樣聲明fg

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_partimg_partmagnitudeangle這些函數,你當時是怎麼實現的?大概是這樣吧:

double real_part(struct complex_struct z)
{
	if (z.t == RECTANGULAR)
		return z.a;
	else
		return z.a * cos(z.b);
}

現在類型欄位有兩種取值,RECTANGULARPOLAR,每個函數都要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_partpol_real_part各做各的,互相獨立,而不必把它們的代碼都耦合到一個函數中。“低耦合,高內聚”(Low Coupling, High Cohesion)是程序設計的一條基本原則,這樣可以更好地復用現有代碼,使代碼更容易維護。如果類型欄位z.t又多了一種取值,只需要添加一組新的函數,修改函數指針數組,原有的函數仍然可以不加改動地復用。