首页 新闻 论坛 群组 Blog 文档 下载 读书 Tag 网摘 搜索 开源 FAQ 第二书店 博文视点 程序员
频道: 研发 数据库 中间件 信息化 视频 .NET Java 游戏 移动 服务: 人才 外包 培训
    图书品种:235680
       
热门搜索: ASP.NET Ajax Spring Hibernate Java

3.3   循环

C++拥有三种主要的结构来处理循环。它还有一种方式来“滚动循环”,然而,优秀的程序员一般都避开那个选项。

语言注解:某些语言,特别是函数式语言,使用递归作为其主要的重复工具。在C++中也可以使用这个机制,但结果代码通常不如使用C++本身的循环机制所编写的代码好。当把递归作为循环机制使用时,C++编译器往往不能生成优秀的目标代码。将递归用于循环还会让其他C++程序员难以读懂你的代码,因为你的代码编写方式对他们来说可能很陌生。

3.3.1   do-while循环

我们已经以一种高度特殊化的形式使用过do-while循环,可以重复执行某些动作,直到遇到一个导致跳出循环的内部条件为止。do-while循环的一般形式是:

用伪代码表示,do-while循环具有这样的效果:

它总是至少执行一次动作(action)部分。然后程序检查是否应该回去重复动作部分。程序会保持重复动作部分,直到发生下面两件事情之一为止:while子句中的布尔表达式的值为false,或者内部动作之一强行跳出。第一个选项很常见,尽管迄今为止书中我们尚未使用这种形式。例如,以下代码片断显示数字0到9以及它们的平方:

实   验

编写一个小程序,加入以上代码片断。编译、连接并执行,检查输出是否如预期的一样。现在将++i改为i++,编译并执行结果。不同的结果令你吃惊吗?你理解这个区别吗?我们有时需要注意究竟该使用前自增还是后自增。

3.3.2   while循环

while循环在开始处执行条件测试,它大概比do-while还要常用。主要的区别在于do-while循环总是至少执行一次,而while循环立即进行测试并且只有测试求得值为true时,才会执行动作(action)部分。在C++中,while循环的形式是:

用伪代码表示,while循环有这样的效果(可拿它与do-while循环的伪代码对比):

目前为止我可以在本书中每一个使用了do-while(true)循环的地方直截了当地使用while(true)循环。事实上,或许对依赖内部跳出的循环使用这种方式更好,因为它警告代码阅读者留意一个内部的break语句或return语句。我选择别的方式的原因,部分因为是那么做使我得以在这里拿while循环举例子。两种选择之间没有太多的区别,但把稍微好一点的放在第二位给出,也许有助于你记住C++常常给你选择的机会。

我们可以将上文的代码片断写成:

然而,结果不完全一样。

抵制诱惑,切勿这么写:

如果你编写的代码,在同一条语句中既递增了一个变量,又第二次使用那个变量,那么就可能会发生潜在的非常危险的事情,注意我说的“潜在的”一词。这样的语句拥有“未定义行为(undefined behavior)”(我将在第6章中明确地介绍它)。这意味着C++标准没有对这样的代码作任何要求。它可以做你所期望的事情,它也可以做某些不同的事情,甚至是一些灾难性的事情。我曾经写过一个程序,因为未定义行为而改编(reprogrammmed)了一块昂贵的显卡的BIOS。使得未定义行为难以理解的原因是,程序可以做你所期待的事情,但那不是硬性要求。

随着我们的进展,我将指出其他潜在的未定义行为。然而,你应该注意这并不是C++的专利:所有编程语言都有未定义行为的情况,只不过C++在这方面比某些语言更容易出问题。

经验丰富的C++程序员恪守一个简单的方针:“不在带有其他变量的表达式中使用自增和自减操作符”。这仅仅是一个方针,因此在有的地方老练的程序员可能会忽视它。然而,你要有一个充分的理由才能这么做。绝对的准则是:“永远不要在同一个完整的表达式中使用一个正在增加或减少的变量的第二个实例。”目前,你可以将“完整的表达式”想像为诸如决策或循环中的控制表达式之类的东西,或者是任何以分号结尾的表达式。

