5.3条件语句
C++语言提供了两种按条件执行的语句。一种是if语句,它根据条件决定控制流:另外一种是switch语句,它计算一个整型表达式的值,然后根据这个值从几条执行路径中选择一条。
5.3.1 if语句
if语句(if statement)的作用是:判断一个指定的条件是否为真,根据判断结果决定是否执行另外一条语句。if语句包括两种形式:一种含有else分支,另外一种没有。简单if语句的语法形式是
if (condition)
statement
if else 语句的形式是
if (condition)
statement
else
statement2
在这两个版本的if语句中,condiion都必须用圆括号包围起来。condition可以是一个表达式,也可以是一个初始化了的变量声明(参见5.2节,第155页)。不管是表达式还是变量,其类型都必须能转换成(参见411节,第141页)布尔类型。通常情况下,stalement和statement2是块语句。
如果 condition为真,执行statement。当statement 执行完成后,程序继续执行 if 语句后面的其他语句。
如果 condition 为假,跳过statement。对于简单if语句来说,程序继续执行if语句后面的其他语句;对于if else语来说,执行statement2。
使用if else 语句
我们举个例子来说明if语句的功能,程序的目的是把数字形式表示的成绩转换成字母形式。假设数字成绩的范围是从0到100(包括100在内),其中100分对应的字母形式是“A++”,低于60分的成绩对应的字母形式是“F”。其他成绩每10个划分成一组:60到69(包括69 在内)对应字母“D”、70 到 79 对应字母“℃”,以此类推。使用 vector对象存放字母成绩所有可能的取值:
const vector<string>scores={"F","D”,"C","B","A","A++"};
我们使用if else语句解决该问题,根据成绩是否合格执行不同的操作:
//如果grade 小于60,对应的字母是F;否则计算其下标
string lettergrade;
if(grade <60)
lettergrade =scores[0];
else
lettergrade=scores[(grade-50)/10];
判断 grade 的值是否小于60,根据结果选择执行if分支还是else分支。在else分支中,由成绩计算得到一个下标,具体过程是:首先从grade中减去50,然后执行整数除法(参见4.2节,在125页),去掉余数后所得的商就是数组scores对应的下标。
嵌套if语句
接下来让我们的程序更有趣点儿,试着给那些合格的成绩后面添加一个加号或减号。如果成绩的末位是8或者9,添加一个加号;如果末位是0、1或2,添加一个减号:
if(grade%10>7)
lettergrade+='+';//末尾是8或者9的成绩添加一个加号
else if(grade%10<3)
lettergrade+='-';//末尾是0、1或者2的成绩添加一个减号
我们使用取模运算符(参见4.2节,第125页)计算余数,根据余数决定添加哪种符号
接着把这段添加符号的代码整合到转换成绩形式的代码中去:
//如果成绩不合格,不需要考虑添加加号减号的问题
if(grade <60)
lettergrade =scores[0];
else
{
lettergrade=scores[(grade-50)/10];//获得字母形式的成绩
if(grade!=100)//只要不是++,就考虑添加加号或减号
if(grade%10>7)
lettergrade+='+';//末尾是8或者9的成绩添加一个加号
else if(grade10<3)
lettergrade+='';//末尾是0、1或者2的成绩添加一个减号
}
注意,我们使用花括号把第一个else后面的两条语句组合成了一个块。如果grade不小于60要做两件事:从数组scores中获取对应的字母成绩,然后根据条件设置加号或减号。
注意使用花括号
有一种常见的错误:本来程序中有几条语句应该作为一个块来执行,但是我们忘了用花括号把这些语句包围。在下面的例子中,添加加号减号的代码将被无条件地执行,这显然违背了我们的初衷:
if(grade <60)
lettergrade =scores[0];
else //错误:缺少花括号
lettergrade=scores[(grade-50)/10];
//虽然下面的语句从形式上看有缩进,但是因为没有花括号:
//所以无论什么情况都会执行接下来的代码
//不及格的成绩也会添加上加号或减号,这显然是错误的
if(grade !=100)
if(grade10>7)
lettergrade+='+';//末尾是8或者9的成绩添加一个加号
else if(grade号10<3)
lettergrade+='-’;//末尾是0、1或者2的成绩添加一个减号
要想发现这个错误可能非常困难,毕竟这段代码“看起来”是正确的。
为了避免此类问题,有些编码风格要求在if或else之后必须写上花括号(对 while和for语句的循环体两端也有同样的要求)。这么做的好处是可以避免代码混乱不清,以后修改代码时如果想添加别的语句,也可以很容易地找到正确位置。
Best Prstiees:
许多编辑器和开发环境都提供一种辅助工具,它可以自动地缩进代码以匹配其语法结构。善用此类工具益处多多。
悬垂 else
当一个if语句嵌套在另一个if语句内部时,很可能 if分支会多于else 分支。事实上,之前那个成绩转换的程序就有4个if分支,而只有2个else分支。这时候问题出现了:我们怎么知道某个给定的else是和哪个if匹配呢?
这个问题通常称作悬垂else(dangling else),在那些既有 if语句又有if else语句的编程语言中是个普遍存在的问题。不同语言解决该问题的思路也不同,就C++而言,它规定else与离它最近的尚未匹配的if匹配,从而消除了程序的二义性。
当代码中if分支多于else分支时,程序员有时会感觉比较麻烦。举个例子来说明对于添加加号减号的那个最内层的if else语句,我们用另外一组条件改写它:
//错误:实际的执行过程并非像缩进格式显示的那样;else分支匹配的是内层if语句
if(grade%10>=3)
if(grade%10>7)
lettergrade+='+';//末尾是8或者9的成绩添加一个加号
else
lettergrade +='-';//末尾是3、4、5、6或者7的成绩添加一个减号!
从代码的缩进格式来看,程序的初衷应该是希望else和外层的if匹配,也就是说,我们希望当 grade 的末位小于3时执行 else 分支。然而,不管我们是什么意图,也不管程序如何缩进,这里的else分支其实是内层if语句的一部分。最终,上面的代码将在末位大于3小于等于7的成绩后面添加减号!它的执行过程实际上等价于如下形式:
//缩进格式与执行过程相符,但不是程序员的意图
if(grade%10>=3)
if(grade%10>7)
lettergrade+='+';//末尾是8或者9的成绩添加一个加号
else
lettergrade+='';//末尾是3、4、5、6或者7的成绩添加一个减号!
使用花括号控制执行路径
要想使else分支和外层的if语句匹配起来,可以在内层if语句的两端加上花括使其成为一个块:
//末尾是8或者9的成绩添加一个加号,末尾是0、1或者2的成绩添加一个减号
if(grade%10>=3)
{
if(grade%10>7)
lettergrade+='+';//末尾是8或者9的成绩添加一个加号
}
else //花括号强迫else与外层if 匹配
lettergrade +='-';//末尾是0、1或者2的成绩添加一个减号
语句属于块,意味着语句一定在块的边界之内,因此内层if语句在关键字else前面的那个花括号处已经结束了。else不会再作为内层if的一部分。此时,最近的尚未匹配的if是外层if,也就是我们希望else 匹配的那个。
5.3.2 switch 语句
switch语句(switch statement)提供了一条便利的途径使得我们能够在若干固定选项中做出选择。举个例子,假如我们想统计五个元音字母在文本中出现的次数,程序逻辑应该如下所示:
1)从输入的内容中读取所有字符。
2)令每一个字符都与元音字母的集合比较。
3)如果字符与某个元音字母匹配,将该字母的数量加1。
4)显示结果。
例如,以(原书中)本章的文本作为输入内容,程序的输出结果将是:
Number of voweI a:3195
Number of vowel e:6230
Number of vowel i:3102
Number of vowel o:3289
Number of vowel u:1033
要想实现这项功能,直接使用switch语句即可:
//为每个元音字母初始化其计数值
unsigned aCnt=0,eCnt=0,iCnt=0,oCnt=0,uCnt=0;
char h;
while(cin>>ch)
{
//如果ch是元音字母,将其对应的计数值加1
switch(ch)
case 'a':
++aCnt;
break;
case 'e':
++eCnt;
break;
case 'i':
++iCnt;
break;
case 'o':
++oCnt;
break;
case 'u':
++uCnt;
break;
}
//输出结果
cout << "Number of vowel a:\t"<< aCnt << '\n'
<< "Number ofvowel e:\t" << eCnt << '\n'
<< "Number ofvowel i:\t" << iCnt <<'\n'
<< "Number ofvowel 0:\t" << oCnt<<'\n'
<<"Number of Vowel u:\t"<<uCnt<< endl;
switch语句首先对括号里的表达式求值,该表达式紧跟在关键字switch的后面,可以是一个初始化的变量声明(参见5.2节,第155页)。表达式的值转换成整数类型,然后与每个 case 标签的值比较。
如果表达式和某个 case标签的值匹配成功,程序从该标签之后的第一条语句开始执行,直到到达了switch的结尾或者是遇到一条break语句为止。
我们将在5.5.1节(第170页)详细介绍break语句,简言之,break语句的作用是中断当前的控制流。此例中,break语句将控制权转移到switch语句外面。因为switch是 while循环体内唯一的语句,所以从switch语句中断出来以后,程序的控制权将移到 while语句的右花括号处。此时 while语句内部没有其他语句要执行,所以 while会返回去再一次判断条件是否满足。
如果switch语句的表达式和所有case都没有匹配上,将直接跳转到switch 结构之后的第一条语句。刚刚说过,在上面的例子中,退出switch 后控制权回到 while 语句的条件部分。
case关键字和它对应的值一起被称为case标签(case label)。case 标签必须是整型常量表达式(参见2.4.4节,第58页):
char ch=getVal();
int ival = 42;
switch(ch)
case 3.14://错误:case标签不是一个整数
case ival://错误:case标签不是一个常量
任何两个case标签的值不能相同,否则就会引发错误。另外,default也是一种特殊的 case 标签,关于它的知识将在第162 页介绍。
switch 内部的控制流
理解程序在 case 标签之间的执行流程非常重要。如果某个case标签匹配成功,将从该标签开始往后顺序执行所有case分支,除非程序显式地中断了这一过程,否则直到switch的结尾处才会停下来。要想避免执行后续case分支的代码,我们必须显式地告诉编译器终止执行过程。大多数情况下,在下一个case 标签之前应该有一条 break 语句。
然而,也有一些时候默认的switch行为才是程序真正需要的。每个case标签只能对应一个值,但是有时候我们希望两个或更多个值共享同一组操作。此时,我们就故意省略掉break语句,使得程序能够连续执行若干个case标签
例如,也许我们想统计的是所有元音字母出现的总次数:
unsigned vowelCnt=0;
//
switch(ch)
{
//出现了a、e、i、o或u中的任意一个都会将vowelCnt的值加1
Case 'a':
Case 'e':
case 'i':
case 'o':
case 'u':
++vowelCnt;
break;
}
在上面的代码中,几个case标签连写在一起,中间没有break语句。因此只要 ch 是元音字母,不管到底是五个中的哪一个都执行相同的代码。
C++程序的形式比较自由,所以case标签之后不一定非得换行。把几个case 标签写在一行里,强调这些case代表的是某个范围内的值:
switch (ch)
{
//另一种合法的书写形式
case 'a':casee'e':case'i':case 'o':case 'u':
++vowelCnt;
break;
}
一般不要省略case分支最后的break语句。如果没写break语句,最好加一段注释说清楚程序的逻辑。
漏写 break 容易引发缺陷
有一种常见的错觉是程序只执行匹配成功的那个case分支的语句。例如,下面程序的统计结果是错误的:
//警告:不正确的程序逻辑!
switch(ch)
{
Case 'a':
++aCnt;//此处应该有一条break语句
Case 'e':
++eCnt;//此处应该有一条break语句
case 'i':
++iCnt;//此处应该有一条break语句
Case 'o':
++oCnt;//此处应该有一条break语句
Case 'u':
++uCnt;
}
要想理解这段程序的执行过程,不妨假设ch的值是’e’。此时,程序直接执行 case’e’标签后面的代码,该代码把eCnt的值加1。接下来,程序将跨越case标签的边界,接着递增 iCnt、oCnt 和 uCnt。
尽管 switch 语句不是非得在最后一个标签后面写上 break,但是为了安全起见,最好这么做。因为这样的话,即使以后再增加新的case分支,也不用再在前面补充break语句了。
default 标签
如果没有任何一个case标签能匹配上switch表达式的值,程序将执行紧跟在default标签(default label)后面的语句。例如,可以增加一个计数值来统计非元音字母的数量,只要在default分支内不断递增名为otherCnt的变量就可以了:
//如果ch是一个元音字母,将相应的计数值加1
switch(ch)
{
case'a':case 'e':case 'i':case'o':case'u':
++vowelCnt;
break;
default :
++otherCnt;
break ;
}
在这个版本的程序中,如果ch不是元音字母,就从default标签开始执行并把otherCnt加1.
即使不准备在 default标签下做任何工作,定义一个 default 标签也是有用的。其目的在于告诉程序的读者,我们已经考虑到了默认的情况,只是目前什么也没做。
标签不应该孤零零地出现,它后面必须跟上一条语句或者另外一个case标签。如果switch 结构以一个空的 default 标签作为结束,则该 default 标签后面必须跟上一条空语句或一个空块。
switch 内部的变量定义
如前所述,switch的执行流程有可能会跨过某些case标签。如果程序跳转到了某个特定的 case,则switch 结构中该 case 标签之前的部分会被忽略掉。这种忽略掉部分代码的行为引出了一个有趣的问题:如果被略过的代码中含有变量的定义该怎么办?答案是:如果在某处一个带有初值的变量位于作用域之外,在另一处该变量位于作用域之内,则从前一处跳转到后一处的行为是非法行为。
case true://因为程序的执行流程可能绕开下面的初始化语句,
//所以该switch语句不合法
string file_name;//错误:控制流绕过一个隐式初始化的变量
int ival = 0;//错误:控制流绕过一个显式初始化的变量
int jval ;//正确:因为jval没有初始化
break;
case false ://正确:jval虽然在作用域内,但是它没有被初始化
jval =next num();//正确:给jval赋一个值
if(file_name.empty())//file_name 在作用域内,但是没有被初始化
假设上述代码合法,则一旦控制流直接跳到false分支,也就同时略过了变量file_name和 jva1 的初始化过程。此时这两个变量位于作用域之内,跟在false 之后的代码试图在尚未初始化的情况下使用它们,这显然是行不通的。因此C++语言规定,不允许跨过变量的初始化语句直接跳转到该变量作用域内的另一个位置。
如果需要为某个case分支定义并初始化一个变量,我们应该把变量定义在块内,从而确保后面的所有case标签都在变量的作用域之外。
case true:
{
//正确:声明语句位于语句块内部
string file_name =get file name();
}
break;
case false:
if(file name.empty())//错误:file name 不在作用域之内