C++ Primer Plus 第6版 读书笔记(7)第 7 章 函数——C++的编程模块

第 7 章 函数——C++的编程模块

本章内容包括:
 
 函数基本知识。
 
 函数原型。
 
 按值传递函数参数。
 
 设计处理数组的函数。
 
 使用 const 指针参数。
 
 设计处理文本字符串的函数。
 
 设计处理结构的函数。
 
 设计处理 string 对象的函数。
 
 调用自身的函数(递归)。
 
 指向函数的指针。

乐趣在于发现。仔细研究,读者将在函数中找到乐趣。C++自带了一个包含函数的大型库(标准 ANSI 库
加上多个 C++类),但真正的编程乐趣在于编写自己的函数;另一方面,要提高编程效率,

本章和第 8 章介绍如何定义函数、给函数传递信息以及从函数那里获得信息。本章
首先复习函数是如何工作的,然后着重介绍如何使用函数来处理数组、字符串和结构,最后介绍递归和函数指
针。如果读者熟悉 C 语言,将发现本章的很多内容是熟悉的。然而,不要因此而掉以轻心,产生错误认识。在
函数方面,C++在 C 语言的基础上新增了一些功能,这将在第 8 章介绍。现在,把注意力放在基础知识上。

7.1 复习函数的基本知识

来复习一下介绍过的有关函数的知识。要使用 C++函数,必须完成如下工作:

  1. 提供函数定义;

  2. 提供函数原型;

  3. 调用函数。

7.1.1 定义函数

对于有返回值的函数,必须使用返回语句,以便将值返回给调用函数。值本身可以是常量、变量,也
可以是表达式,只是其结果的类型必须为 typeName 类型或可以被转换为 typeName(例如,如果声明的返
回类型为 double,而函数返回一个 int 表达式,则该 int 值将被强制转换为 double 类型)。然后,函数将最
终的值返回给调用函数。C++对于返回值的类型有一定的限制:不能是数组,但可以是其他任何类型—整
数、浮点数、指针,甚至可以是结构和对象!(有趣的是,虽然 C++函数不能直接返回数组,但可以将数
组作为结构或对象组成部分来返回。)

作为一名程序员,并不需要知道函数是如何返回值的,但是对这个问题有所了解将有助于澄清概念。(另
外,还有助于与朋友和家人交换意见。)通常,函数通过将返回值复制到指定的 CPU 寄存器或内存单元中来
将其返回。随后,调用程序将查看该内存单元。返回函数和调用函数必须就该内存单元中存储的数据的类型
达成一致。函数原型将返回值类型告知调用程序,而函数定义命令被调用函数应返回什么类型的数据(参见
图 7.1)。在原型中提供与定义中相同的信息似乎有些多余,但这样做确实有道理。要让信差从办公室的办公
桌上取走一些物品,则向信差和办公室中的同事交代自己的意图,将提高信差顺利完成这项工作的概率。

7.1.2 函数原型和函数调用

至此,读者已熟悉了函数调用,但对函数原型可能不太熟悉,因为它经常隐藏在 include 文件中。程序
清单 7.2 在一个程序中使用了函数 cheer( )和 cube( )。请留意其中的函数原型。

1.为什么需要原型
原型描述了函数到编译器的接口,也就是说,它将函数返回值的类型(如果有的话)以及参数的类型
和数量告诉编译器。例如,请看原型将如何影响程序清单 7.2 中下述函数调用:

首先,原型告诉编译器,cube( )有一个 double 参数。如果程序没有提供这样的参数,原型将让编译器
能够捕获这种错误。其次,cube( )函数完成计算后,将把返回值放置在指定的位置—可能是 CPU 寄存器,
也可能是内存中。然后调用函数(这里为 main( ))将从这个位置取得返回值。由于原型指出了 cube( )的类
型为 double,因此编译器知道应检索多少个字节以及如何解释它们。如果没有这些信息,编译器将只能进
行猜测,而编译器是不会这样做的。

读者可能还会问,为何编译器需要原型,难道它就不能在文件中进一步查找,以了解函数是如何定义
的吗?这种方法的一个问题是效率不高。编译器在搜索文件的剩余部分时将必须停止对 main( )的编译。一
个更严重的问题是,函数甚至可能并不在文件中。C++允许将一个程序放在多个文件中,单独编译这些文
件,然后再将它们组合起来。在这种情况下,编译器在编译 main( )时,可能无权访问函数代码。如果函数
位于库中,情况也将如此。避免使用函数原型的唯一方法是,在首次使用函数之前定义它,但这并不总是
可行的。另外,C++的编程风格是将 main( )放在最前面,因为它通常提供了程序的整体结构。

C++原型与 ANSI 原型