实   验

修改之前的程序,用while循环代替do-while循环。检验结果并决定如何修改程序,以便你可以获得和从前一样的结果。

3.3.3   for循环

C++中最常用的第三个循环结构是for循环。一旦你找到了感觉,便可轻松驾驭它,但一开始可能会有点生疏。for循环的形式是:

用伪代码表示,for循环具有这样的效果:

让我来分步讲解这些部分。

初始化

初始化部分只执行一次。它陈述了第一次进入循环前必须要完成的事情。它可以为空(即什么也不做),可以在循环开始前为许多变量设定初始值,还可以用于定义某个类型的一个或多个变量。C++中最常见的是最后一项,定义一个将要控制该循环的变量。

控制表达式

控制表达式总是在动作语句的每次执行之前被执行,以决定是否应该再执行一次动作语句(如果是第一次的话,则决定是否应该执行它一次)。一旦此表达式求得值为false,循环就立即终止。这个部分可以为空,但如果是这样,循环将不得不使用一个内部跳出——正如我们在dowhile(true)循环中所做的那样。换句话说,省去控制表达式等价于在那里写上true。

终止

终止部分是可选的动作,它的执行位于每一次通过(pass through)循环的末尾,且在对下一次控制表达式的测试之前。注意,它发生在每一次通过的结束处,所以第一次通过之前它不会被执行。

递归

我在本章中数次提到递归。某些程序员可能还不熟悉这个概念,其他人则可能迷惑不解:它怎么会是循环的替补方案呢。这里有一个简短的程序,举例说明了递归及其循环用法。

这段代码工作起来令人非常满意,并且某些编译器也许能高效地编译它,但因为在C++中一般不这样使用递归,所以我们不指望C++编译器能够高效地处理这段代码。就编译器而言,它需要10个print_square的参数的实例,而我们正常的C++版的程序只使用单一控制变量。从函数式编程的观点来看,递归形式更为清晰,因为它不使用任何变量,只有纯粹的值。然而,C++编译器并没有专为处理纯值和递归而进行调节。

如果你仍然感到迷惑,术语递归指的是函数调用它自身的情况,如print_square在上述例子中所做的一样。就像循环一样,递归函数需要某种方式停下来。与循环不同的是,当失去控制时,它可以迅速耗尽你的计算机上所有可利用的资源。

动作

动作或受控制的语句,可以是单个语句,但更常见的是复合语句(包含在花括号内的一个语句块)。它提供每一次通过循环期间将会执行的动作。这个语句可以是一个null语句(空的),在这种情况下,执行的唯一动作将由控制表达式和终止表达式提供。例如,下列代码是有效的(尽管一般不提倡其为优良的代码):

整合

这里是早先那个代码片断的等价物,但使用for循环代替了while循环:

在初始化子句中定义for循环的控制变量是C++中的标准惯用法。像本例中这样用于计数的控制表达式通常使用!=(不等于)操作符来表达。如果你来自一门例如C的语言,以前使用不同的惯用法(如小于操作符),则可能会对此感到奇怪,但是在C++中不等于的比较工作甚至可以在控制变量为用户自定义类型(不支持严格的排序)的情况下工作(最终我们将广泛使用可以展现此性质的类型—迭代器)。这个C++的惯用法在使用整型变量时也能很好地工作,正如在这里一样。

当我们处理独立式的自增操作符时,在C++中通常也使用前自增。对于整型对象,使用前或后自增尚无多大区别,但稍后我们将使用其他类型的迭代器(用于控制重复和标识集合成员的对象),对于那些类型的对象,前自增效率更高。因此,我们采用一种在所有情况下都工作良好的风格。我们把这种自动的选择称作“惯用法(idiom)”。知道了一门语言的惯用法,无论是编写你自己的代码,抑或阅读其他程序员的代码,都会更轻松一些。

