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 。那么可以这样声明 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 又多了一种取值,只需要添加一组新的函数,修改函数指针数组,原有的函数仍然可以不加改动地复用。