- 内容提要
- 前言
- 第 1 章 预备知识
- 第 2 章 开始学习 C++
- 第 3 章 处理数据
- 第 4 章 复合类型
- 第 5 章 循环和关系表达式
- 第 6 章 分支语句和逻辑运算符
- 第 7 章 函数——C++的编程模块
- 第 8 章 函数探幽
- 第 9 章 内存模型和名称空间
- 第 10 章 对象和类
- 第 11 章 使用类
- 第 12 章 类和动态内存分配
- 第 13 章 类继承
- 第 14 章 C++中的代码重用
- 第 15 章 友元、异常和其他
- 第 16 章 string 类和标准模板库
- 第 17 章 输入、输出和文件
- 第 18 章 探讨 C++新标准
- 附录 A 计数系统
- 附录 B C++保留字
- 附录 C ASCII 字符集
- 附录 D 运算符优先级
- 附录 E 其他运算符
- 附录 F 模板类 string
- 附录 G 标准模板库方法和函数
- 附录 H 精选读物和网上资源
- 附录 I 转换为 ISO 标准 C++
- 附录 J 复习题答案
7.10 函数指针
如果未提到函数指针,则对 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论