语言注解:惯用法的问题之一是,新手常常试图重复以前所用的语言的惯用法。有时这样行得通,有时看上去可以工作,其实隐藏着陷阱,还有时在C++中运作不良(例如使用递归进行循环)或根本就无法工作(例如使用Python形式的for循环,这是个强大的东西,但不是C++当前所能支持的)。多数C惯用法在C++中可以工作,但它们需要在C++环境中加以复查,因为它们并非完全是C++中的首选用法。

3.3.4   break、continue和goto

在一个构造中,存在数种偏离正常代码流程的方式。我将return的细节留到第5章,当讨论函数时再去研究。你可能也注意到了从正常流程抛出一个异常退出。我也将那个细节留给稍后的某一章讨论。

我已经广泛使用了break,但没有深入介绍太多关于它所做事情的细节。break致使从所有上述的循环结构(for、while和do-while)中立即跳出,以及强行从一个swtich语句(和if语句,但通常你应当避免在那儿使用它)中退出。紧跟在所讨论的构造之后的那条语句,将是在break语句之后执行的一句。

良好的经验是,避免使用break来退出循环,尽管我们已经见过一个特例——使用它来退出一个无限循环。我们常常可以修改源代码,使之不再需要break。结果代码常常更为简单,即使乍看上去不是那么明显。如果你发现自己为避免提早退出循环而编写了更复杂的代码,那么你大概走错了方向。往往当程序员努力遵守禁用break的编码方针时,他们依然在使用一种会卷入提早退出的思想模式中。尝试设计你的代码,以使得提早从循环退出不再必不可少。等你做到为了所有正当的理由而减少break的使用这一点,你的代码将会变得更简练、更优雅。

有时我们觉得需要放弃循环的当前这次的通过(pass),并立即转到下一次。C++为此提供了一个特殊的机制:关键字continue。下面是一小段示范了其用法的代码(这只是一个用法示范,并非优良的编码实践):

在本例中,我们本可以毫不费力地消除continue而替换成下面的代码:

然而,我可以辩称第一种形式更为清晰,因为它使得特例情形一目了然,而第二种形式却掩盖了它。在这个简单的例子中,我认为没有太多的东西值得深究,是否使用continue应当取决于基于哪一种方式可以让代码变得更清晰。

我要好好谈谈goto。在超过十年的C++编程中,我从未发现它有用处。我没有多强的哲学理由去反对goto,我只是感到,任何使用goto的代码都可以用一种没有goto但同时又更简单的方式重新编写。它之所以继续存在于C++中,不过是从过去普遍使用长函数的时期沿袭下来的。现如今,我们趋向于编写许多简短的函数,并依赖优秀的编译器减少实际函数调用的次数。一般说来,goto在现代C++编程中没有用。你有必要知道它的存在,只是因为你可能在他人的代码中遇到它,但你自己不需要使用它。

语言注解:对于习惯了BASIC这样的语言的程序员来说,问题之一是他们习惯于使用goto。他们习惯的某些惯用法要使用goto,所以他们就将其用法引入C++中。使用goto最大的问题是经常导致不够清晰的代码。这样的代码难以维护且容易滋生bug。

理论上,你可以使用goto和if(用法参见前面的伪代码)来滚动(roll)循环。实践中,没有C++程序员会那么做。

代码详解

Number-Sorting程序

这里再次给出那个程序的重要代码,以免你来回折腾于源代码(第41页)和我的讲解之间。我已经突出显示了将要解释的代码行。

第9行将numbers定义为一个对象的名字,该对象封装了一个ints的连续序列。std::vector的使用指定了它将是一个连续序列。<int>则指定了它是一个ints的序列。由于我在定义中没有提供额外的信息,因此numbers将作为一个空序列亮相。C++的std::vector序列具有这样的增长策略:允许视需求以一种提供最优综合性能的方式扩展。它们是C++程序员用来封装相同类型对象的首选容器。