ANSI C 借鉴了 C++中的原型,但这两种语言还是有区别的。其中最重要的区别是,为与基本 C 兼容,
ANSI C 中的原型是可选的,但在 C++中,原型是必不可少的。例如,请看下面的函数声明:

在 C++中,括号为空与在括号中使用关键字 void 是等效的—意味着函数没有参数。在 ANSI C 中,
括号为空意味着不指出参数—这意味着将在后面定义参数列表。在 C++中,不指定参数列表时应使用省
略号:
通常,仅当与接受可变参数的 C 函数(如 printf( ))交互时才需要这样做。

原型的功能

正如您看到的,原型可以帮助编译器完成许多工作;但它对程序员有什么帮助呢?它们可以极大地降
低程序出错的几率。具体来说,原型确保以下几点:

  1. 编译器正确处理函数返回值;

  2. 编译器检查使用的参数数目是否正确;

  3. 编译器检查使用的参数类型是否正确。如果不正确,则转换为正确的类型(如果可能的话)。

7.2 函数参数和按值传递

被调用时,该函数将创建一个新的名为 x 的 double 变量,并将其初始化为 5。这样,cube( )执行的操
作将不会影响 main( )中的数据,因为 cube( )使用的是 side 的副本,而不是原来的数据。稍后将介绍一个实
现这种保护的例子。用于接收传递值的变量被称为形参。传递给函数的值被称为实参。出于简化的目的,
C++标准使用参数(argument)来表示实参,使用参量(parameter)来表示形参,因此参数传递将参量赋
给参数(参见图 7.2)。

在函数中声明的变量(包括参数)是该函数私有的。在函数被调用时,计算机将为这些变量分配内存;
在函数结束时,计算机将释放这些变量使用的内存(有些 C++文献将分配和释放内存称为创建和毁坏变量,
这样似乎更激动人心)。这样的变量被称为局部变量,因为它们被限制在函数中。前面提到过,这样做有助
于确保数据的完整性。这还意味着,如果在 main( )中声明了一个名为 x 的变量,同时在另一个函数中也声
明了一个名为 x 的变量,则它们将是两个完全不同的、毫无关系的变量,这与加利福尼亚州的 Albany 与纽
约的 Albany 是两个完全不同的地方是一样的道理(参见图 7.3)。这样的变量也被称为自动变量,因为它们
是在程序执行过程中自动被分配和释放的。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

7.2.1 多个参数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rbqv4y9w-1679139307627)(2023-03-07-11-15-32.png)]

7.2.2 另外一个接受两个参数的函数

下面创建另一个功能更强大的函数,它执行重要的计算任务。另外,该函数将演示局部变量的用法,
而不是形参的用法。

目前,美国许多州都采用某种纸牌游戏的形式来发行彩票,让参与者从卡片中选择一定数目的选项。
例如,从 51 个数字中选取 6 个。随后,彩票管理者将随机抽取 6 个数。如果参与者选择的数字与这 6 个完
全相同,将赢得大约几百万美元的奖金。我们的函数将计算中奖的几率。(是的,能够成功预测获奖号码的
函数将更有用,但虽然 C++的功能非常强大,目前还不具备超自然能力。)

首先,需要一个公式。假设必须从 51 个数中选取 6 个,而获奖的概率为 1/R,则 R 的计算公式如下:

7.3 函数和数组

到目前为止,本书的函数示例都很简单,参数和返回值的类型都是基本类型。但是,函数是处理更复
杂的类型(如数组和结构)的关键。下面来如何将数组和函数结合在一起。

假设使用一个数组来记录家庭野餐中每人吃了多少个甜饼(每个数组索引都对应一个人,元素值对应
于这个人所吃的甜饼数量)。现在想知道总数。这很容易,只需使用循环将所有数组元素累积起来即可。将
数组元素累加是一项非常常见的任务,因此设计一个完成这项工作的函数很有意义。这样就不必在每次计
算数组总和时都编写新的循环了。

考虑函数接口所涉及的内容。由于函数计算总数,因此应返回答案。如果不分吃甜饼,则可以让函数
的返回类型为 int。另外,函数需要知道要对哪个数组进行累计,因此需要将数组名作为参数传递给它。为
使函数通用,而不限于特定长度的数组,还需要传递数组长度。这里唯一的新内容是,需要将一个形参声
明为数组名。下面来看一看函数头及其其他部分:
这看起来似乎合理。方括号指出 arr 是一个数组,而方括号为空则表明,可以将任何长度的数组传递
给该函数。但实际情况并非如此:arr 实际上并不是数组,而是一个指针!好消息是,在编写函数的其余部
分时,可以将 arr 看作是数组。首先,通过一个示例验证这种方法可行,然后看看它为什么可行。

