返回介绍

7.10 函数指针

发布于 2024-10-08 23:14:06 字数 9556 浏览 0 评论 0 收藏 0

如果未提到函数指针,则对 C 或 C++函数的讨论将是不完整的。我们将大致介绍一下这个主题,将完整的介绍留给更高级的图书。

与数据项相似,函数也有地址。函数的地址是存储其机器语言代码的内存的开始地址。通常,这些地址对用户而言,既不重要,也没有什么用处,但对程序而言,却很有用。例如,可以编写将另一个函数的地址作为参数的函数。这样第一个函数将能够找到第二个函数,并运行它。与直接调用另一个函数相比,这种方法很笨拙,但它允许在不同的时间传递不同函数的地址,这意味着可以在不同的时间使用不同的函数。

7.10.1 函数指针的基础知识

首先通过一个例子来阐释这一过程。假设要设计一个名为 estimate( ) 的函数,估算编写指定行数的代码所需的时间,并且希望不同的程序员都将使用该函数。对于所有的用户来说,estimate( ) 中一部分代码都是相同的,但该函数允许每个程序员提供自己的算法来估算时间。为实现这种目标,采用的机制是,将程序员要使用的算法函数的地址传递给 estimate( )。为此,必须能够完成下面的工作:

  • 获取函数的地址;
  • 声明一个函数指针;
  • 使用函数指针来调用函数。

1.获取函数的地址

获取函数的地址很简单:只要使用函数名(后面不跟参数)即可。也就是说,如果 think( ) 是一个函数,则 think 就是该函数的地址。要将函数作为参数进行传递,必须传递函数名。一定要区分传递的是函数的地址还是函数的返回值:

process( ) 调用使得 process( ) 函数能够在其内部调用 think( ) 函数。thought( ) 调用首先调用 think( ) 函数,然后将 think( ) 的返回值传递给 thought( ) 函数。

2.声明函数指针

声明指向某种数据类型的指针时,必须指定指针指向的类型。同样,声明指向函数的指针时,也必须指定指针指向的函数类型。这意味着声明应指定函数的返回类型以及函数的特征标(参数列表)。也就是说,声明应像函数原型那样指出有关函数的信息。例如,假设 Pam leCoder 编写了一个估算时间的函数,其原型如下:

则正确的指针类型声明如下:

这与 pam( ) 声明类似,这是将 pam 替换为了(*pf)。由于 pam 是函数,因此(*pf)也是函数。而如果(*pf)是函数,则 pf 就是函数指针。

提示:

通常,要声明指向特定类型的函数的指针,可以首先编写这种函数的原型,然后用(*pf)替换函数名。这样 pf 就是这类函数的指针。

为提供正确的运算符优先级,必须在声明中使用括号将*pf 括起。括号的优先级比*运算符高,因此*pf(int)意味着 pf( ) 是一个返回指针的函数,而(*pf)(int)意味着 pf 是一个指向函数的指针:

正确地声明 pf 后,便可以将相应函数的地址赋给它:

注意,pam( ) 的特征标和返回类型必须与 pf 相同。如果不相同,编译器将拒绝这种赋值:

现在回过头来看一下前面提到的 estimate( ) 函数。假设要将将要编写的代码行数和估算算法(如 pam( ) 函数)的地址传递给它,则其原型将如下:

上述声明指出,第二个参数是一个函数指针,它指向的函数接受一个 int 参数,并返回一个 double 值。要让 estimate( ) 使用 pam( ) 函数,需要将 pam( ) 的地址传递给它:

显然,使用函数指针时,比较棘手的是编写原型,而传递地址则非常简单。

3.使用指针来调用函数

现在进入最后一步,即使用指针来调用被指向的函数。线索来自指针声明。前面讲过,(*pf)扮演的角色与函数名相同,因此使用(*pf)时,只需将它看作函数名即可:

实际上,C++也允许像使用函数名那样使用 pf:

第一种格式虽然不太好看,但它给出了强有力的提示——代码正在使用函数指针。

历史与逻辑

