- 内容提要
- 前言
- 第 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.3 函数和数组
到目前为止,本书的函数示例都很简单,参数和返回值的类型都是基本类型。但是,函数是处理更复杂的类型(如数组和结构)的关键。下面来如何将数组和函数结合在一起。
假设使用一个数组来记录家庭野餐中每人吃了多少个甜饼(每个数组索引都对应一个人,元素值对应于这个人所吃的甜饼数量)。现在想知道总数。这很容易,只需使用循环将所有数组元素累积起来即可。将数组元素累加是一项非常常见的任务,因此设计一个完成这项工作的函数很有意义。这样就不必在每次计算数组总和时都编写新的循环了。
考虑函数接口所涉及的内容。由于函数计算总数,因此应返回答案。如果不分吃甜饼,则可以让函数的返回类型为 int。另外,函数需要知道要对哪个数组进行累计,因此需要将数组名作为参数传递给它。为使函数通用,而不限于特定长度的数组,还需要传递数组长度。这里唯一的新内容是,需要将一个形参声明为数组名。下面来看一看函数头及其其他部分:
这看起来似乎合理。方括号指出 arr 是一个数组,而方括号为空则表明,可以将任何长度的数组传递给该函数。但实际情况并非如此:arr 实际上并不是数组,而是一个指针!好消息是,在编写函数的其余部分时,可以将 arr 看作是数组。首先,通过一个示例验证这种方法可行,然后看看它为什么可行。
程序清单 7.5 演示如同使用数组名那样使用指针的情况。程序将数组初始化为某些值,并使用 sum_arr( ) 函数计算总数。注意到 sum_arr( ) 函数使用 arr 时,就像是使用数组名一样。
程序清单 7.5 arrfun1.cpp
下面是该程序的输出:
从中可知,该程序管用。下面讨论为何该程序管用。
7.3.1 函数如何使用指针来处理数组
在大多数情况下,C++和 C 语言一样,也将数组名视为指针。第 4 章介绍过,C++将数组名解释为其第一个元素的地址:
该规则有一些例外。首先,数组声明使用数组名来标记存储位置;其次,对数组名使用 sizeof 将得到整个数组的长度(以字节为单位);第三,正如第 4 章指出的,将地址运算符&用于数组名时,将返回整个数组的地址,例如&cookies 将返回一个 32 字节内存块的地址(如果 int 长 4 字节)。
程序清单 7.5 执行下面的函数调用:
其中,cookies 是数组名,而根据 C++规则,cookies 是其第一个元素的地址,因此函数传递的是地址。由于数组的元素的类型为 int,因此 cookies 的类型必须是 int 指针,即 int *。这表明,正确的函数头应该是这样的:
其中用 int * arr 替换了 int arr [ ]。这证明这两个函数头都是正确的,因为在 C++中,当(且仅当)用于函数头或函数原型中,int *arr 和 int arr [ ]的含义才是相同的。它们都意味着 arr 是一个 int 指针。然而,数组表示法(int arr[ ])提醒用户,arr 不仅指向 int,还指向 int 数组的第一个 int。当指针指向数组的第一个元素时,本书使用数组表示法;而当指针指向一个独立的值时,使用指针表示法。别忘了,在其他的上下文中,int * arr 和 int arr [ ]的含义并不相同。例如,不能在函数体中使用 int tip[ ]来声明指针。
鉴于变量 arr 实际上就是一个指针,函数的其余部分是合理的。第 4 章在介绍动态数组时指出过,同数组名或指针一样,也可以用方括号数组表示法来访问数组元素。无论 arr 是指针还是数组名,表达式 arr [3]都指的是数组的第 4 个元素。就目前而言,提请读者记住下面两个恒等式,将不会有任何坏处:
记住,将指针(包括数组名)加 1,实际上是加上了一个与指针指向的类型的长度(以字节为单位)相等的值。对于遍历数组而言,使用指针加法和数组下标时等效的。
7.3.2 将数组作为参数意味着什么
我们来看一看程序清单 7.5 暗示了什么。函数调用 sum_arr(coolies, ArSize) 将 cookies 数组第一个元素的地址和数组中的元素数目传递给 sum_arr( ) 函数。sum_arr( ) 函数将 cookies 的地址赋给指针变量 arr,将 ArSize 赋给 int 变量 n。这意味着,程序清单 7.5 实际上并没有将数组内容传递给函数,而是将数组的位置(地址)、包含的元素种类(类型)以及元素数目(n 变量)提交给函数(参见图 7.4)。有了这些信息后,函数便可以使用原来的数组。传递常规变量时,函数将使用该变量的拷贝;但传递数组时,函数将使用原来的数组。实际上,这种区别并不违反 C++按值传递的方法,sum_arr( ) 函数仍传递了一个值,这个值被赋给一个新变量,但这个值是一个地址,而不是数组的内容。
图 7.4 告知函数有关数组的信息
数组名与指针对应是好事吗?确实是一件好事。将数组地址作为参数可以节省复制整个数组所需的时间和内存。如果数组很大,则使用拷贝的系统开销将非常大;程序不仅需要更多的计算机内存,还需要花费时间来复制大块的数据。另一方面,使用原始数据增加了破坏数据的风险。在经典的 C 语言中,这确实是一个问题,但 ANSI C 和 C++中的 const 限定符提供了解决这种问题的办法。稍后将介绍一个这样的示例,但先来修改程序清单 7.5,以演示数组函数是如何运作的。程序清单 7.6 表明,cookies 和 arr 的值相同。它还演示了指针概念如何使 sum_arr 函数比以前更通用。该程序使用限定符 std::而不是编译指令 using 来提供对 cout 和 endl 的访问权。
程序清单 7.6 arrfun2.cpp
下面是该程序的输出(地址值和数组的长度将随系统而异):
注意,地址值和数组的长度随系统而异。另外,有些 C++实现以十进制而不是十六进制格式显示地址,还有些编译器以十六进制显示地址时,会加上前缀 0x。
程序说明
程序清单 7.6 说明了数组函数的一些有趣的地方。首先,cookies 和 arr 指向同一个地址。但 sizeof cookies 的值为 32,而 sizeof arr 为 4。这是由于 sizeof cookies 是整个数组的长度,而 sizeof arr 只是指针变量的长度(上述程序运行结果是从一个使用 4 字节地址的系统中获得的)。顺便说一句,这也是必须显式传递数组长度,而不能在 sum_arr( ) 中使用 sizeof arr 的原因;指针本身并没有指出数组的长度。
由于 sum_arr( ) 只能通过第二个参数获知数组中的元素数量,因此可以对函数“说谎”。例如,程序第二次使用该函数时,这样调用它:
通过告诉该函数 cookies 有 3 个元素,可以让它计算前 3 个元素的总和。
为什么在这里停下了呢?还可以提供假的数组起始位置:
由于 cookies 是第一个元素的地址,因此 cookies + 4 是第 5 个元素的地址。这条语句将计算数组第 5、6、7、8 个元素的总和。请注意输出中第三次函数调用选择将不同于前两个调用的地址赋给 arr 的。是的,可以将&cookies[4],而不是 cookies + 4 作为参数;它们的含义是相同的。
注意:
为将数组类型和元素数量告诉数组处理函数,请通过两个不同的参数来传递它们:而不要试图使用方括号表示法来传递数组长度:
7.3.3 更多数组函数示例
选择使用数组来表示数据时,实际上是在进行一次设计方面的决策。但设计决策不仅仅是确定数据的存储方式,还涉及到如何使用数据。程序员常会发现,编写特定的函数来处理特定的数据操作是有好处的(这里讲的好处指的是程序的可靠性更高、修改和调试更为方便)。另外,构思程序时将存储属性与操作结合起来,便是朝 OOP 思想迈进了重要的一步;以后将证明这是很有好处的。
来看一个简单的案例。假设要使用一个数组来记录房地产的价值(假设拥有房地产)。在这种情况下,程序员必须确定要使用哪种类型。当然,double 的取值范围比 int 和 long 大,并且提供了足够多的有效位数来精确地表示这些值。接下来必须决定数组元素的数目。(对于使用 new 创建的动态数组来说,可以稍后再决定,但我们希望使事情简单一点)。如果房地产数目不超过 5 个,则可以使用一个包含 5 个元素的 double 数组。
现在,考虑要对房地产数组执行的操作。两个基本的操作分别是,将值读入到数组中和显示数组内容。我们再添加另一个操作:重新评估每种房地产的值。为简单起见,假设所有房地产都以相同的比率增加或者减少。(别忘了,这是一本关于 C++的书,而不是关于房地产管理的书。)接下来,为每项操作编写一个函数,然后编写相应的代码。下面首先介绍这些步骤,然后将其用于一个完整的示例中。
1.填充数组
由于接受数组名参数的函数访问的是原始数组,而不是其副本,因此可以通过调用该函数将值赋给数组元素。该函数的一个参数是要填充的数组的名称。通常,程序可以管理多个人的投资,因此需要多个数组,因此不能在函数中设置数组长度,而要将数组长度作为第二个参数传递,就像前一个示例那样。另外,用户也可能希望在数组被填满之前停止读取数据,因此需要在函数中建立这种特性。由于用户输入的元素数目可能少于数组的长度,因此函数应返回实际输入的元素数目。因此,该函数的原型如下:
该函数接受两个参数,一个是数组名,另一个指定了要读取的最大元素数;该函数返回实际读取的元素数。例如,如果使用该函数来处理一个包含 5 个元素的数组,则将 5 作为第二个参数。如果只输入 3 个值,则该函数将返回 3。
可以使用循环连续地将值读入到数组中,但如何提早结束循环呢?一种方法是,使用一个特殊值来指出输入结束。由于所有的属性都不为负,因此可以使用负数来指出输入结束。另外,该函数应对错误输入作出反应,如停止输入等。这样,该函数的代码如下所示:
注意,代码中包含了对用户的提示。如果用户输入的是非负值,则这个值将被赋给数组,否则循环结束。如果用户输入的都是有效值,则循环将在读取最大数目的值后结束。循环完成的最后一项工作是将 i 加 1,因此循环结束后,i 将比最后一个数组索引大 1,即等于填充的元素数目。然后,函数返回这个值。
2.显示数组及用 const 保护数组
创建显示数组内容的函数很简单。只需将数组名和填充的元素数目传递给函数,然后该函数使用循环来显示每个元素。然而,还有另一个问题——确保显示函数不修改原始数组。除非函数的目的就是修改传递给它的数据,否则应避免发生这种情况。使用普通参数时,这种保护将自动实现,这是由于 C++按值传递数据,而且函数使用数据的副本。然而,接受数组名的函数将使用原始数据,这正是 fill_array( ) 函数能够完成其工作的原因。为防止函数无意中修改数组的内容,可在声明形参时使用关键字 const(参见第 3 章):
该声明表明,指针 ar 指向的是常量数据。这意味着不能使用 ar 修改该数据,也就是说,可以使用像 ar[0]这样的值,但不能修改。注意,这并不是意味着原始数组必须是常量,而只是意味着不能在 show_array( ) 函数中使用 ar 来修改这些数据。因此,show_array( ) 将数组视为只读数据。假设无意间在 show_array( ) 函数中执行了下面的操作,从而违反了这种限制:
编译器将禁止这样做。例如,Borland C++将给出一条错误消息,如下所示(稍作了编辑):
其他编译器可能用其他措词表示其不满。
这条消息提醒用户,C++将声明 const double ar [ ]解释为 const double *ar。因此,该声明实际上是说,ar 指向的是一个常量值。结束这个例子后,我们将详细讨论这个问题。下面是 show_array( ) 函数的代码:
3.修改数组
在这个例子中,对数组进行的第三项操作是将每个元素与同一个重新评估因子相乘。需要给函数传递 3 个参数:因子、数组和元素数目。该函数不需要返回值,因此其代码如下:
由于这个函数将修改数组的值,因此在声明 ar 时,不能使用 const。
4.将上述代码组合起来
至此,您根据数据的存储方式(数组)和使用方式(3 个函数)定义了数据的类型,因此可以将它们组合成一个程序。由于已经建立了所有的数组处理工具,因此 main( ) 的编程工作非常简单。该程序检查用户输入的是否是数字,如果不是,则要求用户这样做。余下的大部分编程工作只是让 main( ) 调用前面开发的函数。程序清单 7.7 列出了最终的代码,它将编译指令 using 放在那些需要 iostream 工具的函数中。
程序清单 7.7 arrfun3.cpp
下面两次运行该程序时的输出:
函数 fill_array( ) 指出,当用户输入 5 项房地产值或负值后,将结束输入。第一次运行演示了输入 5 项房地产值的情况,第二次运行演示了输入负值的情况。
5.程序说明
前面已经讨论了与该示例相关的重要编程细节,因此这里回顾一下整个过程。我们首先考虑的是通过数据类型和设计适当的函数来处理数据,然后将这些函数组合成一个程序。有时也称为自下而上的程序设计(bottom-up programming),因为设计过程从组件到整体进行。这种方法非常适合于 OOP——它首先强调的是数据表示和操纵。而传统的过程性编程倾向于从上而下的程序设计(top-down programming),首先指定模块化设计方案,然后再研究细节。这两种方法都很有用,最终的产品都是模块化程序。
6.数组处理函数的常用编写方式
假设要编写一个处理 double 数组的函数。如果该函数要修改数组,其原型可能类似于下面这样:
如果函数不修改数组,其原型可能类似于下面这样:
当然,在函数原型中可以省略变量名,也可将返回类型指定为类型。这里的要点是,ar 实际上是一个指针,指向传入的数组的第一个元素;另外,由于通过参数传递了元素数,这两个函数都可使用任何长度的数组,只要数组的类型为 double:
这种做法是通过传递两个数字(数组地址和元素数)实现的。正如您看到的,函数缺少一些有关原始数组的知识;例如,它不能使用 sizeof 来获悉原始数组的长度,而必须依赖于程序员传入正确的元素数。
7.3.4 使用数组区间的函数
正如您看到的,对于处理数组的 C++函数,必须将数组中的数据种类、数组的起始位置和数组中元素数量提交给它;传统的 C/C++方法是,将指向数组起始处的指针作为一个参数,将数组长度作为第二个参数(指针指出数组的位置和数据类型),这样便给函数提供了找到所有数据所需的信息。
还有另一种给函数提供所需信息的方法,即指定元素区间(range),这可以通过传递两个指针来完成:一个指针标识数组的开头,另一个指针标识数组的尾部。例如,C++标准模板库(STL,将在第 16 章介绍)将区间方法广义化了。STL 方法使用“超尾”概念来指定区间。也就是说,对于数组而言,标识数组结尾的参数将是指向最后一个元素后面的指针。例如,假设有这样的声明:
则指针 elboud 和 elboud + 20 定义了区间。首先,数组名 elboub 指向第一个元素。表达式 elboud + 19 指向最后一个元素(即 elboud[19]),因此,elboud + 20 指向数组结尾后面的一个位置。将区间传递给函数将告诉函数应处理哪些元素。程序清单 7.8 对程序清单 7.6 做了修改,使用两个指针来指定区间。
程序清单 7.8 arrfun4.cpp
下面是该程序的输出:
程序说明
请注意程序清单 7.8 中 sum_array( ) 函数中的 for 循环:
它将 pt 设置为指向要处理的第一个元素(begin 指向的元素)的指针,并将*pt(元素的值)加入到 total 中。然后,循环通过递增操作来更新 pt,使之指向下一个元素。只要 pt 不等于 end,这一过程就将继续下去。当 pt 等于 end 时,它将指向区间中最后一个元素后面的一个位置,此时循环将结束。
其次,请注意不同的函数调用是如何指定数组中不同的区间的:
指针 cookies + ArSize 指向最后一个元素后面的一个位置(数组有 ArSize 个元素,因此 cookies[ArSize − 1]是最后一个元素,其地址为 cookies + ArSize – 1)。因此,区间[cookies,cookies + ArSize]指定的是整个数组。同样,cookies,cookies + 3 指定了前 3 个元素,依此类推。
请注意,根据指针减法规则,在 sum_arr( ) 中,表达式 end – begin 是一个整数值,等于数组的元素数目。
另外,必须按正确的顺序传递指针,因为这里的代码假定 begin 在前面,end 在后面。
7.3.5 指针和 const
将 const 用于指针有一些很微妙的地方(指针看起来总是很微妙),我们来详细探讨一下。可以用两种不同的方式将 const 关键字用于指针。第一种方法是让指针指向一个常量对象,这样可以防止使用该指针来修改所指向的值,第二种方法是将指针本身声明为常量,这样可以防止改变指针指向的位置。下面来看细节。
首先,声明一个指向常量的指针 pt:
该声明指出,pt 指向一个 const int(这里为 39),因此不能使用 pt 来修改这个值。换句话来说,*pt 的值为 const,不能被修改:
现在来看一个微妙的问题。pt 的声明并不意味着它指向的值实际上就是一个常量,而只是意味着对 pt 而言,这个值是常量。例如,pt 指向 age,而 age 不是 const。可以直接通过 age 变量来修改 age 的值,但不能使用 pt 指针来修改它:
以前我们将常规变量的地址赋给常规指针,而这里将常规变量的地址赋给指向 const 的指针。因此还有两种可能:将 const 变量的地址赋给指向 const 的指针、将 const 的地址赋给常规指针。这两种操作都可行吗?第一种可行,但第二种不可行:
对于第一种情况来说,既不能使用 g_earth 来修改值 9.80,也不能使用 pe 来修改。C++禁止第二种情况的原因很简单——如果将 g_moon 的地址赋给 pm,则可以使用 pm 来修改 g_moon 的值,这使得 g_moon 的 const 状态很荒谬,因此 C++禁止将 const 的地址赋给非 const 指针。如果读者非要这样做,可以使用强制类型转换来突破这种限制,详情请参阅第 15 章中对运算符 const_cast 的讨论。
如果将指针指向指针,则情况将更复杂。前面讲过,假如涉及的是一级间接关系,则将非 const 指针赋给 const 指针是可以的:
然而,进入两级间接关系时,与一级间接关系一样将 const 和非 const 混合的指针赋值方式将不再安全。如果允许这样做,则可以编写这样的代码:
上述代码将非 const 地址(&pl)赋给了 const 指针(pp2),因此可以使用 pl 来修改 const 数据。因此,仅当只有一层间接关系(如指针指向基本数据类型)时,才可以将非 const 地址或指针赋给 const 指针。
注意:
如果数据类型本身并不是指针,则可以将 const 数据或非 const 数据的地址赋给指向 const 的指针,但只能将非 const 数据的地址赋给非 const 指针。
假设有一个由 const 数据组成的数组:
则禁止将常量数组的地址赋给非常量指针将意味着不能将数组名作为参数传递给使用非常量形参的函数:
上述函数调用试图将 const 指针(months)赋给非 const 指针(arr),编译器将禁止这种函数调用。
尽可能使用 const
将指针参数声明为指向常量数据的指针有两条理由:
这样可以避免由于无意间修改数据而导致的编程错误;
使用 const 使得函数能够处理 const 和非 const 实参,否则将只能接受非 const 数据。
如果条件允许,则应将指针形参声明为指向 const 的指针。
为说明另一个微妙之处,请看下面的声明:
第二个声明中的 const 只能防止修改 pt 指向的值(这里为 39),而不能防止修改 pt 的值。也就是说,可以将一个新地址赋给 pt:
但仍然不能使用 pt 来修改它指向的值(现在为 80)。
第二种使用 const 的方式使得无法修改指针的值:
在最后一个声明中,关键字 const 的位置与以前不同。这种声明格式使得 finger 只能指向 sloth,但允许使用 finger 来修改 sloth 的值。中间的声明不允许使用 ps 来修改 sloth 的值,但允许将 ps 指向另一个位置。简而言之,finger 和*ps 都是 const,而*finger 和 ps 不是(参见图 7.5)。
图 7.5 指向 const 的指针和 const 指针
如果愿意,还可以声明指向 const 对象的 const 指针:
其中,stick 只能指向 trouble,而 stick 不能用来修改 trouble 的值。简而言之,stick 和*stick 都是 const。
通常,将指针作为函数参数来传递时,可以使用指向 const 的指针来保护数据。例如,程序清单 7.5 中的 show_array( ) 的原型:
在该声明中使用 const 意味着 show_array( ) 不能修改传递给它的数组中的值。只要只有一层间接关系,就可以使用这种技术。例如,这里的数组元素是基本类型,但如果它们是指针或指向指针的指针,则不能使用 const。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论