9.10 指针与函数

9.10.1 函数名是指针

如同数组名是指针一样,在C语言中,函数名也是指针。当然这种指针也必然是一种“常量”,因为在内存中“移动”变量尚不可能,更不必说“移动”构成函数的一群机器指令了。

作为一种指针,首先要明白它的类型。描述函数名这样的指针非常容易,只要把函数声明中的函数名换成“(*)”就可以了。比如某个函数的函数声明为:

9.10 指针与函数 - 图1

那么,“qiuhe”这个函数名的类型是:

9.10 指针与函数 - 图2

这种类型写法对于我们来说除了具有形式上的意义,并没有告诉我们更多的关于这种类型的含义,除非我们知道这种类型本身占据多少内存空间以及这个指针指向什么。

没有理由说这种类型和前面数据指针所需要的内存空间相同,这是必须在具体环境中才能确定的事情。但在这里我们不妨假设这种指针需要4个字节的内存空间,无论实际情况是否如此,对后面的讨论都没有什么影响。

笼统地说,这种指针指向函数也没有任何意义,因为我们并不清楚也不可能清楚函数在内存中是什么样子。毕竟函数不同于数据。数据具有统一的类型和构造规则,相同类型的数据具有相同大小的连续存储空间。而函数在内存中的存储空间我们是不可能加以考察的,甚至我们都不清楚函数占据的内存空间是否连续,可以肯定的是各个函数占据的空间原则上是不相同的。

这恰恰是函数与数据这种连续且具有确定内存长度的对象(Object)最大的区别。这个区别,在后面我们可以看到,决定了指向函数的指针与指向数据的指针之间巨大的差异。

函数与数据的相同之处是它们都占据内存空间,而它们各自所占据的空间都是从各自的某个内存单元开始的,这是可以有指向函数指针的基础,毕竟指针的值是地址。函数名的值也是函数经过编译之后在内存中的映像的起始内存单元的编号或地址。

如图9-29所示的部分内容是不真实的,只是为了帮助理解,把函数“比拟”成了一种类似数组对象的东西。后面将会说明哪些是能被C语言证实的,而哪些是虚构的。

9.10 指针与函数 - 图3

图9-29 函数名的意义

函数占据内存空间,这是确定无疑的。但不清楚占据的是否为连续空间,也不可能清楚这块空间的大小。但在图中画成了一块连续的内存空间来表示“int qiuhe()”函数在内存中的实体,这是虚构的,但是只要我们不从这种虚构中引申出错误的结论,而只是为了帮助理解指向函数的指针这种数据类型,应该是能够获得大家的理解和宽恕的。

函数占据的内存空间有个起点,这个起点处的内存单元有一个编号,也就是所谓的“入口地址”,这是确定的。图中“qiuhe”这个函数名的实线箭头表示的是这一点。

图中,虚线箭头表示“qiuhe”这个指针指向函数所占据的这块内存的整体,这是虚拟的想象,C语言并没有承认这是事实;方向向上的“}”用来表示“qiuhe”这个函数名也代表函数所占据的内存实体,这是作者虚构的,C语言没有这样说过。这样做的目的是把函数名比拟成数组名(8),期待我们能自然地接受函数名的某些性质。

9.10.2 指向函数指针的性质

前面搭建的那个半真半假的模型的本质如下。

■ qiuhe这个函数名是指向qiuhe()这个函数的指针。

■ qiuhe这个函数名也代表qiuhe()函数所占据的内存实体。

第一点没有人会否认,只不过C语言没有明确“指向函数”的具体含义。而我虚构了一个“指向函数”的具体含义,我确信这对于编程没有什么危险,因为编程不会用到这点,只可能用到后面推导出的和C语言一致的结论。第二点则完全是我虚构的,是为了更直接地导出下面的推理和正确的结论。

由于“qiuhe”是指向函数的指针,所以“*qiuhe”就是函数的实体;而函数的实体又可以用函数名“qiuhe”表示,所以结论是

9.10 指针与函数 - 图4

同理,由于“qiuhe”代表函数的内存实体,所以“&qiuhe”就是指向这个函数的指针;而指向这个函数的指针又是“qiuhe”这个函数名本身,所以可以得到另一个结论

9.10 指针与函数 - 图5

这样我们就用一个半真半假的模型,自然地推导出了C语言生硬且直接给出的函数名最重要的性质

9.10 指针与函数 - 图6

9.10.3 指向函数指针的运算

定义与函数名类型相同的指针变量

如前所述,函数名是指针常量。也可以定义这种类型的变量。仍以“int qiuhe(int, int);”这个函数原型为例,定义与函数名“qiuhe”类型相同的指针变量的方法是:

9.10 指针与函数 - 图7

当然也可以构造这种类型的数组:

9.10 指针与函数 - 图8

这个定义有些复杂,这里不准备详细解读,后面将专门介绍复杂定义的解读问题。

赋值运算

由于“p”的类型与函数名“qiuhe”的类型一致,所以可以进行赋值运算:

9.10 指针与函数 - 图9

这时称指针“p”指向了“qiuhe()”函数。

类似的,指向函数的指针也可以作为函数的实参把值传给相同类型的形参。

函数调用运算

函数名可以进行函数调用运算是不言而喻的,与其相同类型的指针变量也可以进行这种运算。由于函数名这种指针具有函数名==*函数名==&函数名这样的性质,所以很容易地可以得到结论——下面几种函数调用方式是完全等价的:

9.10 指针与函数 - 图10

其中“(qiuhe)”、“(&qiuhe)”、“(p)”的括号是必需的,因为“*”、“&”的优先级低于函数调用运算的优先级。

由于函数名这种指针具有函数名==*函数名==&函数名这样的性质,甚至可以得出更惊人的推论:

9.10 指针与函数 - 图11

除了赋值、函数调用以及类型转换,其他的运算对于指向函数的指针没有意义,也是非法的。

指向函数的指针是解决某些复杂问题的一个非常巧妙的手法,它可以使代码更具有表现力、更简洁、更有美感。

9.10.4 例题

例题:编程,在键盘上输入:

9.10 指针与函数 - 图12

这样的表达式,要求程序按照C语言的表达式的规则计算其值。

补充说明如下。

■ 键盘输入格式为ddd…doddd…doddd…d,其中ddd…d表示连续的十进制字符序列,所得到的数值不超过int的表示范围,且表达式求值中和最后的结果也不超过“int”的表示范围。

■ o表示“+”、“_”、“*”、“/”、“%”这5个运算符中的一个。

讨论:由所规定的输入格式,显然可以理解为"%d%c%d%c"并通过调用scanf()函数获取这些数据。之后需要考虑的是两个运算符的优先级问题。根据优先级关系的不同,可以借助switch语句完成运算,这种写法究竟有多烦琐可以自己试写一下。

下面的代码演示了指向函数的指针的用法,并且假定输入没有任何错误。

程序代码9-30

9.10 指针与函数 - 图13

9.10 指针与函数 - 图14

程序运行结果如图9-30所示。

9.10 指针与函数 - 图15

图9-30 指向函数的指针

代码中的函数调用exit(1)的作用是结束程序,并返回一个值“1”给操作系统,告之程序运行的最后状态。