第12行在紧靠第一次使用变量next处之前定义了它。严格说来,由于我们是在代码块内定义next,所以代码会在每一次语句块重复执行时再次创建它。然而,任何高品质的编译器都会避免添加额外的代码。我没有初始化next,因为我要从下一行的std::cin中获取一个值。这是可以合理地省略基础类型变量初始化的少数情形之一。尽管如此,许多程序员坚持认为即使在这种情况下也要初始化,因为他们担心以后有人可能会将变量的定义点与它首次被写入的点分离开,从而敞开大门让其他人添加的代码在此变量的值被写入之前就使用它。

在我做了假定说明的上下文中(译注:前面有一行输入提示“Next whole number

(-999 to stop):”),如果你仔细考虑第13行,便会注意到代码假定用户总是输入一个有效的整数,因为甚至连输入一个带小数点的数字这样微不足道的错误,也会把程序送入无限循环。至今我们仍没有准备好如何处理这个问题,我们所做的只是向std::cerr发送一条错误消息,然后通过抛出一个异常来终止程序。

实   验

如果你还没有这么做,请着手改进程序,使其在使用std::cin后立即进行测试,如果失败,就抛出一个异常。

代码详解

第14行测试输入是否结束。如果用户输入了指定的值,程序便越过后面的代码,从第17行开始执行。否则,它就执行第15行。这一行使用了由std::vector提供的一种机制,用于在它所持有序列的末尾添加一个新对象。如果不存在足够的空间存放这个新对象,push_back()将触发可提供更大空间的内部行为,如果有必要,还会将现有值复制到那个空间内,以使序列中的对象保持连续。换句话说,std::vector自动维护它的内容,使其保持为连续的序列。

第17到20行处理了无数字输入的可能性,使用了由std::vector提供的报告序列中当前对象个数的机制。numbers.size()用于求取numbers序列中对象的个数。第19行return 0用以提早离开main(也就是程序)。我们可以把剩余的正常处理代码放入一个由else控制的语句块,以避免对提前退出的需要。

实   验

修改程序以消除return 0,并且使余下的正常处理成为一个受else控制的语句块。在你做到了这一点后,考察两个版本,决定哪一个对你而言更清楚。我倾向于先前的return,但某些编码方针禁止提前使用return。尝试提出一个替代设计,避免使正常处理成为if-else的else部分。

代码详解

第21行使用了现代C++较强大的特性之一:将“算法”应用于序列。#include <algorithm>提供了对库中有点儿命名不当的“算法”部分的访问。std::sort是库提供的大约80种算法之一,其默认版本需要两个实参。第一个实参指定序列从哪儿开始,第二个实参指定如何确定序列已经结束。默认情况下,std::sort()通过使用针对存储在序列中的对象的<(小于)操作符,将序列元素按自然升序进行排列。

numbers.begin()使用std::vector的机制,提供标识了序列起点的值(称为迭代器(iterator))。numbers.end()提供表示序列结束的迭代器(我们会另找时间讨论迭代器的细节,眼下只要知道它们通常标识对象的位置就可以了)。注意,end()标识的并不是最后一个对象:它是个特殊的迭代器,用于决定序列有没有结束(换言之,与大多数迭代器不同,它并不标识某个对象的位置)。乍看上去这种表示序列结束的方式看起来有点奇怪,但它具有许多优点,随着你渐渐习惯它,你可能会感到奇怪:你竟然曾经期望序列的结尾是最后一个元素,而不是最后一个元素之后的一位。

现在如果再看一眼第21行,你就会发现它导致依照升序重新排列了名为numbers的序列。

第24到26行将存储在numbers中的序列作为以逗号分隔的列表输出。for循环的控制表达式保持运转,直到它数完numbers的所有元素为止。注意,连同许多其他语言一起,C++从零开始计数,所以当计数达到容器中元素的个数(由numbers.size()给出)时,就完成任务了。

第25行使用了std::vector允许通过使用下标或索引访问元素的特性。这意味着我们可以像使用数组一样使用std::vector对象,但它比数组多了许多功能。究竟对此感到多么惊讶,取决于你之前的编程经验。

查看所有评论(0)条】

最近评论



正在载入评论列表...
热点评论