第6章 控制程序流程
大多数应用程序都需要在不同的情形(或用户输入)下以不同的方式执行。为让应用程序能够做出不同的反应,需要编写条件语句,在不同的情形下执行不同的代码片段。
在本章中,您将学习:
• 如何根据特定的条件改变程序的行为;
• 如何使用循环重复执行一系列代码;
• 如何在循环中更好地控制流程。
6.1 使用if…else有条件地执行
本书前面介绍的程序都按顺序从上到下执行,且执行每行代码,不跳过任何代码行。但在大多数应用程序中,很少按从上到下的顺序依次执行每行代码。
假设您要编写一个程序,在用户按m键时将两个数相乘,而在用户按其他任何键时将这两个数相加。
如图6.1所示,并非程序每次执行时,都会执行所有的代码。如果用户按m键,将执行将两个数相乘的代码,否则将执行相加的代码。无论在什么情形下,都不可能同时执行这两部分代码。
图6.1 一个根据用户输入进行条件处理的示例
在C++中,使用if…else有条件地执行代码,这种结构类似于下面这样:
因此,在用户输入m时将两个数相乘,否则将两个数相加的if…else结构类似于下面这样:
在C++中,表达式的结果为true意味着不为false,而false为零。因此,在条件语句中,只要表达式的结果不为零(负数或正数),就被视为结果为true。
程序清单6.1演示了if…else结构。它让用户决定要将两个数相乘还是相加,因此使用条件处理来生成所需的结果。
程序清单6.1 根据用户输入决定将两个正数相乘还是相加
输出:
再次运行的输出:
分析:
注意到第15行包含if,而第17行包含else。这些代码告诉编译器,如果if后面的表达式(Userselection == ‘m’)为 true,则执行乘法运算;如果该表达式为 false,则执行加法运算。如果用户输入的字符为m (区分大小写)时,表达式(Userselection == ‘m’)将为 true,否则将为 false。因此,这个简单的程序模拟了图6.1所示的流程图,演示了如何让应用程序在不同的情形下采取不同的行动。
if…else结构的else部分是可选的。如果在表达式为false时不执行任何操作,可以不使用这部分。
在程序清单6.1中,如果第15行为下面这样:
这个if结构将毫无意义,因为它在同一行以空语句(分号)结束。务必小心避免这样做,因为if没有配套的else并不会导致编译错误。
有些优秀的编译器在“控制语句为空”时会发出警告。
如果要在满足(或不满足)条件时执行多条语句,需要将它们组合成一个语句块。包含在大括号({})内的多条语句被视为语句块,例如:
这样的语句块也被称为复合语句。
第 4 章解释了使用静态数组时跨越其边界带来的危险。这种问题在字符数组中最明显。将字符串写入或复制到字符数组中时,务必检查数组是否足够大,是否能够存储该字符串。程序清单 6.2 演示了如何执行这种重要的检查,以免缓冲区溢出。
程序清单6.2 将字符串复制到char数组中之前,检查数组的容量
输出:
分析:
注意到将字符串复制到缓冲区中前,第12行检查字符串是否比缓冲区短。另外,这条if语句的特殊之处在于,如果条件为true,将执行第13~16行的语句块(也叫复合语句)。
注意到 if(condition)的行尾没有分号。这是有意为之,旨在确保条件为 true 时将执行 if后面的语句。
下面的语句能够通过编译,但得不到所需的结果,因为if子句后面的分号导致它到此结束,这意味着没有条件处理,后面的语句总是会执行。
经常需要检查一系列不同的条件,且很多条件依赖于前一个条件是否满足。为满足这种需求, C++允许您对if语句进行嵌套。
嵌套if语句类似于下面这样:
假设有一个类似于程序清单6.1所示的应用程序,用户可通过按d或m键,让应用程序执行除法或乘法运算。执行除法运算前,必须核实除数不为零。因此,除检查用户输入外,在用户要求程序执行除法运算时,还必须核实除数不为零。为此,可使用嵌套if语句,如程序清单6.3所示。
程序清单6.3 使用嵌套if语句执行乘法或除法运算
输出:
再次运行的输出:
最后一次运行的输出:
分析:
这是运行程序三次得到的输出,每次提供的输入都不同。正如您看到的,程序每次的执行路径都不同。相比于程序清单6.1,这个程序有很多地方不同。
• 为更好地处理小数,将输入存储到了float变量中,执行除法运算时这很重要。
• if条件与程序清单 6.1中不同,不再检查用户按的是否是 m键,而在第 14行使用了表达式(UserSelection == ‘d’),该表达式在用户输入 d时为 true。如果用户输入了 d,则执行除法运算。
• 鉴于这个程序将两个数相除,且除数由用户输入,因此必须核实除数不为零。这是在第 17 行使用嵌套的if语句实现的。
需要根据多个条件执行不同任务时,嵌套if语句很有用,这个程序演示了这一点。
这里使用制表符对嵌套语句进行了缩进,这是可选的,但可极大地改善嵌套if语句的可读性。
也可组合使用多个 if…else 结构。程序清单 6.4 所示的程序让用户输入星期几,并使用一组if…else结构告诉用户它是以哪个星星命名的。
程序清单6.4 指出一个星期的各天是以哪个星星命名的
输出:
再次运行的输出:
分析:
第22~37行的if-else-if结构检查用户输入并生成相应的输出。第二次运行的输出表明,如果用户输入的不是 0~6,即不对应于一个星期的任何一天,程序将指出这一点。这种结构的优点是,非常适合用于检查互斥的条件,即星期一不可能是星期二,而无效输入不与一个星期的任何一天对应。另一个有趣的地方是,在if语句中使用了第5行声明的枚举常量DaysOfWeek。原本可以将用户输入与整数(如 0 表示星期天等)进行比较,但通过使用枚举常量 Sunday,代码的可读性更强。
switch-case 让您能够将特定表达式与一系列常量进行比较,并根据表达式的值时执行不同的操作。在这种结构中,经常会使用C++新增的关键字switch、case、default和break。
switch-case结构的语法如下:
上述代码计算expression的值,并将其与每个case标签进行比较。每个case标签都必须是常量,如果expression的值与case标签相等,就执行标签后面的代码。如果expression与LabelA不相等,将把expression与LabelB进行比较。如果它与LabelB相同,就执行DoSomethingElse。将不断重复这个过程,直到遇到break。这是您首次见到break,它导致程序退出当前代码块。break并非必不可少,但如果省略它,将不断与后面的标签进行比较,这并非您希望的。default 也是可选的,它用于执行expression不与switch-case中的任何标签匹配时应执行的操作。
switch-case结构非常适合与枚举常量结合使用。关键字enum在第3章介绍过。
程序清单6.5使用了switch-case结构,它与程序清单6.4等效,指出一个星期的各天是以哪个星星命名的,也使用了枚举常量。
程序清单6.5 指出一个星期的各天是以哪个星星命名的
输出:
再次运行的输出:
分析:
第22~55行的switch-case结构根据用户输入的整数(存储在变量Day中)生成不同的输出。用户输入数字5时,应用程序将switch表达式(Day,其值为5)与标签进行比较,并跳过前4个标签后面的代码,因为这些标签为Sunday(0)~Thursday(4),它们都与5不相等。到达标签Friday后,由于switch表达式的值(5)与枚举常量Friday相等,因此执行该标签后面的代码,并在到达break后退出swtich结构。第二次运行时提供的值无效,因此到达default后执行它后面的代码,显示一条让用户再执行一次的消息。
这个程序使用的是 switch-case,其输出与使用 if-else-if 结构的程序清单 6.4 相同。然而, switch-case 版本的结构化程度更高,可能非常适合不仅仅将一行文本显示在屏幕上的情形(在这种情形下,可将代码放在大括号内,组成语句块)。
C++提供了一个有趣且功能强大的运算符——条件运算符,它相当于紧凑的if-else结构。
条件运算符也叫三目运算符,因为它使用三个操作数:
可使用这个运算符获得两个数字中较大的那个:
程序清单6.6演示了如何使用运算符?:进行条件处理。
程序清单6.6 指出一个星期的各天是以哪个星星命名的
输出:
分析:
需要注意的是第10行。它包含一条非常紧凑的语句,该语句判断输入的两个数字那个更大,与下述使用if-else的代码等效:
使用条件运算符可节省几行代码!但不应将节省代码放在首位。有些程序员很喜欢条件运算符,而有些不喜欢。使用条件运算符时,确保代码易于理解至关重要。
6.2 在循环中执行代码
至此,您知道如何让程序在变量包含不同的值时执行不同的操作。例如,当用户按m键时,程序清单 6.2 执行乘法运算,否则执行加法运算。然而,如果用户不希望程序就此结束,而要再执行一次(甚至5次)乘法或加法运算,该如何办呢?在这种情况下,您需要重复执行现有的代码。
为此,您需要使用循环。
顾名思义,goto将指令指针移到代码的特定位置,您可使用它回过头去再次执行特定的语句。
goto语句的语法如下:
这里声明了一个名为JumpToPoint的标签,并使用goto跳转到这个地方,如程序清单6.7所示。除非给goto语句指定在特定情况下将为false的执行条件,或者重复执行的代码中包含在特定条件下将被执行的return语句,否则goto命令和标签之间的代码将无休止地执行下去,导致程序永不结束。
程序清单6.7 使用goto语句询问用户是否想重复计算
输出:
分析:
程序清单6.7和程序清单6.1的主要差别在于,要让用户再次输入一组数字,并查看加法或乘法运算的结果,需要再次运行程序清单6.1,而程序清单6.7不需要这样,它询问用户是否想再执行一次运算。实际实现这种重复的代码位于第20行,它在用户输入表示yes的y时执行goto语句。执行第20行的goto语句将导致程序跳转到第5行声明的标签JumpToPoint处,这相当于重新启动程序。
不推荐使用goto语句来编写循环,因为大量使用goto语句将导致代码的执行流程无法预测,即不按特定的顺序从一行跳转到另一行;在有些情况下,也可能导致变量的状态无法预测。糟糕地使用 goto 语句将导致意大利面条式代码。要避免使用 goto 语句,可使用接下来将介绍的while、do…while和for循环。
这里介绍goto语句只是为了帮助您理解使用这种语句的代码。
C++关键字while可帮助您完成程序清单6.7中goto语句完成的工作,但更优雅。while循环的语法如下:
只要expression为true,就将执行该语句块。因此,必须确保expression在特定条件下将为false,否则while循环将永不停止。
程序清单6.8与程序清单6.7等效,但使用while而不是goto让用户能够重复计算。
程序清单6.8 使用while循环让用户能够重复计算
输出:
分析:
第7~19行的while循环包含了该程序的大部分逻辑。while循环检查表达式(UserSelection != ‘x’),仅当该表达式为 true 时才继续执行后面的代码。为确保第一次循环能够进行,第 5 行将 char 变量UserSelection初始化为‘m’。需要确保该变量不为‘x’,否则将导致第一次循环不会进行,应用程序退出,而不做任何有意义的工作。第一次循环非常简单,但第17行询问用户是否想再次执行计算。第18行读取用户输入,这将影响while计算的表达式的结果,确定程序继续执行还是就此终止。第一次循环结束后,将跳转到第7行,计算while语句中表达式的值,如果用户按的不是x键,将再次执行循环。如果用户在循环末尾按了x键,下次计算第7行的表达式时,结果将为false,这将退出while循环,并在显示再见消息后结束应用程序。
循环也叫迭代,while、do…while和for语句也被称为迭代语句。
在有些情况(如程序清单 6.8 所示的情况)下,您需要将代码放在循环中,并确保它们至少执行一次。此时do…while循环可派上用场。
do…while循环的语法如下:
注意到包含while(expression)的代码行以分号结尾,这不同于前面介绍的while循环。在while循环中,如果包含while(expression)的代码行以分号结尾,循环将就此结束,变成一条空语句。
程序清单6.9演示了如何使用do…while循环来确保语句至少执行一次。
程序清单6.9 使用do…while循环重复执行代码块
输出:
分析:
这个程序的行为和输出与前一个程序很像。实际上,唯一的差别在于,第 6 行包含关键字do,而第18行使用了while。将按顺序执行每行代码,直到达到第18行的while。到达第18行后,while计算表达式(UserSelection != ‘x’)的值。如果该表达式为 true,即用户没有按 x退出,将重复执行循环。如果该表达式为 false,即用户按了 x 键,将退出循环,显示再见消息,并结束应用程序。
for语句是一种更复杂的循环,因为它允许您指定执行一次的初始化语句(通常用于初始化计数器)、检查退出条件(通常使用计数器)并在每次循环末尾执行操作(通常是将计数器递增或修改其值)。
for循环的语法如下:
for循环让程序员能够定义并初始化一个计数器变量,在每次循环开头检查退出条件,在循环末尾修改计数器变量的值。
程序清单6.10演示了一种使用for循环访问数组元素的高效方式。
程序清单6.10 使用for循环填充和显示静态数组的元素
输出:
分析:
程序清单6.10包含两个for循环,分别位于第10和 18行。第一个for循环帮助填充一个int数组的元素,而另一个帮助显示该数组的元素。这两个 for 循环的语法相同,都声明了索引变量ArrayIndex,用于访问数组中的元素。在每次循环末尾,这个变量都递增,以便在下一次循环时访问数组中的下一个元素。在 for 语句中,中间的表达式为退出条件,它将 ArrayIndex 与ARRAY_LENGTH 进行比较,检查在每次循环末尾递增后,ArrayIndex 是否还在数组边界内。这也确保了for循环不会跨越数组边界。
用于帮助访问集合(如数组)元素的变量(如程序清单6.10中的ArrayIndex)也被称为迭代器。
在for语句中声明的迭代器的作用域为for循环,因此在程序清单6.10中,第二个for循环中声明的ArrayIndex实际上是个新变量。
然而,初始化语句、条件表达式以及修改变量的语句都是可选的,for语句可以不包含这些部分,如程序清单6.11所示。
程序清单6.11 使用for循环(省略了修改变量的语句)根据用户的请求重复执行计算
输出:
分析:
这与使用 while 循环的程序清单 6.8 相同,唯一的差别在于第 8 行使用了 for 循环。这个 for循环有趣的地方在于,它只包含初始化表达式和条件表达式,而省略了在每次循环末尾修改变量的语句。
在for循环的初始化表达式中,可初始化多个变量。对于程序清单6.11所示的for循环,如果在其中初始化多个变量,将类似于下面这样:
注意到新增的变量被初始化为5。
有趣的是,还可使用循环表达式在每次循环时都将其递减。
6.3 使用continue和break修改循环的行为
在有些情况下(尤其是使用大量条件处理大量参数的复杂循环中),无法编写有效的循环条件,而需要在循环中修改程序的行为。在这种情况下,contiune和break可提供帮助。
continue让您能够跳转到循环开头,跳过循环块中后面的代码。因此,在while、do…while和for循环中,continue导致重新评估循环条件,如果为true,则重新进入循环块。
在for循环中遇到continue时,将在评估条件前执行循环表达式(for语句中的第三个表达式,通常用于递增计数器)。
break退出循环块,即结束当前循环。
程序员通常的预期是,如果循环条件满足,将执行循环中的所有代码。contiune和break改变了这种行为,可能导致代码不直观。
因此,应慎用continue和break,仅当不使用它们就无法正确而高效地编写循环时才用。
while、do…while和for 循环都包含一个条件表达式,循环在它为false时结束。如果您指定的条件总是为true,循环就不会结束。
无限while循环类似于下面这样:
无限do…while循环类似于下面这样:
而无限for循环类似于下面这样:
这种循环看似奇怪,但确实有用武之地。假设操作系统需要不断检查USB端口是否连接了设备,只要操作系统在运行,这种活动就不应停止。在这种情况下,就应使用永不结束的循环。这种循环也叫无限循环,因为它们将不断执行下去,直到永远。
如果要结束无限循环(假设前述示例中的操作系统需要关闭),可插入一条break语句(通常放在if(condition)代码块中)。
下面是一个使用break退出无限while循环的例子:
下面是一个使用break退出无限do…while循环的例子:
下面是一个使用break退出无限for循环的例子:
程序清单6.12演示了如何使用continue和break来指定无限循环的退出条件。
程序清单6.12 使用continue进入下一次循环,并使用break退出无限for循环
输出:
分析:
相比于程序清单6.11的for循环,第5行的for循环的不同之处在于,它是一个无限for循环,没有包含每次循环迭代前评估的条件表达式。换句话说,如果没有 break 语句,该循环(进而是该应用程序)将永远不会结束。相比于您在本书前面看到的其他输出,这里的输出的不同之处在于,在执行加法和乘法运算前,用户可修改其输入的整数。这种逻辑是使用continue实现的;如第16和17行所示,程序根据指定的条件决定是否执行continue。被询问是否要修改数字时,如果用户按y键,第16行的条件将为true,进行执行后面的continue。遇到continue后,将跳转到循环开头处执行,让用户输入两个整数。同样,在循环末尾询问用户是否想退出时,第26行检查用户输入是否是‘x’,如果是,则执行break语句,结束循环。
在程序清单6.12中,使用了空语句for(;;)来创建无限循环。您也可以使用其他类型的循环来生成相同的输出,为此可将该语句替换为while(true)或do…while(true);。
6.4 编写嵌套循环
就像本章开头介绍的嵌套 if 语句一样,经常需要在一个循环内嵌套另一个循环。假设有两个 int数组,而您想将Array1的每个元素与Array2的每个元素相乘,则通过使用嵌套循环,这种编程工作将很容易完成。第一个循环遍历Array1,第二个循环遍历Array2,且位于第一个循环内部。
程序清单6.13表明,您可在任何类型的循环内部嵌套任何类型的循环。
程序清单6.13 使用嵌套循环将一个数组的每个元素与另一个数组的每个元素相乘
输出:
分析:
第13和14行是两个嵌套的for循环。第一个for循环遍历数组MyInts1,而第二个for循环遍历数组MyInts2。第一个for循环每次迭代时,都执行第二个for循环。第二个for循环遍历数组MyInts2,在每次迭代中,都将数组MyInts2中的当前元素与数组MyInts1中索引为Array1Index的元素相乘。因此,对于MyInts1中的每个元素,第二个循环都遍历MyInts2的所有元素。其结果是,首先将MyInts1的第一个元素(偏移量为0)与数组MyInts2的每个元素相乘;然后,将MyInts1的第二个元素与数组MyInts2的每个元素相乘;最后,将MyInts1的第三个元素与数组MyInts2的每个元素相乘。
出于方便以及将重点放在循环上的考虑,在程序清单6.13中,对数组进行了初始化。但也可以前面的示例(如程序清单 6.10)那样,让用户输入整数,并使用它们来填充 int数组。
第 4 章介绍了多维数组。实际上,在程序清单 4.3 中,您遍历了一个三行、三列的二维数组。当时的做法是,分别访问数组中的每个元素,每个元素占一行代码。这种方式的可扩展性不强,如果数组变大了,除修改各维的长度外,还需添加大量的代码。然而,使用循环可改变这一点,如程序清单6.14所示。
程序清单6.14 使用嵌套循环遍历二维int数组的元素
输出:
分析:
第14~22行包含遍历二维int数组所需的两个for循环。二维数组实际上是数组的数组。注意到第一个for循环访问行,每行都是一个int数组;而第二个for循环访问数组中的每个元素,即列。
程序清单6.14使用大括号将嵌套的for循环括起来了,这只是为了改善可读性。即便没有大括号,嵌套的循环也不会有问题,因为循环语句只是一条语句,而不是复合语句,需要必须用大括号括起。
著名的斐波纳契数列以 0和 1打头,随后的每个数字都是前两个数字之和。因此斐波纳契数列的开头类似于下面这样:
程序清单 6.15 演示了如何创建斐波纳契数列:用户想要多长就多长——长度只受限于 int 变量可存储的最大值。
程序清单6.15 使用嵌套循环计算斐波纳契数列
输出:
分析:
第 13 行的外部 do…while 循环基本上是一个询问循环,询问用户是否要生成更多的数字。第 15行的内部for循环计算并显示接下来的5个斐波纳契数。第19行将Num2赋给一个临时变量,以便第21行将这个值赋给Num1。如果不将Num2的原始值存储到临时变量中,第20行修改Num2的值后,就无法将其赋给 Num1 了。如果用户按 y 键,将使用 Num1 和 Num2 存储的新值再次执行内部 for循环,这都要归功于这3行代码。
6.5 总结
本章介绍的全部内容都旨在避免代码只能从上到下执行:如何编写可提供不同执行路径的条件语句以及如何使用循环重复执行代码。您学习了if…else 结构以及如何使用 switch-case 语句来处理不同的情形(即变量包含不同的值)。
为帮助您理解循环,介绍了goto语句,但同时警告您不要使用它,因为使用它创建的代码难以理解。您学习了如何使用while、do…while和for结构编写循环,学习了如何让循环无休止地迭代下去(即无限循环)以及如何使用continue和break更好地控制它们。
6.6 问与答
问:如果在switch-case语句中省略了break,结果将如何?
答:break让程序能够退出switch结构,如果没有它,将继续评估后面的case语句。
问:如何退出无限循环?
答:使用break退出当前循环;使用return退出当前函数模块。
问:我编写了一个类似于while(Integer)的循环,如果Integer的值为-1,这个while循环会执行吗?
答:理想情况下,while循环表达式应为布尔值true或false,否则这样解读:零表示false,非零表示true。由于-1不是零,因此该while条件为true,循环将执行。如果希望仅当Integer为正数时才执行循环,可编写表达式while(Integer>0)。这种规则适用于所有的条件语句和循环。
问:有与for(;;)等效的空while语句吗?
答:没有,while语句必须有配套的条件表达式。
问:我通过复制并粘贴将do…while(exp);改成了while(exp);,这会导致问题吗?
答:会出大问题!while(exp);合法,却是一个空 while循环,因为 while后面是一条空语句(分号),即便后面有语句块亦如此。后面的语句块将执行一次,但它位于循环外面。复制并粘贴代码时务必小心。
6.7 作业
作业包括测验和练习,前者帮助读者加深对所学知识的理解,后者提供了使用新学知识的机会。请尽量先完成测验和练习题,然后再对照附录D的答案。在继续学习下一章前,请务必弄懂这些答案。
1.既然不缩进也能通过编译,为何要缩进语句块、嵌套if语句和嵌套循环?
2.使用goto可快速解决问题,为何要避免使用它?
3.可编写计数器递减的for循环吗?这样的for循环是什么样的?
4.下面的循环有何问题?
1.编写一个for循环,以倒序方式访问数组的元素。
2.编写一个类似于程序清单6.13的嵌套for循环,但以倒序方式将一个数组的每个元素都与另一个数组的每个元素相加。
3.编写一个程序,像程序清单6.15那样显示斐波纳契数列,但让用户指定每次显示多少个。
4.编写一个switch-case结构,指出用户选择的颜色是否出现在彩虹中。请使用枚举常量。
5.查错:下面的代码有何错误?
6.查错:下面的代码有何错误?
7.查错:下面的代码有何错误?