8. 函数类型和函数指针类型

在C语言中,函数也是一种类型,可以定义指向函数的指针。我们知道,指针变量的内存单元存放一个地址值,而函数指针存放的就是函数的入口地址(位于.text段)。下面看一个简单的例子:

例 23.3. 函数指针

  1. #include <stdio.h>
  2.  
  3. void say_hello(const char *str)
  4. {
  5. printf("Hello %s\n", str);
  6. }
  7.  
  8. int main(void)
  9. {
  10. void (*f)(const char *) = say_hello;
  11. f("Guys");
  12. return 0;
  13. }

分析一下变量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:

  1. typedef int F(void);

这种类型的函数不带参数,返回值是int。那么可以这样声明fg

  1. F f, g;

相当于声明:

  1. int f(void);
  2. int g(void);

下面这个函数声明是错误的:

  1. F h(void);

因为函数可以返回void类型、标量类型、结构体、联合体,但不能返回函数类型,也不能返回数组类型。而下面这个函数声明是正确的:

  1. F *e(void);

函数e返回一个F *类型的函数指针。如果给e多套几层括号仍然表示同样的意思:

  1. F *((e))(void);

但如果把*号也套在括号里就不一样了:

  1. int (*fp)(void);

这样声明了一个函数指针,而不是声明一个函数。fp也可以这样声明:

  1. F *fp;

通过函数指针调用函数和直接调用函数相比有什么好处呢?我们研究一个例子。回顾第 3 节 “数据类型标志”的习题1,由于结构体中多了一个类型字段,需要重新实现real_partimg_partmagnitudeangle这些函数,你当时是怎么实现的?大概是这样吧:

  1. double real_part(struct complex_struct z)
  2. {
  3. if (z.t == RECTANGULAR)
  4. return z.a;
  5. else
  6. return z.a * cos(z.b);
  7. }

现在类型字段有两种取值,RECTANGULARPOLAR,每个函数都要if ... else ...,如果类型字段有三种取值呢?每个函数都要if ... else if ... else,或者switch ... case ...。这样维护代码是不够理想的,现在我用函数指针给出一种实现:

  1. double rect_real_part(struct complex_struct z)
  2. {
  3. return z.a;
  4. }
  5.  
  6. double rect_img_part(struct complex_struct z)
  7. {
  8. return z.b;
  9. }
  10.  
  11. double rect_magnitude(struct complex_struct z)
  12. {
  13. return sqrt(z.a * z.a + z.b * z.b);
  14. }
  15.  
  16. double rect_angle(struct complex_struct z)
  17. {
  18. double PI = acos(-1.0);
  19.  
  20. if (z.a > 0)
  21. return atan(z.b / z.a);
  22. else
  23. return atan(z.b / z.a) + PI;
  24. }
  25.  
  26. double pol_real_part(struct complex_struct z)
  27. {
  28. return z.a * cos(z.b);
  29. }
  30.  
  31. double pol_img_part(struct complex_struct z)
  32. {
  33. return z.a * sin(z.b);
  34. }
  35.  
  36. double pol_magnitude(struct complex_struct z)
  37. {
  38. return z.a;
  39. }
  40.  
  41. double pol_angle(struct complex_struct z)
  42. {
  43. return z.b;
  44. }
  45.  
  46. double (*real_part_tbl[])(struct complex_struct) = { rect_real_part, pol_real_part };
  47. double (*img_part_tbl[])(struct complex_struct) = { rect_img_part, pol_img_part };
  48. double (*magnitude_tbl[])(struct complex_struct) = { rect_magnitude, pol_magnitude };
  49. double (*angle_tbl[])(struct complex_struct) = { rect_angle, pol_angle };
  50.  
  51. #define real_part(z) real_part_tbl[z.t](z)
  52. #define img_part(z) img_part_tbl[z.t](z)
  53. #define magnitude(z) magnitude_tbl[z.t](z)
  54. #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又多了一种取值,只需要添加一组新的函数,修改函数指针数组,原有的函数仍然可以不加改动地复用。