程序清单 7.5 演示如同使用数组名那样使用指针的情况。程序将数组初始化为某些值,并使用 sum_arr( )
函数计算总数。注意到 sum_arr( )函数使用 arr 时,就像是使用数组名一样。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TrKrva35-1679139307628)(2023-03-07-11-17-35.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1Z1mPsao-1679139307628)(2023-03-07-11-32-00.png)]

7.7 函数和 string 对象

虽然 C-风格字符串和 string 对象的用途几乎相同,但与数组相比,string 对象与结构的更相似。例如,
可以将一个结构赋给另一个结构,也可以将一个对象赋给另一个对象。可以将结构作为完整的实体传递给
函数,也可以将对象作为完整的实体进行传递。

如果需要多个字符串,可以声明一个 string 对象数组,而
不是二维 char 数组。
程序清单 7.14 提供了一个小型示例,它声明了一个 string 对象数组,并将该数组传递给一个函数以显
示其内容

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gyKQIntM-1679139307628)(2023-03-18-19-27-50.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J8AIFeSZ-1679139307628)(2023-03-18-19-28-04.png)]

7.9 递归

下面介绍一些完全不同的内容。C++函数有一种有趣的特点—可以调用自己(然而,与 C 语言不同
的是,C++不允许 main( )调用自己),这种功能被称为递归。尽管递归在特定的编程(例如人工智能)中是
一种重要的工具,但这里只简单地介绍一下它是如何工作的。

7.9.1 包含一个递归调用的递归

如果递归函数调用自己,则被调用的函数也将调用自己,这将无限循环下去,除非代码中包含终止调
用链的内容。通常的方法将递归调用放在 if 语句中。例如,void 类型的递归函数 recurs( )的代码如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-huHZfZOW-1679139307629)(2023-03-18-19-29-03.png)]

test 最终将为 false,调用链将断开。
递归调用将导致一系列有趣的事件。只要 if 语句为 true,每个 recurs( )调用都将执行 statements 1,然
后再调用 recurs( ),而不会执行 statements 2。当 if 语句为 false 时,当前调用将执行 statements2。当前调用
结束后,程序控制权将返回给调用它的 recurs( ),而该 recurs( )将执行其 stataments2 部分,然后结束,并将
控制权返回给前一个调用,依此类推。

因此,如果 recurs( )进行了 5 次递归调用,则第一个 statements1 部
分将按函数调用的顺序执行 5 次,然后 statements2 部分将以与函数调用相反的顺序执行 5 次。进入 5 层递
归后,程序将沿进入的路径返回。程序清单 7.16 演示了这种行为。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JcMFLZJb-1679139307629)(2023-03-18-19-29-40.png)]

7.9.2 包含多个递归调用的递归

在需要将一项工作不断分为两项较小的、类似的工作时,递归非常有用。例如,请考虑使用这种方法
来绘制标尺的情况。标出两端,找到中点并将其标出。然后将同样的操作用于标尺的左半部分和右半部分。

如果要进一步细分,可将同样的操作用于当前的每一部分。递归方法有时被称为分而治之策略
(divide-and-conquer strategy)。程序清单 7.17 使用递归函数 subdivide( )演示了这种方法,该函数使用一个
字符串,该字符串除两端为 | 字符外,其他全部为空格。

main 函数使用循环调用 subdivide( )函数 6 次,每
次将递归层编号加 1,并打印得到的字符串。这样,每行输出表示一层递归。该程序使用限定符 std::而不
是编译指令 using,以提醒读者还可以采取这种方式。

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( )的地址传递给它:
显然,使用函数指针时,比较棘手的是编写原型,而传递地址则非常简单。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lvdgSvys-1679139307629)(2023-03-18-19-32-39.png)]
3.使用指针来调用函数
现在进入最后一步,即使用指针来调用被指向的函数。线索来自指针声明。前面讲过,(*pf)扮演的
角色与函数名相同,因此使用(*pf)时,只需将它看作函数名即可:
实际上,C++也允许像使用函数名那样使用 pf:
第一种格式虽然不太好看,但它给出了强有力的提示—代码正在使用函数指针。
历史与逻辑
真是非常棒的语法!为何 pf 和(pf)等价呢?一种学派认为,由于 pf 是函数指针,而pf 是函数,
因此应将(*pf)( )用作函数调用。另一种学派认为,由于函数名是指向该函数的指针,指向函数的指针的
行为应与函数名相似,因此应将 pf( )用作函数调用使用。C++进行了折衷—这 2 种方式都是正确的,或
者至少是允许的,虽然它们在逻辑上是互相冲突的。在认为这种折衷粗糙之前,应该想到,容忍逻辑上无
法自圆其说的观点正是人类思维活动的特点。

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qoFNyhel-1679139307629)(2023-03-18-19-33-01.png)]

