一、基础研究
这里研究的内容是函数指针,需要我们在研究后构造程序来描述函数指针数组的用法和向函数传函数指针的方法。
指针有很多种:整型指针、结构体指针、数组指针等等,它们的本质是它们的值都是一个地址,只不过整形指针的值是一个int型数据的地址,结构体指针的值是一个结构体变量的地址,而这里的函数指针指向的不是一个固定类型数据的地址了,而是一个函数的入口地址。
我们知道int a(char,char);是返回值为int类型,参数为char、char类型的函数a,而书上说int (*a)(char,char);是返回值为int类型、参数为char、char的函数的函数指针变量,要注意这里a是一个函数指针,它存放的是一个地址,它的大小为2字节,而且它是要当指针用而不是当函数用,即它只能赋值、取值、取址、加减、与同类型指针比较大小,而不能传参、返回值。那么这里*a为什么要用括号括起来呢,如果不用会怎样?如果不用括号的话就是int * a(char,char),我们知道其实int * p;也可以看成(int *)p,即指针p是一个int *型的数据,所以int *a(char,char)可以看成是(int *)a(char,char),那么这就是一个参数为char、char型,返回值为int *型指针的函数了。它是一个函数而不是一个指针,不要以为它返回的是指针类型就是函数指针了。查阅资料可知返回指针型变量的函数叫做指针函数。指针函数可以写成int * p(char,char)或者(int *)p(char,char),即返回值的类型可以不加括号,但是函数指针必须写成int (* p)(char,char),也就是*p一定要加括号。
那么函数指针的类型怎么描述呢?整形变量int a的类型是int,函数指针int (*a)(char,char)的类型就是int (*)(char,char)。
下面我们来看一看程序1:
执行结果:
观察程序可以发现,程序打印出了main函数和f函数的入口地址,将f的地址赋给了三个不同类型的变量并将它们打印出来,然后以两种不同的方式调用函数f并将返回值打印出来。观察程序可以发现以下问题:
(1)p=f在这里是将函数的入口地址赋给指针p,这说明函数的名字就代表它的入口地址,这不像我们对一些变量的地址进行操作要用到取址符&,而变量名则代表变量的值,比如int a,a表示a存储的值而&a才表示它的地址,但是数组的使用和上式很相似,数组名代表的是数组的首地址而不是第一个元素的值。如下面程序所验证的:
(2)由a=p(1,2);语句,我们发现如果把函数f的入口地址赋给指针p,那么可以用指针p来代替f调用f函数,这是为什么呢?我们之前的研究指出函数名存储的是函数的入口地址,从汇编角度看,要调用函数,先将函数的参数入栈,再跳转到函数名所表示的地址并运行函数里的语句。这里的函数名起的作用就是表示函数的地址,让函数被调用的时候能够跳转到函数的地址,那么我们将函数地址赋给函数指针之后,函数指针的值就是函数的入口地址,那么函数指针完全可以代替函数名来表示函数的地址。那么既然可以用函数名表示函数的地址,那么为什么要再用函数指针呢,函数指针有什么意义呢?这个问题我们后面再研究。
因为我们之前将函数名的值强制转换成int型赋给了变量b,所以我们也可以用b来表示函数的地址,只不过要先将它再转换成函数指针类型,这样才能表示一个地址。
(3)我们发现函数f的返回值是int型,而它的返回值等于a+b,但是a和b都是char型变量,为什么两个char型变量相加可以返回一个int型变量呢?查阅资料,发现函数在返回时如果要返回的值的类型与函数的类型不同,那么会使用强制类型转换将要返回的值的类型强制转换成函数的类型再返回,即使函数的类型比要返回的值的类型小也是这样,如下图:
再看程序2:
这个程序输出了main函数的偏移地址还有它的段地址加偏移地址、f函数的偏移地址还有它的段地址加偏移地址、还有p、b、c、a的值。与上一个程序不同的是这里的函数指针p被定义成远指针,这里far要写在括号里*p的前面,这样将f赋给p则p存储的是f的段地址加偏移地址。这里我们可以将c强制转换为函数指针的类型再代替f调用函数,但不能将b强制转换再使用,因为这里b是一个int型的变量,他只存储了偏移地址的数据而没有段地址的数据,如果转换成far指针会出错。
那么再回到开始的问题:怎么构造程序来描述函数指针数组的用法和向函数传函数指针的方法呢?函数指针数组首先是一个数组,数组里的元素是函数指针,也就是每一个元素都是一个地址,指向一个函数入口。所以数组有几个元素,就要有几个函数。而向函数传函数指针,就是把函数指针作为函数的参数,需要在定义和声明时将函数的参数定义为函数指针。程序如下:
这里定义了一个函数指针数组a,并将函数f1、f2、f3的地址分别存放到数组中,之后调用函数f,并用数组将函数f1、f2、f3的地址作为参数提供给f。在函数f里根据传进来的参数对函数f1、f2、f3进行了调用,将f1、f2、f3的返回值相加并返回到main函数,main函数再将f的返回值打印出来。这里我们定义并利用了函数指针数组,向函数里传递了函数指针。
二、扩展研究
1、既然可以用函数名表示函数的地址,那么为什么要再用函数指针呢,函数指针有什么意义呢?
答:可以说一个函数名就相当于一个函数指针,但是只是这一个函数的函数指针,我们使用函数指针,可以随时改变它的值,让它指向不同的函数以方便使用。比如高级语言实现一个下拉菜单,其实就是每个菜单项是一个函数,定义一个函数指针,当用户选择一个选项时,让函数指针指向它对应的函数执行,即实现了相应功能。
2、我们先来看一个题目:有一段程序存储在起始地址为 0的一段内存上,如果我们想要调用这段程序,请问该如何去做?答案是 (*(void (*)( ) )0)( )。很显然我们调用的这个函数是没有参数的,而0是它的地址,所以我们可以把0转换成函数指针,让它指向这个函数,即(void(*))0。但是这只相当于地址0000:0000,那么它就相当于函数名啊,函数不就是(void(*))0()了,为什么答案是 (*(void (*)( ) )0)( )呢?还有int *p表示从以p的值为地址所指向的那个空间里取出大小为int类型的值。那么定义(void(*p))()的话,*p表示取的值的大小是多少?结果查阅资料发现void型指针不能复引用,即*p是错误的用法。
3、函数指针在定义的时候一定要定义函数的参数类型和个数吗?
答:可以不定义。
三、研究总结
我们之前学习指针主要是学习数据类型的指针,这类指针的特点是存储一地址,这个地址存放的是指定的空间,而函数指针指向的是一个函数。但是这里函数指针里的数据类型(如int(*p)(char))表示的是函数的返回值类型,那么程序怎么知道函数有多长,该在内存里取多少数据呢?我觉得是通过函数的返回语句判断函数结束了。
我觉得函数指针容易弄错的地方就是它后面跟了函数的参数类型,导致我们在传参时老想将这些参数传进去,而实际上要传的是一个指针。还有从别的指针的定义可以直接看出指向的空间大小,而函数指针只能看出函数的返回类型。
从宏观来看,函数指针让我们调用函数时也能够直接从内存地址调用了,这充分说明了c语言的*性,我们可以用它写出十分精简的程序,但是这样也容易造成使用出错,我们在使用时要小心。