真是非常棒的语法!为何 pf 和(*pf)等价呢?一种学派认为,由于 pf 是函数指针,而*pf 是函数,因此应将(*pf)( ) 用作函数调用。另一种学派认为,由于函数名是指向该函数的指针,指向函数的指针的行为应与函数名相似,因此应将 pf( ) 用作函数调用使用。C++进行了折衷——这 2 种方式都是正确的,或者至少是允许的,虽然它们在逻辑上是互相冲突的。在认为这种折衷粗糙之前,应该想到,容忍逻辑上无法自圆其说的观点正是人类思维活动的特点。

7.10.2 函数指针示例

程序清单 7.18 演示了如何使用函数指针。它两次调用 estimate( ) 函数,一次传递 betsy( ) 函数的地址,另一次则传递 pam( ) 函数的地址。在第一种情况下,estimate( ) 使用 betsy( ) 计算所需的小时数;在第二种情况下,estimate( ) 使用 pam( ) 进行计算。这种设计有助于今后的程序开发。当 Ralph 为估算时间而开发自己的算法时,将不需要重新编写 estimate( )。相反,他只需提供自己的 ralph( ) 函数,并确保该函数的特征标和返回类型正确即可。当然,重新编写 estimate( ) 也并不是一件非常困难的工作,但同样的原则也适用于更复杂的代码。另外,函数指针方式使得 Ralph 能够修改 estimate( ) 的行为,虽然他接触不到 estimate( ) 的源代码。

程序清单 7.18 fun_ptr.cpp

下面是运行该程序的情况:

下面是再次运行该程序的情况:

7.10.3 深入探讨函数指针

函数指针的表示可能非常恐怖。下面通过一个示例演示使用函数指针时面临的一些挑战。首先,下面是一些函数的原型,它们的特征标和返回类型相同:

这些函数的特征标看似不同,但实际上相同。首先,前面说过,在函数原型中,参数列表 const double ar [ ]与 const double * ar 的含义完全相同。其次,在函数原型中,可以省略标识符。因此,const double ar [ ]可简化为 const double [ ],而 const double * ar 可简化为 const double *。因此,上述所有函数特征标的含义都相同。另一方面,函数定义必须提供标识符,因此需要使用 const double ar [ ]或 const double * ar。

接下来,假设要声明一个指针,它可指向这三个函数之一。假定该指针名为 pa,则只需将目标函数原型中的函数名替换为(*pa):

可在声明的同时进行初始化:

使用 C++11 的自动类型推断功能时,代码要简单得多:

现在来看下面的语句:

根据前面介绍的知识可知,(*p1) (av, 3) 和 p2(av, 3) 都调用指向的函数(这里为 f1() 和 f2()),并将 av 和 3 作为参数。因此,显示的是这两个函数的返回值。返回值的类型为 const double *(即 double 值的地址),因此在每条 cout 语句中,前半部分显示的都是一个 double 值的地址。为查看存储在这些地址处的实际值,需要将运算符*应用于这些地址,如表达式*(*p1)(av,3) 和*p2(av,3) 所示。

鉴于需要使用三个函数,如果有一个函数指针数组将很方便。这样,将可使用 for 循环通过指针依次调用每个函数。如何声明这样的数组呢?显然,这种声明应类似于单个函数指针的声明,但必须在某个地方加上[3],以指出这是一个包含三个函数指针的数组。问题是在什么地方加上[3],答案如下(包含初始化):

为何将[3]放在这个地方呢?pa 是一个包含三个元素的数组,而要声明这样的数组,首先需要使用 pa[3]。该声明的其他部分指出了数组包含的元素是什么样的。运算符[]的优先级高于*,因此*pa[3]表明 pa 是一个包含三个指针的数组。上述声明的其他部分指出了每个指针指向的是什么:特征标为 const double *, int,且返回类型为 const double *的函数。因此,pa 是一个包含三个指针的数组,其中每个指针都指向这样的函数,即将 const double *和 int 作为参数,并返回一个 const double *。