7.11 总结

函数是 C++的编程模块。要使用函数,必须提供定义和原型,并调用该函数。函数定义是实现函数功
能的代码;函数原型描述了函数的接口:传递给函数的值的数目和种类以及函数的返回类型。函数调用使
得程序将参数传递给函数,并执行函数的代码。

在默认情况下,C++函数按值传递参数。这意味着函数定义中的形参是新的变量,它们被初始化为函
数调用所提供的值。因此,C++函数通过使用拷贝,保护了原始数据的完整性。

C++将数组名参数视为数组第一个元素的地址。从技术上讲,这仍然是按值传递的,因为指针是原始
地址的拷贝,但函数将使用指针来访问原始数组的内容。当且仅当声明函数的形参时,

下面两个声明才是
等价的:

这两个声明都表明,arr 是指向 typeName 的指针,但在编写函数代码时,可以像使用数组名那样使用
arr 来访问元素:arr[i]。即使在传递指针时,也可以将形参声明为 const 指针,来保护原始数据的完整性。
由于传递数据的地址时,并不会传输有关数组长度的信息,因此通常将数组长度作为独立的参数来传递。

另外,也可传递两个指针(其中一个指向数组开头,另一个指向数组末尾的下一个元素),以指定一个范围,
就像 STL 使用的算法一样。
C++提供了 3 种表示 C-风格字符串的方法:字符数组、字符串常量和字符串指针。它们的类型都是 char*
(char 指针),因此被作为 char*类型参数传递给函数。C++使用空值字符(\0)来结束字符串,因此字符串
函数检测空值字符来确定字符串的结尾。
C++还提供了 string 类,用于表示字符串。函数可以接受 string 对象作为参数以及将 string 对象作为返
回值。string 类的方法 size( )可用于判断其存储的字符串的长度。

C++处理结构的方式与基本类型完全相同,这意味着可以按值传递结构,并将其用作函数返回类型。
然而,如果结构非常大,则传递结构指针的效率将更高,同时函数能够使用原始数据。这些考虑因素也适
用于类对象。
C++函数可以是递归的,也就是说,函数代码中可以包括对函数本身的调用。
C++函数名与函数地址的作用相同。通过将函数指针作为参数,可以传递要调用的函数的名称。

7.12 复习题

1.使用函数的 3 个步骤是什么?
2.请创建与下面的描述匹配的函数原型。
a.igor( )没有参数,且没有返回值。
b.tofu( )接受一个 int 参数,并返回一个 float。
c.mpg( )接受两个 double 参数,并返回一个 double。
d.summation( )将 long 数组名和数组长度作为参数,并返回一个 long 值。
e.doctor( )接受一个字符串参数(不能修改该字符串),并返回一个 double 值。
f.ofcourse( )将 boss 结构作为参数,不返回值。
g.plot( )将 map 结构的指针作为参数,并返回一个字符串。
3.编写一个接受 3 个参数的函数:int 数组名、数组长度和一个 int 值,并将数组的所有元素都设置为
该 int 值。
4.编写一个接受 3 个参数的函数:指向数组区间中第一个元素的指针、指向数组区间最后一个元素后
面的指针以及一个 int 值,并将数组中的每个元素都设置为该 int 值。
5.编写将 double 数组名和数组长度作为参数,并返回该数组中最大值的函数。该函数不应修改数组
的内容。
6.为什么不对类型为基本类型的函数参数使用 const 限定符?
7.C++程序可使用哪 3 种 C-风格字符串格式?
8.编写一个函数,其原型如下:
该函数将字符串中所有的 c1 都替换为 c2,并返回替换次数。
9.表达式*"pizza"的含义是什么?"taco" [2]呢?
10.C++允许按值传递结构,也允许传递结构的地址。如果 glitz 是一个结构变量,如何按值传递它?
如何传递它的地址?这两种方法有何利弊?
11.函数 judge( )的返回类型为 int,它将这样一个函数的地址作为参数:将 const char 指针作为参数,
250 C++ Primer Plus(第 6 版)中文版 
并返回一个 int 值。请编写 judge( )函数的原型。
12.假设有如下结构声明:
a.编写一个函数,它将 application 结构作为参数,并显示该结构的内容。
b.编写一个函数,它将 application 结构的地址作为参数,并显示该参数指向的结构的内容。
13.假设函数 f1()和 f2()的原型如下:
请将 p1 和 p2 分别声明为指向 f1 和 f2 的指针;将 ap 声明为一个数组,它包含 5 个类型与 p1 相同的
指针;将 pa 声明为一个指针,它指向的数组包含 10 个类型与 p2 相同的指针。使用 typedef 来帮助完成这
项工作

猜你喜欢

转载自blog.csdn.net/shaozheng0503/article/details/129641554