《征服 C 指针》摘录4:函数 与 指针

时间:2021-06-02 16:29:42

一、指向函数的指针

函数名可以在表达式中被解读成“指向函数的指针”,因此,正如代码清单 2-2 的实验那样,写成 func 就可以取得指向函数的指针。

“指向函数的指针”本质上也是指针(地址),所以可以将它赋给指针型变量。

比如有下面的函数原型:

int func(double d);

保存指向此函数的指针的变量的声明如下:

int (*func_p)(double);

然后写成下面这样,就可以通过 func_p 调用 func,

int (*func_p)(double);  // 声明
func_p = func; // 将func 赋给func_p
func_p(0.5); // 此时,func_p 等同于func

将“指向函数的指针”保存在变量中的技术经常被运用在如下场合:

  • GUI 中的按钮控件记忆“当自身被按下的时候需要调用的函数”
  • 根据“指向函数的指针的数组”对处理进行分配

后者的“指向函数的指针的数组”,像下面这样使用:

int (*func_table[])(double) = {
func0,
func1,
func2,
func3,
};

func_table[i](0.5);  // 调用func_table[i]的函数,参数为0.5

使用上面的写法,不用写很长的 switch case,只需通过 i 的值就可以对处理进行分配。

哦?不明白为什么?

确实,像

int (*func_p)(double);  // 指向函数的指针

还有,

int (*func_table[])(double);  // 指向函数的指针的数组

这样的声明,是不能用普通的方法来读的。

关于这种声明的解读方式,会在后面进行说明。

二、关于指向函数的指针引起的混乱

正如上面说明的那样,对于 C 语言,表达式中的函数名可以被解读成“指向函数的指针”

在信号处理、事件驱动的程序中,这种特性往往以回调函数的形式被使用。

/*如果发生SIGSEGV(Segmentation falut),回调函数segv_handler */
signal(SIGSEGV, segv_handler);

可是,如果基于之前说明过的 C 语言声明规则,int func()这样的声明会被解释为“返回 int的函数”,如果 函数名在表达式中,只是取出 func,则解释成“指向返回 int 函数的指针”,是不是感觉很怪异?如果一定要使用指向函数的指针,必须要写成&func。

对于上面信号处理的函数,写成

signal(SIGSEGV, &segv_handler);

这样,实际上也能顺利地执行

相反,像

void (*func_p)();

这样,变量 func_p 声明为指向函数的指针,进行函数调用的时候,可以写成

func_p();

但是像 int func()这种声明,都是用 func()这样的方式进行调用的,从对称性的角度考虑,对于 void (*func_p)(),必须要写成

(*func_p)();* 

* 早期的 C 语言中,好像也只能这么写……

这样也是能毫无问题地执行的。

是不是感觉 C 语言的关于指向函数的指针的语法比较混乱?

混乱产生的原因就是:“表达式中 的函数可以解读成‘指向函数的指针’”这个意图不明的规则(难道就是为了和数组保持一致?)。

为了照顾到这种混乱,ANSI C 标准对语法做了以下例外的规定:

  • 表达式中的函数自动转换成“指向函数的指针”。但是,当函数是地址运算符&或者 sizeof 运算符的操作数时,表达式中的函数名不能变换成“指向函数的指针”。
  • 函数调用运算符()的操作数不是“函数”,而是“函数的指针”。

如果对“指向函数的指针”使用解引用*,它暂时会成为函数,但是因为在表达式中,所以它会被瞬间地变回成“指向函数的指针”。

结论就是,即使对“指向函数的指针”使用*运算符,也是对牛弹琴,因为此时的运算符*发挥不了任何作用。

因此,下面的语句也是能顺利执行的,

(**********printf)("hello, world\n");  // 无论如何,*就是什么也没做

延伸阅读:

《征服 C 指针》摘录1:什么是空指针?区分 NULL、0 和 '\0'

《征服 C 指针》摘录2:C变量的 作用域 和 生命周期(存储期)

《征服 C 指针》摘录3:数组 与 指针

《征服 C 指针》摘录4:函数 与 指针

《征服 C 指针》摘录5:函数形参 和 空的下标运算符[]

《征服 C 指针》摘录6:解读 C 的声明

《征服 C 指针》摘录7:练习——挑战那些复杂的声明