C# · 12月 20, 2021

指向函数的指针–转

5.1.2 指向函数的指针

C语言通过&和*操作符来操作数据的地址,但它并没有提供一个用一般的方式来操作代码的地址。然而,C语言并没有完全切断程序员操作代码地址的可能,它提供了一些”受限制的”方式来操作代码的地址。之所以说这些方式是”受限制的”,那是因为这些方式并不像操作数据地址那样自由和灵活。

在C语言中,指针变量也可以指向一个函数。我们已经知道代码也是有地址的,一个函数在编译时会被分配给一个入口地址,这个入口地址就是该函数中第一条指令的地址,这就是该函数的指针。当调用一个函数时除了通过函数名来调用以外,还可以通过指向该函数的指针变量来调用。一个指向函数的指针其初始值不能为空,因为它在使用之前必须被赋予一个真实的函数地址。指向函数的指针变量的一般定义形式如下(其中的函数类型是指函数返回值的类型):

请看下面这段示例代码,它使用普通的函数名方式来实现函数的调用,此函数用于实现矩形法求解

 函数定积分的功能。

  {           length = 0.000001;          (x < b)              sum += x*x*0.000001;              x+=0.000001;   }   main()           result = 0.0;          result = func1(0.0, 1.0);          printf(, result);  

现在我们改写上面的代码,使用一个指向函数的指针变量来调用函数。

  {           length = 0.000001;          (x < b)            }           sum;   {           p = func1;   } 

关于上面的代码,有如下几点需要说明。

①语句”double (*p)(double,double);”定义p是一个指向函数的指针变量,此函数的返回值类型为double型。特别地,double (*p) ()并非是指向某一固定函数的,它仅仅表示定义了这样一个类型的变量,在程序中可以将不同的函数地址赋给它,由此它所指向的函数就会随之变化。

②(*p)两侧的括号不能省略,这样的语法意味着p先与*结合,是一个指针变量;再与后面的括号()结合,表示此指针变量指向函数而非变量。如果将*p两侧的括号去掉,则变成double *p(),这样表示的意思是函数p()的返回值类型是一个指向double型变量的指针。本书前面也介绍过因为()的优先级高于*,所以p会先与()结合,由此就声明了一个函数而非指针。

③赋值语句”p = func1;”的意思是将函数func1的入口地址(即函数中首条指令的地址)赋给指针变量p。易见,在给函数指针变量赋值时,只需要给出函数名即可,并不需要给出参数,因此如果将上面的赋值语句改写成”p = func1(0.0,1.0);”则是错误的!但是由于这里仅仅使用了函数名,而不带括号和参数,为了不让编译器将其与变量混淆,必须在使用之前进行声明,表明func1是函数名而非变量名,这样编译时它们才会被当作函数名来处理。

④在使用函数指针时,只需将(*p)替代函数名即可,但是需要在其后的括号里显式地添加实参,即使函数不传递任何参数,该括号也不可省略。

⑤数组名可以代表数组的起始地址(数组中首元素的地址),所以函数名也可以代表函数的入口地址(函数中的首条指令的地址)。但是对于指向函数的指针变量,它只能指向函数的入口处而无法指向函数中某一条具体的指令,因此对于p+n、p++等指针运算对于指向函数的指针是没有意义的。

⑥获得一个函数地址的方法与获得一个变量地址的方法一样,于是前面程序中的语句”p = func1;”也可以写作”p = &func1; “。但是,必须保证函数func1已经在某个地方被声明过了。

指向函数的指针可以获得函数的入口地址,但它并不能像操作数组一样获得函数中每一条指令的地址,这样的操作是相对受限的。但人们不禁要问,这种语法有什么用处呢?函数指针最常用的地方是作为参数传递给其他的函数。指向函数的指针也可以作为参数以实现函数地址的传递,也就是将函数名传递给形式参数。但是我们知道,在某个函数中调用另外一个函数仅仅需要在此函数中直接调用所需的函数就可以了,这是C语言所支持的非常基本的函数调用语法。如此看来,将函数指针作为参数来传递,然后在函数体中使用实在是多此一举、画蛇添足。然而,将函数指针作为参数来使用还是非常有用的,尤其当每次函数所调用的其他函数无法固定时,这就显得尤为重要了。假设有函数fun,在某次执行过程中需要调用函数func1,而下一次就需要调用函数func2,再下一次又可能调用func3。如果使用函数指针,则不必对函数fun进行修改,只需要让其每次调用函数时通过不同的函数名来作为形参传递即可。这种方法极大地增加了函数使用的灵活性,我们可以编写一个通用的函数来实现各种专用的功能,这是符合结构化程序设计思想的方法。

下面通过一个简单的示例程序来说明这种方法的应用。该例子编写了一个求定积分的通用函数,用它可以分别求得3个函数的定积分:。可见,每次需要求定积分的函数并不相同,但是我们可以使用一个通用的函数integral,并通过3个形式参数:上限 b、下限a和指向函数的指针变量fun来显示各自专用的定积分求解函数。其中,函数func1用于求解函数  的定积分,func2用于求解函数 的定积分,func3用于求解函数  的定积分。

  {           length = 0.000001;          (x < b)              sum += x*x*0.000001;              x+=0.000001;   }   {           length = 0.000001;          (x < b)              sum += sin(x)*0.000001;              x+=0.000001;   }   {           length = 0.000001;          (x < b)              sum += exp(sqrt(x))*0.000001;              x+=0.000001;   }   intergal( a,  b,  (*fun)(, ))           result = 0.0;          printf(, result);   {          integral(0.0, 1.0, func1);          integral(0.0, 3.141593, func2);          integral(0.0, func3);  

完成编码后,编译并运行程序。