- 内容提要
- 前言
- 第 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 复习题答案
5.1 for 循环
很多情况下都需要程序执行重复的任务,如将数组中的元素累加起来或将歌颂生产的赞歌打印 20 份,C++中的 for 循环可以轻松地完成这种任务。我们来看看程序清单 5.1 中,以了解 for 循环所做的工作,然后讨论它是如何工作的。
程序清单 5.1 forloop.cpp
下面是该程序的输出:
该循环首先将整数变量 i 设置为 0:
这是循环的初始化(loop initialization)部分。然后,循环测试(loop test)部分检查 i 是否小于 5:
如果确实小于 5,则程序将执行接下来的语句—循环体(loop body):
然后,程序使用循环更新(loop update)部分将 i 加 1:
这里使用了++运算符—递增运算符(increment operator),它将操作数的值加 1。递增运算符并不仅限于用于 for 循环。例如,在程序中,可以使用 i++;来替换语句 i = i + 1;。将 i 加 1 后,便结束了循环的第一个周期。
接下来,循环开始了新的周期,将新的 i 值与 5 进行比较。由于新值(1)也小于 5,因此循环打印另一行,然后再次将 i 加 1,从而结束这一周期。这样又进入了新的一轮测试、执行语句和更新 i 的值。这一过程将一直进行下去,直到循环将 i 更新为 5 为止。这样,接下来的测试失败,程序将接着执行循环后的语句。
5.1.1 for 循环的组成部分
for 循环为执行重复的操作提供了循序渐进的步骤。我们来具体看一看它是如何工作的。for 循环的组成部分完成下面这些步骤。
1.设置初始值。
2.执行测试,看看循环是否应当继续进行。
3.执行循环操作。
4.更新用于测试的值。
C++循环设计中包括了这些要素,很容易识别。初始化、测试和更新操作构成了控制部分,这些操作由括号括起。其中每部分都是一个表达式,彼此由分号隔开。控制部分后面的语句叫作循环体,只要测试表达式为 true,它便被执行:
C++语法将整个 for 看作一条语句—虽然循环体可以包含一条或多条语句。(包含多条语句时,需要使用复合语句或代码块,这将在本章后面进行讨论。)
循环只执行一次初始化。通常,程序使用该表达式将变量设置为起始值,然后用该变量计算循环周期。
test-expression(测试表达式)决定循环体是否被执行。通常,这个表达式是关系表达式,即对两个值进行比较。这个例子将 i 的值同 5 进行比较,看 i 是否小于 5。如果比较结果为真,则程序将执行循环体。实际上,C++并没有将 test-expression 的值限制为只能为真或假。可以使用任意表达式,C++将把结果强制转换为 bool 类型。因此,值为 0 的表达式将被转换为 bool 值 false,导致循环结束。如果表达式的值为非零,则被强制转换为 bool 值 true,循环将继续进行。程序清单 5.2 通过将表达式 i 用作测试条件来演示了这一特点。更新部分的 i−−与 i++相似,只是每使用一次,i 值就减 1。
程序清单 5.2 num_test.cpp
下面是该程序的输出:
注意,循环在 i 变为 0 后结束。
关系表达式(如 i<5)是如何得到循环终止值 0 的呢?在引入 bool 类型之前,如果关系表达式为 true,则被判定为 1;如果为 false,则被判定为 0。因此,表达式 3<5 的值为 1,而 5<5 的值为 0。然而,C++添加了 bool 类型后,关系表达式就判定为 bool 字面值 true 和 false,而不是 1 和 0 了。这种变化不会导致不兼容的问题,因为 C++程序在需要整数值的地方将把 true 和 false 分别转换为 1 和 0,而在需要 bool 值的地方将把 0 转换为 false,非 0 转换为 true。
for 循环是入口条件(entry-condition)循环。这意味着在每轮循环之前,都将计算测试表达式的值,当测试表达式为 false 时,将不会执行循环体。例如,假设重新运行程序清单 5.2 中的程序,但将起始值设置为 0,则由于测试条件在首次被判定时便为 false,循环体将不被执行:
这种在循环之前进行检查的方式可避免程序遇到麻烦。
update-expression(更新表达式)在每轮循环结束时执行,此时循环体已经执行完毕。通常,它用来对跟踪循环轮次的变量的值进行增减。然而,它可以是任何有效的 C++表达式,还可以是其他控制表达式。这使 for 循环的功能不仅仅是从 0 数到 5(这是第一个循环示例所做的工作),稍后将介绍一些例子。
for 循环体由一条语句组成,不过很快将介绍如何扩展这条规则。图 5.1 对 for 循环设计进行了总结。
for 语句看上去有些像函数调用,因为它使用一个后面跟一对括号的名称。然而,for 是一个 C++关键字,因此编译器不会将 for 视为一个函数,这还将防止将函数命名为 for。
图 5.1 for 循环
提示:
C++常用的方式是,在 for 和括号之间加上一个空格,而省略函数名与括号之间的空格。对于其他控制语句(如 if 和 while),处理方式与 for 相似。这样从视觉上强化了控制语句和函数调用之间的区别。另外,常见的做法是缩进 for 语句体,使它看上去比较显著。
1.表达式和语句
for 语句的控制部分使用 3 个表达式。由于其自身强加的句法限制,C++成为非常具有表现力的语言。任何值或任何有效的值和运算符的组合都是表达式。例如,10 是值为 10 的表达式(一点都不奇怪),28 * 20 是值为 560 的表达式。在 C++中,每个表达式都有值。通常值是很明显的。例如,下面的表达式由两个值和一个加号组成,它的值为 49:
有时值不这么明显,例如,下面是一个表达式,因为它由两个值和一个赋值运算符组成:
C++将赋值表达式的值定义为左侧成员的值,因此这个表达式的值为 20。由于赋值表达式有值,因此可以编写下面这样的语句:
表达式 cooks = 4 的值为 4,因此 maids 的值为 7。然而,C++虽然允许这样做,但并不意味着应鼓励这种做法。允许存在上述语句存在的原则也允许编写如下的语句:
这种方法可以快速地将若干个变量设置为相同的值。优先级表(见附录 D)表明,赋值运算符是从右向左结合的,因此首先将 0 赋给 z,然后将 z = 0 赋给 y,依此类推。
最后,正如前面指出的,像 x<y 这样的关系表达式将被判定为 bool 值 true 或 false。程序清单 5.3 中的小程序指出了有关表达式值的一些重要方面。<<运算符的优先级比表达式中使用的运算符高,因此代码使用括号来获得正确的运算顺序。
程序清单 5.3 express.cpp
注意:
老式 C++实现可能要求使用 ios:boolalpha,而不是 ios_base::boolalpha 来作为 cout.setf( ) 的参数。有些老式实现甚至无法识别这两种形式。
下面是该程序的输出:
通常,cout 在显示 bool 值之前将它们转换为 int,但 cout.setf(ios::boolalpha)函数调用设置了一个标记,该标记命令 cout 显示 true 和 false,而不是 1 和 0。
注意:
C++表达式是值或值与运算符的组合,每个 C++表达式都有值。
为判定表达式 x = 100,C++必须将 100 赋给 x。当判定表达式的值这种操作改变了内存中数据的值时,我们说表达式有副作用(side effect)。因此,判定赋值表达式会带来这样的副作用,即修改被赋值者的值。有可能把赋值看作预期的效果,但从 C++的构造方式这个角度来看,判定表达式才是主要作用。并不是所有的表达式都有副作用。例如,判定 x + 15 将计算出一个新的值,但不会修改 x 的值。然而,判定++x + 15 就有副作用,因为它将 x 加 1。
从表达式到语句的转变很容易,只要加分号即可。因此下面是一个表达式:
而下面是一条语句:
更准确地说,这是一条表达式语句。只要加上分号,所有的表达式都可以成为语句,但不一定有编程意义。例如,如果 rodents 是个变量,则下面就是一条有效的 C++语句:
编译器允许这样的语句,但它没有完成任何有用的工作。程序仅仅是计算和,而没有使用得到的结果,然后便进入下一条语句(智能编译器甚至可能跳过这条语句)。
2.非表达式和语句
有些概念对于理解 C++至关重要,如了解 for 循环的结构。不过句法中也有一些相对次要的内容,让认为自己理解语言的人突然觉得不知所措。下面来看看这样的内容。
对任何表达式加上分号都可以成为语句,但是这句话反过来说就不对了。也就是说,从语句中删除分号,并不一定能将它转换为表达式。就我们目前使用的语句而言,返回语句、声明语句和 for 语句都不满足“语句=表达式+分号”这种模式。例如,下面是一条语句:
但 int toad 并不是表达式,因为它没有值。因此,下面的代码是非法的:
同样,不能把 for 循环赋给变量。在下面的示例中,for 循环不是表达式,因此没有值,也不能给它赋值:
3.修改规则
C++在 C 循环的基础上添加了一项特性,要求对 for 循环句法做一些微妙的调整。
这是原来的句法:
具体地说,正如本章前面指出的,for 结构的控制部分由 3 个表达式组成,它们由分号分隔。然而,C++循环允许像下面这样做:
也就是说,可以在 for 循环的初始化部分中声明变量。这很方便,但并不适用于原来的句法,因为声明不是表达式。这种一度是非法的行为最初是通过定义一种新的表达式—声明语句表达式(declaration-statement expression)—来合法化的,声明语句表达式不带分号声明,只能出现在 for 语句中。然而,这种调整已经被取消了,代之以将 for 语句的句法修改成下面这样:
乍一看很奇怪,因为这里只有一个分号(而不是两个分号)。但是这是允许的,因为 for-init-statement 被视为一条语句,而语句有自己的分号。对于 for-init-statement 来说,它既可以是表达式语句,也可以是声明。这种句法规则用语句替换了后面跟分号的表达式,语句本身有自己的分号。总之,C++程序员希望能够在 for 循环初始化部分中声明和初始化变量,他们会做 C++句法需要和英语所允许的工作。
在 for-init-statement 中声明变量还有其实用的一面,这也是应该知道的。这种变量只存在于 for 语句中,也就是说,当程序离开循环后,这种变量将消失:
您还应知道的一点是,有些较老的 C++实现遵循以前的规则,对于前面的循环,将把 i 视为是在循环之前声明的,因此在循环结束后,i 仍可用。
5.1.2 回到 for 循环
下面使用 for 循环完成更多的工作。程序清单 5.4 使用循环来计算并存储前 16 个阶乘。阶乘的计算方式如下:零阶乘写作 0!,被定义为 1。1!是 1*0!,即 1。2!为 2*1!,即 2。3!为 3*2!,即 6,依此类推。每个整数的阶乘都是该整数与前一个阶乘的乘积(钢琴家 Victor Borge 最著名的独白以其语音标点为特色,其中,惊叹号的发音就像 phffft pptz,带有濡湿的口音。然而,刚才提到的“!”读作“阶乘”)。该程序用一个循环来计算连续阶乘的值,并将这些值存储在数组中。然后,用另一个循环来显示结果。另外,该程序还在外部声明了一些值。
程序清单 5.4 formore.cpp
下面是该程序的输出:
阶乘增加得很快!
注意:
这个程序清单使用了类型 long long。如果您的系统不支持这种类型,可使用 double。然而,整型使得阶乘的增大方式看起来更明显。
程序说明
该程序创建了一个数组来存储阶乘值。元素 0 存储 0!,元素 1 存储 1!,依此类推。由于前两个阶乘都等于 1,因此程序将 factorials 数组的前两个元素设置为 1(记住,数组第一个元素的索引值为 0)。然后,程序用循环将每个阶乘设置为索引号与前一个阶乘的乘积。该循环表明,可以在循环体中使用循环计数。
该程序演示了 for 循环如何通过提供一种访问每个数组成员的方便途径来与数组协同工作。另外,formore.cpp 还使用 const 创建了数组长度的符号表示(ArSize)。然后,它在需要数组长度的地方使用 ArSize,如定义数组以及限制循环如何处理数组时。现在,如果要将程序扩展成处理 20 个阶乘,则只需要将 ArSize 设置为 20 并重新编译程序即可。通过使用符号常量,就可以避免将所有的 10 修改为 20。
提示:
通常,定义一个 const 值来表示数组中的元素个数是个好办法。在声明数组和引用数组长度时(如在 for 循环中),可以使用 const 值。
表达式 i < ArSize 反映了这样一个事实,包含 ArSize 个元素的数组的下标从 0 到 ArSize – 1,因此数组索引应在 ArSize 减 1 的位置停止。也可以使用 i <= ArSize –1,但它看上去没有前面的表达式好。
该程序在 main( ) 的外面声明 const int 变量 ArSize。第 4 章末尾提到过,这样可以使 ArSize 成为外部数据。以这种方式声明 ArSize 的两种后果是,ArSize 在整个程序周期内存在、程序文件中所有的函数都可以使用它。在这个例子中,程序只有一个函数,因此在外部声明 ArSize 几乎没有任何实际用处,但包含多个函数的程序常常会受益于共享外部常量,因此我们现在就开始练习使用外部变量。
另外,这个示例还提醒您,可使用 std::而不是编译指令 using 来让选定的标准名称可用。
5.1.3 修改步长
到现在为止,循环示例每一轮循环都将循环计数加 1 或减 1。可以通过修改更新表达式来修改步长。例如,程序清单 5.5 中的程序按照用户选择的步长值将循环计数递增。它没有将 i++用作更新表达式,而是使用表达式 i = i + by,其中 by 是用户选择的步长值。
程序清单 5.5 bigstep.cpp
下面是该程序的运行情况:
当 i 的值到达 102 时,循环终止。这里的重点是,更新表达式可以是任何有效的表达式。例如,如果要求每轮递增以 i 的平方加 10,则可以使用表达式 i = i * i + 10。
需要指出的另一点是,检测不等通常比检测相等好。例如,在这里使用条件 i == 100 不可行,因为 i 的取值不会为 100。
最后,这个示例使用了 using 声明,而不是 using 编译指令。
5.1.4 使用 for 循环访问字符串
for 循环提供了一种依次访问字符串中每个字符的方式。例如,程序清单 5.6 让用户能够输入一个字符串,然后按相反的方向逐个字符地显示该字符串。在这个例子中,可以使用 string 对象,也可以使用 char 数组,因为它们都让您能够使用数组表示法来访问字符串中的字符。程序清单 5.6 使用的是 string 对象。string 类的 size( ) 获得字符串中的字符数;循环在其初始化表达式中使用这个值,将 i 设置为字符串中最后一个字符的索引(不考虑空值字符)。为了反向计数,程序使用递减运算符(− −),在每轮循环后将数组下标减 1。另外,程序清单 5.6 使用关系运算符大于或等于(>=)来测试循环是否到达第一个元素。稍后我们将对所有的关系运算符做一总结。
程序清单 5.6 forstr1.cpp
注意:
如果所用的实现没有添加新的头文件,则必须使用 string.h,而不是 cstring。
下面是该程序的运行情况:
程序成功地按相反的方向打印了 animal;与回文 rotator、redder 或 stats 相比,animal 能更清晰地说明这个程序的作用。
5.1.5 递增运算符(++)和递减运算符(−−)
C++中有多个常被用在循环中的运算符,因此我们花一点时间来讨论它们。前面已经介绍了两个这样的运算符:递增运算符(++)(名称 C++由此得到)和递减运算符(−−)。这两个运算符执行两种极其常见的循环操作:将循环计数加 1 或减 1。然而,它们还有很多特点不为读者所知。这两个运算符都有两种变体。前缀(prefix)版本位于操作数前面,如++x;后缀(postfix)版本位于操作数后面,如 x++。两个版本对操作数的影响是一样的,但是影响的时间不同。这就像对于钱包来说,清理草坪之前付钱和清理草坪之后付钱的最终结果是一样的,但支付钱的时间不同。程序清单 5.7 演示递增运算符的这种差别。
程序清单 5.7 plus_one.cpp
下面是该程序的输出:
粗略地讲,a++意味着使用 a 的当前值计算表达式,然后将 a 的值加 1;而++b 的意思是先将 b 的值加 1,然后使用新的值来计算表达式。例如,我们有下面这样的关系:
递增和递减运算符是处理将值加减 1 这种常见任务的一种简约、方便的方法。
递增运算符和递减运算符都是漂亮的小型运算符,不过千万不要失去控制,在同一条语句对同一个值递增或递减多次。问题在于,规则“使用后修改”和“修改后使用”可能会变得模糊不清。也就是说,下面这条语句在不同的系统上将生成不同的结果:
对这种语句,C++没有定义正确的行为。
5.1.6 副作用和顺序点
下面更详细地介绍 C++就递增运算符何时生效的哪些方面做了规定,哪些方面没有规定。首先,副作用(side effect)指的是在计算表达式时对某些东西(如存储在变量中的值)进行了修改;顺序点(sequence point)是程序执行过程中的一个点,在这里,进入下一步之前将确保对所有的副作用都进行了评估。在 C++中,语句中的分号就是一个顺序点,这意味着程序处理下一条语句之前,赋值运算符、递增运算符和递减运算符执行的所有修改都必须完成。本章后面将讨论的有些操作也有顺序 点。另外,任何完整的表达式末尾都是一个顺序点。
何为完整表达式呢?它是这样一个表达式:不是另一个更大表达式的子表达式。完整表达式的例子有:表达式语句中的表达式部分以及用作 while 循环中检测条件的表达式。
顺序点有助于阐明后缀递增何时进行。例如,请看下面的代码:
while 循环将在本章后面讨论,它类似于只有测试表达式的 for 循环。在这里,C++新手可能认为“使用值,然后递增”意味着先在 cout 语句中使用 guests 的值,再将其值加 1。然而,表达式 guests++ < 10 是一个完整表达式,因为它是一个 while 循环的测试条件,因此该表达式的末尾是一个顺序点。所以,C++确保副作用(将 guests 加 1)在程序进入 cout 之前完成。然而,通过使用后缀格式,可确保将 guests 同 10 进行比较后再将其值加 1。
现在来看下面的语句:
表达式 4 + x++不是一个完整表达式,因此,C++不保证 x 的值在计算子表达式 4 + x++后立刻增加 1。在这个例子中,整条赋值语句是一个完整表达式,而分号标示了顺序点,因此 C++只保证程序执行到下一条语句之前,x 的值将被递增两次。C++没有规定是在计算每个子表达式之后将 x 的值递增,还是在整个表达式计算完毕后才将 x 的值递增,有鉴于此,您应避免使用这样的表达式。
在 C++11 文档中,不再使用术语“顺序点”了,因为这个概念难以用于讨论多线程执行。相反,使用了术语“顺序”,它表示有些事件在其他事件前发生。这种描述方法并非要改变规则,而旨在更清晰地描述多线程编程。
5.1.7 前缀格式和后缀格式
显然,如果变量被用于某些目的(如用作函数参数或给变量赋值),使用前缀格式和后缀格式的结果将不同。然而,如果递增表达式的值没有被使用,情况又如何呢?例如,下面两条语句的作用是否不同?
下面两条语句的作用是否不同?
和
从逻辑上说,在上述两种情形下,使用前缀格式和后缀格式没有任何区别。表达式的值未被使用,因此只存在副作用。在上面的例子中,使用这些运算符的表达式为完整表达式,因此将 x 加 1 和 n 减 1 的副作用将在程序进入下一步之前完成,前缀格式和后缀格式的最终效果相同。
然而,虽然选择使用前缀格式还是后缀格式对程序的行为没有影响,但执行速度可能有细微的差别。对于内置类型和当代的编译器而言,这看似不是什么问题。然而,C++允许您针对类定义这些运算符,在这种情况下,用户这样定义前缀函数:将值加 1,然后返回结果;但后缀版本首先复制一个副本,将其加 1,然后将复制的副本返回。因此,对于类而言,前缀版本的效率比后缀版本高。
总之,对于内置类型,采用哪种格式不会有差别;但对于用户定义的类型,如果有用户定义的递增和递减运算符,则前缀格式的效率更高。
5.1.8 递增/递减运算符和指针
可以将递增运算符用于指针和基本变量。本书前面介绍过,将递增运算符用于指针时,将把指针的值增加其指向的数据类型占用的字节数,这种规则适用于对指针递增和递减:
也可以结合使用这些运算符和*运算符来修改指针指向的值。将*和++同时用于指针时提出了这样的问题:将什么解除引用,将什么递增。这取决于运算符的位置和优先级。前缀递增、前缀递减和解除引用运算符的优先级相同,以从右到左的方式进行结合。后缀递增和后缀递减的优先级相同,但比前缀运算符的优先级高,这两个运算符以从左到右的方式进行结合。
前缀运算符的从右到到结合规则意味着*++pt 的含义如下:现将++应用于 pt(因为++位于*的右边),然后将*应用于被递增后的 pt:
另一方面,++*pt 意味着先取得 pt 指向的值,然后将这个值加 1:
在这种情况下,pt 仍然指向 arr[2]。
接下来,请看下面的组合:
圆括号指出,首先对指针解除引用,得到 24.4。然后,运算符++将这个值递增到 25.4,pt 仍然指向 arr[2]。
最后,来看看下面的组合:
后缀运算符++的优先级更高,这意味着将运算符用于 pt,而不是*pt,因此对指针递增。然而后缀运算符意味着将对原来的地址(&arr[2])而不是递增后的新地址解除引用,因此*pt++的值为 arr[2],即 25.4,但该语句执行完毕后,pt 的值将为 arr[3]的地址。
注意:
指针递增和递减遵循指针算术规则。因此,如果 pt 指向某个数组的第一个元素,++pt 将修改 pt,使之指向第二个元素。
5.1.9 组合赋值运算符
程序清单 5.5 使用了下面的表达式来更新循环计数:
C++有一种合并了加法和赋值操作的运算符,能够更简洁地完成这种任务:
+=运算符将两个操作数相加,并将结果赋给左边的操作数。这意味着左边的操作数必须能够被赋值,如变量、数组元素、结构成员或通过对指针解除引用来标识的数据:
每个算术运算符都有其对应的组合赋值运算符,表 5.1 对它们进行了总结。其中每个运算符的工作方式都和+=相似。因此,下面的语句将 k 与 10 相乘,再将结果赋给 k:
表 5.1 组合赋值运算符
操 作 符 | 作用(L 为左操作数,R 为右操作数) |
---|---|
+= | 将 L+R 赋给 L |
-= | 将 L-R 赋给 L |
*= | 将 L*R 赋给 L |
/= | 将 L/R 赋给 L |
%= | 将 L%R 赋给 L |
5.1.10 复合语句(语句块)
编写 C++for 语句的格式(或句法)看上去可能比较严格,因为循环体必须是一条语句。如果要在循环体中包含多条语句,这将很不方便。所幸的是,C++提供了避开这种限制的方式,通过这种方式可以在循环体中包含任意多条语句。方法是用两个花括号来构造一条复合语句(代码块)。代码块由一对花括号和它们包含的语句组成,被视为一条语句,从而满足句法的要求。例如,程序清单 5.8 中的程序使用花括号将 3 条语句合并为一个代码块。这样,循环体便能够提示用户、读取输入并进行计算。该程序计算用户输入的数字的和,因此有机会使用+=运算符。
程序清单 5.8 block.cpp
下面是该程序的运行情况:
假设对循环体进行了缩进,但省略了花括号:
编译器将忽略缩进,因此只有第一条语句位于循环中。因此,该循环将只打印出 5 条提示,而不执行其他操作。循环结束后,程序移到后面几行执行,只读取和计算一个数字。
复合语句还有一种有趣的特性。如果在语句块中定义一个新的变量,则仅当程序执行该语句块中的语句时,该变量才存在。执行完该语句块后,变量将被释放。这表明此变量仅在该语句块中才是可用的:
注意,在外部语句块中定义的变量在内部语句块中也是被定义了的。
如果在一个语句块中声明一个变量,而外部语句块中也有一个这种名称的变量,情况将如何呢?在声明位置到内部语句块结束的范围之内,新变量将隐藏旧变量;然后就变量再次可见,如下例所示:
5.1.11 其他语法技巧—逗号运算符
正如读者看到的,语句块允许把两条或更多条语句放到按 C++句法只能放一条语句的地方。逗号运算符对表达式完成同样的任务,允许将两个表达式放到 C++句法只允许放一个表达式的地方。例如,假设有一个循环,每轮都将一个变量加 1,而将另一个变量减 1。在 for 循环控制部分的更新部分中完成这两项工作将非常方便,但循环句法只允许这里包含一个表达式。在这种情况下,可以使用逗号运算符将两个表达式合并为一个:
逗号并不总是逗号运算符。例如,下面这个声明中的逗号将变量列表中相邻的名称分开:
程序清单 5.9 在一个程序中使用了两次逗号运算符,该程序将一个 string 类对象的内容反转。也可以使用 char 数组来编写该程序,但可输入的单词长度将受 char 数组大小的限制。注意,程序清单 5.6 按相反的顺序显示数组的内容,而程序清单 5.9 将数组中的字符顺序反转。该程序还使用了语句块将几条语句组合成一条。
程序清单 5.9 forstr2.cpp
下面是该程序运行情况:
顺便说一句,在反转字符串方面,string 类提供了更为简洁的方式,这将在第 16 章介绍。
1.程序说明
来看程序清单 5.9 中的 for 循环控制部分。
首先,它使用逗号运算符将两个初始化操作放进控制部分第一部分的表达式中。然后,再次使用逗号运算符将两个更新合并到控制部分最后一部分的表达式中。
接下来看循环体。程序用括号将几条语句合并为一个整体。在循环体中,程序将数组第一个元素和最后一个元素调换,从而将单词反转过来。然后,它将 j 加 1,将 i 减 1,让它们分别指向第二个元素和倒数第二个元素,然后将这两个元素调换。注意,测试条件 j<i 使得到达数组的中间时,循环将停止。如果过了这一点后,循环仍继续下去,则便开始将交换后的元素回到原来的位置(参见图 5.2)。
图 5.2 反转字符串
需要注意的另一点是,声明变量 temp、i、j 的位置。代码在循环之前声明 i 和 j,因为不能用逗号运算符将两个声明组合起来。这是因为声明已经将逗号用于其他用途—分隔列表中的变量。也可以使用一个声明语句表达式来创建并初始化两个变量,但是这样看起来有些乱:
在这种情况下,逗号只是一个列表分隔符,而不是逗号运算符,因此该表达式对 j 和 i 进行声明和初始化。然而,看上去好像只声明了 j。
另外,可以在 for 循环内部声明 temp:
这样,temp 在每轮循环中都将被分配和释放。这比在循环前声明 temp 的速度要慢一些。另一方面,如果在循环内部声明 temp,则它将在循环结束后被丢弃。
2.逗号运算符花絮
到目前为止,逗号运算符最常见的用途是将两个或更多的表达式放到一个 for 循环表达式中。不过 C++还为这个运算符提供了另外两个特性。首先,它确保先计算第一个表达式,然后计算第二个表达式(换句话说,逗号运算符是一个顺序点)。如下所示的表达式是安全的:
其次,C++规定,逗号表达式的值是第二部分的值。例如,上述表达式的值为 40,因为 j = 2 * i 的值为 40。
在所有运算符中,逗号运算符的优先级是最低的。例如,下面的语句:
被解释为:
也就是说,将 cats 设置为 17,240 不起作用。然而,由于括号的优先级最高,下面的表达式将把 cats 设置为 240—逗号右侧的表达式值:
5.1.12 关系表达式
计算机不只是机械的数字计数器。它能够对值进行比较,这种能力是计算机决策的基础。在 C++中,关系运算符是这种能力的体现。C++提供了 6 种关系运算符来对数字进行比较。由于字符用其 ASCII 码表示,因此也可以将这些运算符用于字符。不能将它们用于 C-风格字符串,但可用于 string 类对象。对于所有的关系表达式,如果比较结果为真,则其值将为 true,否则为 false,因此可将其用作循环测试表达式。(老式实现认为结果为 true 的关系表达式的值为 1,而结果为 false 的关系表达式为 0。)表 5.2 对这些运算符进行了总结。
表 5.2 关系运算符
操 作 符 | 含 义 |
---|---|
< | 小于 |
<= | 小于或等于 |
= = | 等于 |
> | 大于 |
>= | 大于或等于 |
!= | 不等于 |
这 6 种关系运算符可以在 C++中完成对数字的所有比较。如果要对两个值进行比较,看看哪个值更漂亮或者更幸运,则这里的运算符就派不上用场了。
下面是一些测试示例:
关系运算符的优先级比算术运算符低。这意味着表达式:
对应于:
而不是:
由于将 bool 值提升为 int 后,表达式(3>y) 要么为 1,要么为 0,因此第二个和第三个表达式都是有效的。不过我们更希望第一个表达式等价于第二个表达式,而 C++正是这样做的。
5.1.13 赋值、比较和可能犯的错误
不要混淆等于运算符(= =)与赋值运算符(=)。下面的表达式问了一个音乐问题—musicians 是否等于 4?
该表达式的值为 true 或 false。下面的表达式将 4 赋给 musicians:
在这里,整个表达式的值为 4,因为该表达式左边的值为 4。
for 循环的灵活设计让用户很容易出错。如果不小心遗漏了= =运算符中的一个等号,则 for 循环的测试部分将是一个赋值表达式,而不是关系表达式,此时代码仍是有效的。这是因为可以将任何有效的 C++表达式用作 for 循环的测试条件。别忘了,非零值为 true,零值为 false。将 4 赋给 musicians 的表达式的值为 4,因此被视为 true。如果以前使用过用=判断是否相等的语言,如 Pascal 或 BASIC,则尤其可能出现这样的错误。
程序清单 5.10 中指出了可能出现这种错误的情况。该程序试图检查一个存储了测验成绩的数组,在遇到第一个不为 20 的成绩时停止。该程序首先演示了一个正确进行比较的循环,然后是一个在测试条件中错误地使用了赋值运算符的循环。该程序还有另一个重大的设计错误,稍后将介绍如何修复(应从错误中吸取教训,而程序清单 5.10 在这方面很有帮助)。
程序清单 5.10 equal.cpp
由于这个程序存在一个严重的问题,读者可能希望了解它,以便真正运行它。下面是该程序的一些输出:
第一个循环在显示了前 5 个测验成绩后正确地终止,但第二个循环显示整个数组。更糟糕的是,显示的每个值都是 20。更加糟糕的是,它到了数组末尾还不停止。最糟糕的是,该程序可能导致其他应用程序无法运行,您必须重新启动计算机。
当然,错误出在下面的测试表达式中:
首先,由于它将一个非零值赋给数组元素,因此表达式始终为非零,所以始终为 true。其次,由于表达式将值赋给数组元素,它实际上修改了数据。第三,由于测试表达式一直为 true,因此程序在到达数组结尾后,仍不断修改数据。它把一个又一个 20 放入内存中!这会带来不好的影响。
发现这种错误的困难之处在于,代码在语法上是正确的,因此编译器不会将其视为错误(然而,由于 C 和 C++程序员频繁地犯这种错误,因此很多编译器都会发出警告,询问这是否是设计者的真正意图)。
警告:
不要使用=来比较两个量是否相等,而要使用= =。
和 C 语言一样,C++比起大多数编程语言来说,赋予程序员更大的自由。这种自由以程序员应付的更大责任为代价。只有良好的规划才能避免程序超出标准 C++数组的边界。然而,对于 C++类,可以设计一种保护数组类型来防止这种错误,第 13 章提供一个这样的例子。另外,应在需要的时候在程序中加入保护措施。例如,在程序清单 5.10 的循环中,应包括防止超出最后一个成员的测试,这甚至对于“好”的循环来说也是必要的。如果所有的成绩都是 20,“好”的循环也会超出数组边界。总之,循环需要测试数组的值和索引的值。第 6 章将介绍如何使用逻辑运算符将两个这样的测试合并为一个条件。
5.1.14 C-风格字符串的比较
假设要知道字符数组中的字符串是不是 mate。如果 word 是数组名,下面的测试可能并不能像我们预想的那样工作:
请记住,数组名是数组的地址。同样,用引号括起的字符串常量也是其地址。因此,上面的关系表达式不是判断两个字符串是否相同,而是查看它们是否存储在相同的地址上。两个字符串的地址是否相同呢?答案是否定的,虽然它们包含相同的字符。
由于 C++将 C-风格字符串视为地址,因此如果使用关系运算符来比较它们,将无法得到满意的结果。相反,应使用 C-风格字符串库中的 strcmp( ) 函数来比较。该函数接受两个字符串地址作为参数。这意味着参数可以是指针、字符串常量或字符数组名。如果两个字符串相同,该函数将返回零;如果第一个字符串按字母顺序排在第二个字符串之前,则 strcmp( ) 将返回一个负数值;如果第一个字符串按字母顺序排在第二个字符串之后,则 strcpm( ) 将返回一个正数值。实际上,“按系统排列顺序”比“按字母顺序”更准确。这意味着字符是根据字符的系统编码来进行比较的。例如,使用 ASCII 码时,所有大写字母的编码都比小写字母小,所以按排列顺序,大写字母将位于小写字母之前。因此,字符串“Zoo”在字符串“aviary”之前。根据编码进行比较还意味着大写字母和小写字母是不同的,因此字符串“FOO”和字符串“foo”不同。
在有些语言(如 BASIC 和标准 Pascal)中,存储在不同长度的数组中的字符串彼此不相等。但是 C-风格字符串是通过结尾的空值字符定义的,而不是由其所在数组的长度定义的。这意味着两个字符串即使被存储在长度不同的数组中,也可能是相同的:
顺便说一句,虽然不能用关系运算符来比较字符串,但却可以用它们来比较字符,因为字符实际上是整型。因此下面的代码可以用来显示字母表中的字符,至少对于 ASCII 字符集和 Unicode 字符集来说是有效的:
程序清单 5.11 在 for 循环的测试条件中使用了 strcmp( )。该程序显示一个单词,修改其首字母,然后再次显示这个单词,这样循环往复,直到 strcmp( ) 确定该单词与字符串“mate”相同为止。注意,该程序清单包含了文件 cstring,因为它提供了 strcmp( ) 的函数原型。
程序清单 5.11 compstr1.cpp
下面是该程序的输出:
程序说明
该程序有几个有趣的地方。其中之一当然是测试。我们希望只要 word 不是 mate,循环就继续进行。也就是说,我们希望只要 strcmp( ) 判断出两个字符串不相同,测试就继续进行。最显而易见的测试是这样的:
如果字符串不相等,则该语句的值为 1(true),如果字符串相等,则该语句的值为 0(false)。但使用 strcmp(word,"mate")本身将如何呢?如果字符串不相等,则它的值为非零(true);如果字符串相等,则它的值为零(false)。实际上,如果字符串不同,该返回 true,否则返回 false。因此,可以只用这个函数,而不是整个关系表达式。这样得到的结果将相同,还可以少输入几个字符。另外,C 和 C++程序员传统上就是用这种方式使用 strcmp( ) 的。
检测相等或排列顺序:
可以使用 strcmp( ) 来测试 C-风格字符串是否相等(排列顺序)。如果 str1 和 str2 相等,则下面的表达式为 true:
如果 str1 和 str2 不相等,则下面两个表达式都为 true:
如果 str1 在 str2 的前面,则下面的表达式为 true:
如果 str1 在 str2 的后面,则下面的表达式为 true:
因此,根据要如何设置测试条件,strcmp( ) 可以扮演= =、!=、<和>运算符的角色。
接下来,compstr1.cpp 使用递增运算符使变量 ch 遍历字母表:
可以对字符变量使用递增运算符和递减运算符,因为 char 类型实际上是整型,因此这种操作实际上将修改存储在变量中的整数编码。另外,使用数组索引可使修改字符串中的字符更为简单:
5.1.15 比较 string 类字符串
如果使用 string 类字符串而不是 C-风格字符串,比较起来将简单些,因为类设计让您能够使用关系运算符进行比较。这之所以可行,是因为类函数重载(重新定义)了这些运算符。第 12 章将介绍如何将这种特性加入到类设计中,但从应用的角度说,读者现在只需知道可以将关系运算符用于 string 对象即可。程序清单 5.12 是在程序清单 5.11 的基础上修改而成的,它使用的是 string 对象而不是 char 数组。
程序清单 5.12 compstr2.cpp
该程序的输出与程序清单 5.11 相同。
程序说明
在程序清单 5.12 中,下面的测试条件使用了一个关系运算符,该运算符的左边是一个 string 对象,右边是一个 C-风格字符串:
string 类重载运算符!=的方式让您能够在下述条件下使用它:至少有一个操作数为 string 对象,另一个操作数可以是 string 对象,也可以是 C-风格字符串。
string 类的设计让您能够将 string 对象作为一个实体(在关系型测试表达式中),也可以将其作为一个聚合对象,从而使用数组表示法来提取其中的字符。
正如您看到的,使用 C-风格字符串和 string 对象可获得相同的结果,但使用 string 对象更简单、更直观。
最后,和前面大多数 for 循环不同,此循环不是计数循环。也就是说,它并不对语句块执行指定的次数。相反,此循环将根据情况(word 为“mate”)来确定是否停止。对于这种测试,C++程序通常使用 while 循环,下面来看看这种循环。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论