第 18 讲:指针和函数
前文我们提到了指针的很多用法,比如指针和普通变量的使用,利用指针来指向变量,以间接操作变量的方式来控制程序的行为。我们还讲到了指针和数组的方式,以及把数组传入函数的参数的时候,会退化为指针的形式,因此数组当参数传递到函数里的时候,是以指针形式存在的,所以我们需要单独为其添加一个参数表示数组的长度。我们甚至还讨论了字符串(字符数组)和普通数组的表现方式。
今天我们要谈论一种新型语法,这种语法不多见,所以很少有人接触到,所以比较难。但它依然很有用途。
数组有指针,那么函数呢?
很令人欣喜和疑惑的是,在 C 语言里,函数也具有指针一说。在内存里,如果我们不尝试把代码编译后的二进制串放到内存里,我们就无法调用和执行这些方法。那么唯一能找到它们执行位置的就是通过这些函数的执行地址来确定函数的位置了。既然是地址,就可以存在指针。所以 C 语言为函数也提供了指针的说法。
在前文里,我们已经提到了数组退化为指针作为返回值的一个说法。这种返回指针的函数叫做指针函数,它们并不是多余和难用的,因为需要堆内存这一知识点才能辅助我们更好使用它们,不过因为超过了讲解范围,所以我们没有在上一节内容里提及到非常深入。
今天要提到的是,间接调用函数的机制。这种机制被称为函数指针。
函数指针的用法
还记得函数需要声明吧?声明也被称为定义,所以这些写在开头的内容被称为“函数变量”的定义。它们被广义化称为一个变量,它们满足标识符命名规则,而且也需要先定义后使用。
基本用法的实现流程
比如上述一个简单的执行流程,我们在外面写的 int Add(int, int)
就是函数的定义。当我们定义了它们,我们就可以使用 Add
的函数了,否则,你必须把 Add
放在 main
前面书写。
不过,函数指针的写法格式有点类似于数组指针,它需要把函数名括起来,前面加上指针符号:
这就是一个函数指针。这种指针仅用于指向一个函数:
*
,取出 Adder
Adder
函数指针,来调用指向的函数 Add
,传入两个参数 a
和 b
来得到结果,赋值给 result
这里特别要提及的是,调用和声明函数指针的时候,都需要这个星号。第一个星号(第 8 行)指的是定义语句的星号,表示定义的这个变量是一个函数指针变量;而第二个星号(第 11 行)则指的是调用指向函数(即取出内容)。
在 C 语言里,由于为函数指针赋值操作(int (*Adder)(int, int) = &Add
)和调用指向函数操作((*Adder)(a, b)
)两个操作基本上都是这么书写的,没有其它的形式,所以指针赋值操作里的取地址符可以不需要写,而且调用函数指针指向的函数的操作也可以不写这个取内容符号,即上面第 8 行的代码和第 11 行的代码可以修改变为如下方式:
函数指针构成的数组?
下面我们为了更加熟悉语法,我们来一个难一点的。我们尝试用一个数组来存放一组函数指针。我们之前学过,数组可以存放整数(整数数组)、小数(小数数组)和字符(字符串或字符数组)。
首先考虑如下情况,我们尝试用四个函数来实现四则运算。
int
参数,返回 int
那么,这个数组就是
为什么函数指针数组这么书写呢?这是因为,数组符号
[]
一般都挨在变量名称后,所以这里也不例外。我们在原始的函数指针名后添加数组符号[]
暗示它是数组,这个时候它就可以表示一个函数指针数组了。而且,我们不用考虑函数指针左侧的这个星号*
到底是作用在谁上的。因为这个运算符挨着谁,就是谁,它是单目运算符。
现在,我们为其赋初始值。
for
循环遍历,然后挨个执行和输出。
18 -8 65 0 5
函数指针作参数
在什么场合会用到如此鸡肋和别扭的写法呢?考虑一下。前文提到的 printf
、puts
等等函数都是系统为我们已经提供好了的,我们无需实现,而且无法修改它们的实现。这样的函数既然自带,肯定考虑到了扩展性,即考虑到我们可能会在后期更改或给予一些特殊情况。函数指针就可以解决一部分问题。
如果我们尝试为代码写一个冒泡排序的算法代码,如下所示:
这样就实现了。不过,如果我们考虑扩展性,万一用户要写一个降序执行冒泡排序的算法呢?那岂不是照抄代码,只改掉比较的代码(第 8 行)?
是的,实际上,我们实现仅需要修改第 8 行的比较,把 >
改为 <
就好了。不过很显然的是,这种东西如果非得让用户自行实现一遍,还不如提供一个参数,让它自己去实现比较操作。所以我们可以尝试为这个函数添加一个函数指针作为一个参数,专门让用户自己实现这个比较算法,然后用函数指针来调用自己实现的这个比较就可以了:
仅需要这么修改它,用户层面就可以自己实现和修改比较算法的代码,来实现比较操作了。整体代码如下:
函数指针传参时的函数的声明
最头疼的是,这种函数的声明语句应该如何写。实际上,照着前文数组指针的格式简写就好,把参数名称全去掉,符号该有的还保留就可以了:
你答对了吗?
那么,再来一个麻烦的。我们尝试把刚才的函数指针变量构成的数组作为函数参数传递进去。
答案就是
为什么可以写作 int (*[])(int, int)
或 int (**)(int, int)
呢?这是因为,去掉函数指针的部分 int (*)(int, int)
外,这两种写法就剩下一个 []
和一个指针定义符 *
了,这不就恰好是一个数组的意思吗?所以,这种写法恰好就表示了一个函数指针变量的数组。
快速排序 qsort
函数
在 C 语言里甚至为我们贴心地提供了为数值序列排序的函数 qsort
,不过这个函数用起来非常复杂,需要函数指针。
先来看看使用
这样调用它就可以立马得到排序后的数组,和上面自己实现的冒泡排序的操作基本上一样,不过差别是需要添加的函数指针,有一些奇怪。
这个函数第一个参数传入的是数组名(即数组的首地址),而第二个参数传入的是数组的长度 9。
第三个参数和第四个参数比较麻烦。第三个参数传入的是数组的每一个元素的内存大小,显然这个数组是 int
类型的,所以需要指定这个数组的元素大小是 sizeof(int)
,而第四个参数传入的是比较函数。这里是在上面实现的 cmp
函数,所以这里写 &cmp
或直接写 cmp
。
下面来分析一下自己实现的函数 cmp
到底为什么参数这么奇怪。
void *
类型
const
是一个关键字,表示指向不可修改,所以这里我们先可以忽略掉。那么,void *
是什么类型呢?void *
往往被称为无类型指针,这种类型允许你在传入的时候传入任何指针类型(int *
、char *
等都可以)。不过在使用取值的时候,由于指针无法确定指向的元素类型,所以需要你自己给出强制转换,它就会把这个类型转换为指定的类型的指针。例如
第 3 行将会输出这个 p
指针的地址数值,而第 4 行将在取出地址的前提下,使用 *
间接取出数值,得到 3 这个结果。注意,*(int *)p
和 *((int *)p)
是等价的,你无需为后面这一坨内容添加括号。
那么,我们转回 qsort
函数就可以看到,整个函数的声明是这样的:
从注释里就可以看到,第四个参数里为什么传入的两个参数是 const void *
类型了:因为两个参数都指向元素,而这两个元素的类型我们是无法从第一个参数 void *
类型而确定下来的,所以我们不得不推后到调用时候才能确定,所以这个时候,我们不得不为其设置 void *
类型来表示这个指向的元素是无类型(未知类型)的。
那么,从实现角度上说,cmp
函数里给定的代码是:
由此可以看出,我们如果需要强制得到元素的类型,需要把指针 a
和 b
转换为 int *
类型才可以去使用和取值。然后,我们转换后,把地址数值再使用 *
运算符取出元素数值,将两者相减就可以得到它们俩的大小关系了。当然,如果需要降序排序,只需要把被减数和减数换一下位置,即改为 *(int *)b - *(int *)a
即可。
总的来说,void *
类型是一个可以指向任何类型变量的指针,但需要确定指向元素的数值时,需要自己给定指针的类型,并通过间接取值的运算符 *
来取值:
即强制转换 void *
为其它类型的指针 T *
。
那么 NULL
常量?
前文提到了一个 NULL
常量,这个常量表示为所有指针类型的默认数值,即指向 0 号内存的指针。当我们现在已经学会了 void *
类型后,就可以知道 NULL
的本面目:(void *)0
,它直接把 0 强制转换为一个地址,而这个地址还是无类型的。所以这允许了所有的类型都可以用。所以,NULL
和 (void *)0
等价。
总结
那么至此,我们就把所有关于指针的内容就介绍完了。现在我们作出总结。

这些写法都在函数声明时去掉变量名 p
即可。