这里能否使用 auto 呢?不能。自动类型推断只能用于单值初始化,而不能用于初始化列表。但声明数组 pa 后,声明同样类型的数组就很简单了:

本书前面说过,数组名是指向第一个元素的指针,因此 pa 和 pb 都是指向函数指针的指针。

如何使用它们来调用函数呢?pa[i]和 pb[i]都表示数组中的指针,因此可将任何一种函数调用表示法用于它们:

要获得指向的 double 值,可使用运算符*:

可做的另一件事是创建指向整个数组的指针。由于数组名 pa 是指向函数指针的指针,因此指向数组的指针将是这样的指针,即它指向指针的指针。这听起来令人恐怖,但由于可使用单个值对其进行初始化,因此可使用 auto:

如果您喜欢自己声明,该如何办呢?显然,这种声明应类似于 pa 的声明,但由于增加了一层间接,因此需要在某个地方添加一个*。具体地说,如果这个指针名为 pd,则需要指出它是一个指针,而不是数组。这意味着声明的核心部分应为(*pd)[3],其中的括号让标识符 pd 与*先结合:

换句话说,pd 是一个指针,它指向一个包含三个元素的数组。这些元素是什么呢?由 pa 的声明的其他部分描述,结果如下:

要调用函数,需认识到这样一点:既然 pd 指向数组,那么*pd 就是数组,而(*pd)[i]是数组中的元素,即函数指针。因此,较简单的函数调用是(*pd) i ,而*(*pd) i 是返回的指针指向的值。也可以使用第二种使用指针调用函数的语法:使用(*(*pd)[i])(av,3) 来调用函数,而*(*(*pd)[i])(av,3) 是指向的 double 值。

请注意 pa(它是数组名,表示地址)和&pa 之间的差别。正如您在本书前面看到的,在大多数情况下,pa 都是数组第一个元素的地址,即&pa[0]。因此,它是单个指针的地址。但&pa 是整个数组(即三个指针块)的地址。从数字上说,pa 和&pa 的值相同,但它们的类型不同。一种差别是,pa+1 为数组中下一个元素的地址,而&pa+1 为数组 pa 后面一个 12 字节内存块的地址(这里假定地址为 4 字节)。另一个差别是,要得到第一个元素的值,只需对 pa 解除一次引用,但需要对&pa 解除两次引用:

程序清单 7.19 使用了这里讨论的知识。出于演示的目的,函数 f1() 等都非常简单。正如注释指出的,这个程序演示了 auto 的 C++98 替代品。

程序清单 7.19 arfupt.cpp

该程序的输出如下:

显示的地址为数组 av 中 double 值的存储位置。

这个示例可能看起来比较深奥,但指向函数指针数组的指针并不少见。实际上,类的虚方法实现通常都采用了这种技术(参见第 13 章)。所幸的是,这些细节由编译器处理。

感谢 auto

C++11 的目标之一是让 C++更容易使用,从而让程序员将主要精力放在设计而不是细节上。程序清单 7.19 演示了这一点:

自动类型推断功能表明,编译器的角色发生了改变。在 C++98 中,编译器利用其知识帮助您发现错误,而在 C++11 中,编译器利用其知识帮助您进行正确的声明。

存在一个潜在的缺点。自动类型推断确保变量的类型与赋给它的初值的类型一致,但您提供的初值的类型可能不对:

上述声明导致 pc 的类型与*pa 一致,在程序清单 7.19 中,后面使用它时假定其类型与&pa 相同,这将导致编译错误。

7.10.4 使用 typedef 进行简化

除 auto 外,C++还提供了其他简化声明的工具。您可能还记得,第 5 章说过,关键字 typedef 让您能够创建类型别名:

这里采用的方法是,将别名当做标识符进行声明,并在开头使用关键字 typedef。因此,可将 p_fun 声明为程序清单 7.19 使用的函数指针类型的别名:

然后使用这个别名来简化代码:

使用 typedef 可减少输入量,让您编写代码时不容易犯错,并让程序更容易理解。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文