第五章 语句
通常情况下,语句是顺序执行的,但除非是最简单的程序,否则仅有顺序执行远远不够。因此, C++语言提供了一组 控制流(flow-of-control) 语句以支持更复杂的执行路径。
5.1 简单语句
C++语言中的大多数语句都以分号结束,一个表达式,末尾加上 ;
就变成了 表达式语句(expression statement)。表达式语句的作用是执行表达式并丢弃求值结果。
ival + 5; // 一条没什么实际用处的表达式语句
cout << ival; // 一条有用的表达式语句
空语句
空语句中只含有一个单独的分号 ;
。如果在程序的某个地方,语法上需要一条语句但是逻辑上不需要,则应该使用 空语句(null statement)。
// 重复读入数据直至到达文件末尾或某次输入的值等于sought
while (cin >> s && s != sought)
; // 空语句
使用空语句时应该加上注释,从而令读这段代码的人知道该语句是有意省略的。
别漏写分号,也别多写分号
不要漏写分号 ;
,也不要多写分号(即空语句),多余的空语句并非总是无害的。
ival = vl + v2;; // 正确: 第二个分号表示一条多余的空语句
// 出现了糟糕的情况:额外的分号,循环体是那条空语句
while (iter != svec.end()) ; // while循环体是那条空语句
++iter; // 递增运算不属于循环的一部分
复合语句(块)
复合语句(compound statement) 也被称为块(block),是指用花括号括起来的(可能为空)语句和声明的序列。一个块就是一个作用域,在块中引入的名字只能在块内部以及嵌套在块中的子块里访问。通常,名字在有限的区域内可见,该区域从名字定义处开始,到名字所在(最内层)块的结尾处为止。
如果在程序的某个地方,语法上需要一条语句,但是逻辑上需要多条语句,则应该使用复合语句。把要执行的语句用花括号括起来, 就将其转换成了一条(复合〉语句。
while (val <= 10) {
sum += val; // 把sum+val的值赋给sum
++val; // 给val加1
}
块不以分号作为结束。
所谓空块, 是指内部没有任何语句的一对花括号。空块的作用等价于空语句:
while (cin >> s && s != sought)
{ } // 空块
5.2 语句作用域
可以在 if
、switch
、while
和 for
语句的控制结构内定义变量,这些变量只在相应语句的内部可见,一旦语句结束,变量也就超出了其作用范围:
while (int i = get_num()) // 每次迭代时创建并初始化i
cout << i << endl;
i = 0; // 错误:在循环外部无法访问i
如果其他代码也需要访问控制变量,则变量必须定义在语句的外部:
// 寻找第一个负位元素
auto beg = v.begin();
while (beg != v.end() && *beg >= 0)
++beg;
if (beg == v.end())
// 此时我们知道v中的所有元素都大于等于0
因为控制结构定义的对象的值马上要由结构本身使用,所以这些变量必须初始化。
5.3 条件语句
C++语言提供了两种按条件执行的语句。if
和switch
5.3.1 if语句
if语句(if statement)的作用:判断一个指定条件是否为真,根据判断结果决定是否执行另外一条语句。if
语句包括两种形式:一种含有else
分支,另外一种没有。
if
语句的形式:
if (condition)
statement
12
if-else
语句的形式:
if (condition)
statement
else
statement2
condition 必须用圆括号括起来。
condition 是判断条件,可以是一个表达式,也可以是一个初始化了的变量声明。
- 如果 condition 为真,执行 statement。执行完成后,程序继续执行
if
语句后面的其他语句。 - 如果 condition 为假,跳过 statement。对于简单
if
语句来说,程序直接执行if
语句后面的其他语句;对于if-else
语句来说,程序先执行 statement2,再执行if
语句后面的其他语句。
使用if else 语句
嵌套if语句
注意使用花括号
有一种常见的错误:本来程序中有几条语句应该作为一个块来执行,但是忘了用花括号把这些语句包围。
为了避免这类问题,有些编码风格要求在if
或 else
之后必须写上花括号(对 while
和 for
语句的循环体两端也有同样的要求),这么做的好处是可以避免代码混乱不惰,以后修改代码时如果想添加别的语句,也可以很容易地找到正确位置。
悬垂else
当代码中 if
分支多下 else
分支时,我们怎么知道某个给定的else
是和哪个if
匹配呢?------这个问题通常称为悬垂else(dangling else)。
C++规定 else
与离它最近的尚未匹配的 if
匹配,从而消除了程序的二义性。
// 错误:实际的执行过程并非像缩进格式显示的那样,else分支匹配的是内层if语句
if (grade % 10 >= 3)
if (grade %10 > 7)
lettergrade += '+'; // 末尾是8或者9的成绩添加一个加号
else
lettergrade += '-'; // 末尾是3、4、5、6或者7的成绩添加一个减号!
等价于 <=>
// 缩进格式与执行过程相符,但不是程序员的意图
if (grade % 10 >= 3)
if (grade %10 > 7)
lettergrade +='+'; // 末尾是8或者9的成绩添加一个加号
else
lettergrade += '-'; // 末尾是3、4、5、6或者7的成绩添加一个减号!
使用花括号控制执行路径
要想 else
分支和外层的 if
语句匹配起来,可以在内层 if
语句的两端加上花括号, 使其成为一个块:
// 错误:实际的执行过程并非像缩进格式显示的那样,else分支匹配的是内层if语句
if (grade % 10 >= 3) {
if (grade %10 > 7)
lettergrade +='+'; // 末尾是8或者9的成绩添加一个加号
} else
lettergrade += '-'; // 末尾是1或者2的成绩添加一个减号!
123456
5.3.2 switch语句
switch
语句提供了一条便利的途径使得我们能够在若干固定选项中做出选择。
示例,统计五个元音字母在文本中出现的次数:
// 为每个元音字母初始化其计数值
unsigned aCnt = 0, eCnt = 0, iCnt = 0, oCnt = 0, uCnt = 0;
char ch;
while (cin >> ch) {
// 如果ch是元音字母,将其对应的计数位加1
switch (ch) {
case 'a':
++aCnt;
break;
case 'e':
++eCnt;
break;
case 'i':
++iCnt;
break;
case '0':
++oCnt;
break;
case 'u':
++uCnt;
break;
}
}
// 输出结果
cout << "Number of vowel a : " << aCnt << '
'
<< "Number of vowel e : " << eCnt << '
'
<< "Number of vowel i : " << iCnt << '
'
<< "Number of vowel 0 : " << oCnt << '
'
<< "Number of vowel u : " << uCnt << endl;
switch
语句先对括号里的表达式求值,该表达式紧跟在switch
的后面,可以是一个初始化的变量声明。表达式的值转换成整数类型,然后再与每个 case
标签的值进行比较。
-
如果表达式的值和某个
case
标签匹配,程序从该标签之后的第一条语句开始执行,直到到达switch
的结尾或者遇到break
语句为止。 -
如果
switch
语句的表达式和所有case
都没匹配上,将直接跳转到switch
结构之后的第一条语句。
switch内部的控制流
大多数情况下,在下一个 case
标签之前应该有一条 break
语句;然而,也有一些时候默认的switch
行为才是程序真正需要的。如果确实不应该出现 break
语句,最好写一段注释说明程序的逻辑。
漏写break容易引发缺陷
尽管
switch
语句不是非得在最后一个case
标签后面写上break
,但为了安全起见,最好添加break
。即使以后增加了新的case
分支,也不用再在前面补充break
语句了。
default标签
如果没有任何一个 case
标签能匹配上 switch
表达式的值,程序将执行 default
标签后的语句。
即使不准备在
default
标签下做任何操作,定义一个default
标签也是有用的。其目的在于告诉程序的读者,我们已经考虑到了默认情况,只是目前不需要实际操作。
标签不应该孤零零地出现,它后面必须跟上一条语句或者另外一个case
标签。如果switch
结构以一个空的default
标签作为结束,则该default
标签后面必须跟上一条空语句或者一个空块。
switch内部地变量定义
C++语言规定,不允许跨过变量的初始化语句直接跳转到该变量作用域内的另一个位置。
如果需要为 switch
的某个 case
分支定义并初始化一个变量,我们应该把变量定义在块内,从而确保后面的所有case
标签都在变量的作用域之外。
case true:
{
// 正确:声明语句位于语句块内部
string file_name = get_file_name();
// ...
}
break;
case false:
if (file_name.empty()) // 错误:file_name不在作用域之内
5.4 迭代语句
迭代语句通常称为循环,它重复执行操作直到满足某个条件才停止。while
和 for
语句在执行循环体之前检查条件,do-while
语句先执行循环体再检查条件。
5.4.1 while语句
只要条件为真,while
语句就重复地执行循环体。while
语句的形式:
while (condition)
statement
只要 condition 的求值结果为 true
,就一直执行 statement(通常是一个块)。condition 不能为空,如果 condition 第一次求值就是 false
,statement 一次都不会执行。
while
地条件部分可以是一个表达式或者是一个带初始化地变量声明。
定义在
while
条件部分或者while
循环体内的变量,每次迭代都经历从创建到销毁的过程。在不确定迭代次数,或者想在循环结束后访问循环控制变量时,使用while
比较合适。
使用while循环
当不确定到底要迭代多少次时,使用while
循环比较合适。
vector<int> v:
int i;
// 重复读入数据,直至到达文件末尾或者遇到其他输入问题
while (cin >> i)
v.push_back(i);
// 寻找第一个负值元素
auto beg = v.begin();
while (beg != v.end() && *beg >= 0)
++beg;
if (beg == v.end())
// 此时我们知道v中的所有元素都大于等于0
5.4.2 传统的for语句
for
语句的形式(关键字for
及括号里的部分称作for
语句头):
for (init-statement; condition; expression)
statement
init-statement
必须是以下三种形式中的一种:声明语句、表达式语句或者空语句。
一般情况下,init-statement 负责初始化一个值,这个值会随着循环的进行而改变。condition 作为循环控制的条件:
- 只要 condition 为真,就执行一次 statement,执行后 expression 负责修改 init-statement 初始化的变量,这个变量就是 condition 检查的对象,修改发生在每次循环迭代之后。
- 如果 condition 第一次求值就是
false
,statement 一次都不会执行。
statement 可以是一条单独的语句也可以是一条复合语句。
传统for循环的执行流程
牢记
for
语句头中定义的对象只在for
循环体内可见。
for语句头中的多重定义
init-statement 中也可以定义多个对象,但是只能有一条声明语句,因此所有变量的基础类型必须相同。
省略for语句头的某些部分
for
语句头能省略掉 initializer 、condition 和 expression 中的任何一个(或者全部)。
- 省略 condition 的效果等价于在条件部分写了一个
true
。因为条件的值永远是true
,所以在循环体内必须有语句负责退出循环,否则循环就会无休止地执行下去。 - 省略
expression
,在循环体中必须要求条件部分或者循环体必须改变迭代变量的值。
auto beg = v.begin();
for ( /* 空语句 */; beg != v.end() && *beg >= 0; ++beg)
; //什么也不做
for (int i = 0; /* 条件为空 */ ; ++i) {
//对i进行处理,循环内部的代码必须负责终止迭代过程
}
vector<int> v;
for (int i; cin >> i; /* 表达式为空 */)
v.push_back(i);
5.4.3 范围for语句
C++11标准引入了一种更简单的for
语句,这种语句可以遍历容器或其他序列的所有元素。
范围 for
语句的形式:
for (declaration : expression)
statement
12
- expression 表示的必须是一个序列,即拥有能返回迭代器的
begin
和end
成员。比如用花括号括起来的初始值列表、数组或者者vector
或string
等类型的对象。 - declaration 定义一个变量,序列中的每个元素都应该能转换成该变量的类型(可使用
auto
)。如果需要对序列中的元素执行写操作,循环变量必须声明成引用类型。
每次迭代都会重新定义循环控制变量,并将其初始化为序列中的下一个值,之后才会执行 statement。
vector<int> v = {O, 1, 2, 3, 4, 5, 6, 7, 8, 9};
// 范围变量必须是引用类型,这样才能对元素执行写操作
for (auto &r : v) // 对于v中的每一个元素
r *= 2; // 将v中每个元素的值翻倍
// 等价于<=>
for (auto beg = v.begin(), end = v.end(); beg != end; ++beg) {
auto &r = *beg; // r必须是引用类型,这样才能对元素执行写操作
r *= 2; // 将v中每个元素的值翻倍
}
在范围 for
语句中,预处了end()
的值。一旦在序列中添加(删除)元素,end
函数的值就可能变得无效了。因此,不能通过范围 for
语句增加vector
对象(或者其他容器)的元素。
5.4.4 do while循环
do-while
语句和while
语句非常相似,唯一的区别是,do-while
语句先执行循环体后检查条件。不管条件的值如何,循环至少执行一次。
do-while
语句的形式:
do
statement
while (condition);
do while
语句应该在括号包围起来的条件后面用一个分号表示语句结束。
- 求 condition 的值之前会先执行一次 statement,condition 不能为空。如果 condition 的值为
false
,循环终止;否则重复循环过程。 condition
使用的变量必须定义在循环体之外。- 因为
do-while
语句先执行语句或块,再判断条件,所以不允许在条件部分定义变量。
//不断提示用户输入一对数.然后求其和
string rsp; //作为循环的条件,不能定义在do的内部
do {
cout << " please enter two values: ";
int val1 = 0, val2 = 0;
cin >> val1 >> val2;
cout << "The sum of " << val1 << " and " << val2
<< " = " << val1 + val2 << "
"
<< "More? Enter yes or no: ";
cin >> rsp;
} while (!rsp.empty() && rsp[0] != 'n');
5.5 跳转语句
跳转语句中断当前的执行过程。
5.5.1 break语句
break
语句只能出现在迭代语句或者 switch
语句的内部(包括嵌套在此类循环里的语句或块的内部),负责终止离它最近的 while
、do-while
、for
或者 switch
语句,并从这些语句之后的第一条语句开始执行。
break
语句的作用范围仅限于最近的循环或者switch
。
string buf;
while (cin >> buf && !buf.empty()) {
switch(buf[0]) {
case '-':
// 处理到第一个空白为止
for (auto it = buf.begin()+1; it != buf.end(); ++it) {
if (*it == ' ')
break; // #1,离开for循环
// . . .
}
// 离开for循环:break #1将控制权转移到这里
// 剩余的'-'处理:
break; // #2,结束switch
case '+':
// . . .
}
// 结束switch: break #2将控制权转移到这里
} // 结束while
5.5.2 continue语句
continue
语句只能出现在 for
、while
和 do while
循环的内部,或者嵌套在此类循环里的语句或块的内部,负责终止离它最近的循环的当前一次迭代并立即开始下一次迭代。
- 和
break
语句类似的是,出现在嵌套循环中的continue
语句也仅作用于离它最近的循环。 - 和
break
语句不同的是,只有当switch
语句嵌套在迭代语句内部时,才能在switch
中使用continue
。
continue
语句中断当前迭代后,仍继续执行循环:
- 对于
while
和do-while
语句来说,继续判断条件的值。 - 对于传统的
for
语句来说,继续执行for
语句头中的expression
,之后再判断条件的值。 - 对于范围
for
语句来说,是用序列中的下一个元素初始化循环变量。
string buf ;
while (cin >> buf && !buf.empty()) {
if (buf[O] !='_')
continue; // 接着读取下一个输入
// 程序执行过程到了这里?说明当前的输入是以下画线开始的;接着处理buf......
5.5.3 goto 语句
若能不使用
goto
语句,强烈建议就不要使用,因为它使得程序既难理解又难修改。
goto
语句(labeled statement)是从 goto
语句无条件跳转到同一函数内的另一条语句。goto
语句的形式:
goto label;
其中,label是用于标识一条语句的标识符。带标签语句(labeled statement)是一种特殊的语句,在它之前有一个标识符以及一个冒号:
end: return; // 带标签语句,可以作为goto的目标
-
标签标识符独立于变量和其他标识符的名字,因此,标签标识符可以和程序中其他实体的标识符使用同一个名字而不会相互干扰。
-
但
goto
语句和控制权转向的那条带标签的语句必须位于同一个函数内,同时goto
语句也不能将程序的控制权从变量的作用域之外转移到作用域之内。
// ...
goto end;
int ix =10; // 错误:goto语句绕过了一个带初始化的变量定义
end:
// 错误:此处的代码需妥使用ix,但是goto语句绕过了它的声明
ix = 42;
向后跳过一个已经执行的定义是合法的。跳回到变量定义之前意味着系统将销毁该变量,然后重新创建它。
5.6 try语句块和异常处理
异常 是指程序运行时的反常行为,这些行为超出了函数正常功能的范围。当程序的某一部分检测到一个它无法处理的问题时,需要用到 异常处理。
异常处理机制为程序中 异常检测 和 异常处理 这两部分的协作提供支持。在C++语言中,异常处理包括:
throw
表达式(throw expression),异常检测部分使用throw
表达式表示它遇到了无法处理的问题。我们说throw
引发了异常。try
语句块(try block),异常处理部分使用try
语句块处理异常。try
语句块以关键字try
开 始,并以一个或多个catch
子句(catch clause)结束。try
语句块中代码抛出的异常通常会被某个catch
子句处理。因为catch
子句 “处理” 异常,所以它们也被称作异常处理代码(exception handler)。- 一套异常类(exception class),用于在
throw
表达式和相关的catch
子句之间传递异常的具体信息。
5.6.1 throw表达式
throw
表达式包含关键字 throw
和紧随其后的一个表达式,其中表达式的类型就是抛出的异常类型。throw
表达式后面通常紧跟一个分号,从而构成一条表达式语句。
// 首先检查两条数据是否是关于同一种书籍的
if (item1.isbn() != item2.isbn())
throw runtime_error("Data must refer to same ISBN");
// 如果程序执行到了这里,表示两个ISBN
cout << item1 + item2 << endl;
5.6.2 try语句块
try
语句块的通用语法形式:
try {
program-statements
}
catch (exception-declaration) {
handler-statements
}
catch (exception-declaration) {
handler-statements
} // . . .
跟在try
语句块之后的是一个或多个 catch
子句,catch
子句包括三部分:关键字 catch
、括号内一个对象的声明(异常声明,exception declaration)和一个块。当选中了某个 catch
子句处理异常后,执行与之对应的块。catch
一旦完成,程序会跳过剩余的所有 catch
子句,继续执行后面的语句。
try
语句块中的 program-statements 组成程序的正常逻辑,try
语句块内部声明的变量在块外无法访问,特别是在 catch
子句中也无法访问。
编写处理代码
while (cin >> item1 >> item2) {
try {
// 执行添加两个Sales_item对象的代码
// 如果添加失败,代码抛出一个runtime_error异常
} catch (runtime_error err) {
// 提醒用户两个ISBN必须一致,询问是否重新输入
cout << err.what()
<< "
Try Again? Enter y or n" << endl;
char c;
cin >> c;
if (!cin || c == 'n')
break; // 跳出while循环
}
}
函数在寻找处理代码的过程中退出
寻找处理代码的过程与函数调用链刚好相反。当异常被抛出时, 首先搜索抛出该异常的函数。
- 如果没找到匹配的
catch
子句, 终止该函数, 并在调用该函数的函数中继续寻找。 - 如果还是没有找到匹配的
catch
子句,这个新的函数也被终止, 继续搜索调用它的函数。 - 以此类推,沿着程序的执行路径逐层回退,直至找到适当类型的
catch
子句为止。 - 如果最终没能找到与异常相匹配的
catch
子句,程序会执行名为terminate
的标准库函数。该函数的行为与系统有关,一般情况下,执行该函数将导致程序非正常退出。
如果一段程序没有 try
语句块且发生了异常,系统也会调用 terminate
函数并终止当前程序的执行。
提示:编写异常安全的代码非常困难。
5.6.3 标准异常
C++标准库定义了一组类,用于报告标准库函数遇到的问题。这些异常类也可以在用户编写的程序中使用,它们分别定义在4个头文件中:
-
exception 头文件定义了最通用的异常类
exception
。它只报告异常的发生,不提供任何额外信息。 -
stdexcept 头文件定义了几种常用的异常类。
-
new 头文件定义了
bad_alloc
异常类型。 -
type_info 头文件定义了
bad_cast
异常类型。 -
标准库异常类只定义了几种运算,包括创建或拷贝异常类型的对象,以及为异常类型的对象赋值。
-
我们只能以默认初始化的方式初始化
exception
、bad_alloc
和bad_cast
对象,不允许为这些对象提供初始值。 -
其他异常类的行为恰好相反:应该使用
string
或一个C风格字符串初始化这些类型的对象,但是不允许使用默认初始化的方式。当创建此类对象时,必须提供含有错误相关信息初始值。