第五章 语句

104 阅读13分钟

和大多数语言一样,C++ 提供了一组控制流(flow-of-control)语句以支持复杂的执行路径,包括条件语句、循环语句和中断当前控制流的跳转语句。

简单语句

一个以分号结尾的表达式称为表达式语句(expression statement)。只含有一个分号的语句称为空语句(null statement)。当程序中某处语法上需要一条语句但逻辑上不需要时,可以使用空语句,比如:循环的全部工作在条件中已经完成而不需要循环体。

while (cin >> s && s != sought)
  ;

空语句应该添加注释以说明用途。

不要漏写分号,也不要多写分号,多余的空语句并非总是无害的。

{} 括起来的语句序列称为复合语句(compound statement),也称为(block)。一个块就是一个作用域。如果在程序中某处,语法上需要一条语句,但逻辑上需要多条语句,则应使用 {} 括起来将其转为一条复合语句,比如:whilefor 的循环体。内部没有任何语句的块称为空块,相当于空语句。

块不以分号结尾。

语句作用域

ifswitchwhilefor 语句的控制结构中可以定义变量,这些变量只在相应语句内部可见。

条件语句

C++ 提供了两种条件控制语句:

  1. if 语句,按条件决定控制流;
  2. switch 语句,计算一个整型表达式的值,根据求值结果选择一条执行路径。

if 语句

if 语句(if statement):根据判断条件,决定是否执行另一条语句。if 语句有两种形式:

  1. else 分支
    if (condition)
        statement
    
  2. else 分支,也称为 if else 语句
    if (condition)
        statement1
    else
        statement2
    

condition 可以是一个表达式,也可以是一个初始化变量声明。表达式和变量都必须能转为布尔型。通常,statement1statement2 都是块语句。

if 语句本身也是一条语句,因此可以作为 statement 嵌套使用。在 ifelse 后面使用 {} 可以让代码更加清晰,避免了 statement 中包含两条及以上语句时可能产生的 bug。

if (condition1)
  statement1
else
  statement2
  statement3

在一系列嵌套的 if 语句中,if 分支多于 else 分支时,如何确定与 else 匹配的那个 if 这一问题,通常称为悬垂 else(dangling else)。不同语言解决该问题的思路不同。C++ 规定 else 与离它最近的那个尚未匹配的 if 相匹配。介于此,可使用 {} 来控制代码流程。

if (condition1)
  if (condition2)
    statement1
else
  statement2

switch 语句

switch 语句(switch statement)可以在若干固定选项中选择一个执行。

switch (expression) {
  case label1:
    statement11
    ...
  case label2:
    statement21
    ...
  ...
  default:
    statement00
    ...
}

expression 可以是一个表达式,也可以是一个初始化的变量声明。case 关键字和它所对应的值称为 case 标签(case label),case 标签的值必须是整型常量表达式。default 标签(default label)是一种特殊的 case 标签。expression 的值将转为整数类型,然后与每个 case 标签的值比较。找到匹配的 case 标签之后,就从该标签开始执行,如果没有匹配的 case 标签,则执行 default 标签后面的语句,直到 break 语句或 switch 结尾。break 语句将控制权转移到 switch 语句外部。任何两个 case 标签的值不能相同,否则将引发错误。

一般情况下,每个 case 结尾都会有一个 break,如果没有,应补充注释进行说明。

尽管 switch 语句不要求每个标签后面必须有一个 break,但为了安全起见,最好要有。这样可以保证后续增加新的 case 分支时不用再给前面补充 break 语句。

即使不需要给 default 标签做任何工作,也可以定义一个 default 标签,用于告诉读者,已经考虑了默认情况,只是目前无需处理。

标签后面要么是另一个 case 标签,要么是一条语句,至少是空语句或空块。

switch...case 会发生代码跳转,C++ 规定,不允许越过初始化从变量有效区域外部跳转到内部——不管是否使用该变量,定义时未初始化的变量不受影响。使用语句块可以为 case 分支定义并初始化变量,保证其他 case 分支都在变量所处的作用域之外。

变量的有效区域是从定义开始到所处作用域末尾结束,break 不会限制变量的有效区域。

switch (b) {
  int s1;
  int s2 = 1; // 错误,可能越过初始化访问
  case true:
    int t1;
    int t2 = 1; // 错误,可能越过初始化访问
    s1 = 10;
    s2 = 10;
    break;
  case false:
    t1 = 1;
}

迭代语句

while 语句

while 语句(while statement)形式如下:

while (condition)
  statement

condition 可以是一个表达式或一个带有初始化的变量声明,但不能为空。while 语句先判断条件再执行循环体,直到 condition 值为 false

定义在 while 条件部分或 while 循环体内的变量每次迭代都经历从创建到销毁的过程。

不确定需要迭代多少次或者想在循环结束后访问循环控制变量时,都可以使用 while 循环。

传统 for 语句

for 语句形式如下:

for (init-statement; condition; expression)
  statement

关键字 for 和括号内的部分称为 for 语句头init-statement 必须是以下三种形式之一:声明语句、表达式语句、空语句。

一般情况下,init-statement 负责初始化;condition 是循环控制条件,只要为 true,就执行 statement,直到变成 falseexpression 负责修改 init-statement 初始化的变量,每次循环迭代之后执行。

for 语句头中定义的对象只在循环体内可见。init-statement 可以定义多个对象,但还只是一条语句,因此定义的所有变量的基础类型都必须相同。

init-statementconditionexpression 中任何一个都可以省略。如果无需初始化,可以使用一条空语句作为 init-statement。如果省略 condition,则条件的值默认为 trueexpression 也可以省略,这要求条件部分或循环体必须改变迭代变量的值。

范围 for 语句

范围 for 语句(range for statement)的语法形式如下:

for (declaration: expression)
  statement

expression 必须是一个序列,比如:{} 括起来的初始值列表、数组、或者 vectorstring 等类型的对象。这些类型的对象都拥有能返回迭代器的 beginend 成员。

declaration 定义一个变量,序列中每个元素都能转为该类型,循环变量声明为引用类型即可执行写操作。每次迭代都会重新定义循环控制变量,并初始化为序列中的下一个值,然后执行 statement

范围 for 语句的定义来源于等价的传统 for 语句:

vector<int> v = {0, 1, 2, 3, 4};
for (auto &r: v) {
  // codes
}

// 等价于下面的传统 for 语句
for (auto beg = v.begin(), end = v.end(); beg != end; ++beg) {
  auto &r = *beg;
  // codes
}

由于增删元素会导致迭代器失效,因此范围 for 语句中不能对序列增删。

do...while 语句

do...while 语句(do while statement)先执行循环体再检查条件,如果条件值为 true,继续执行循环体,直到条件值为 false。语法形式如下:

do
  statement
while (condition);

do...while 语句必须以分号结尾。

condition 不能为空。condition 使用的变量必须定义在循环体外。由于 do...while 先执行语句,后判断条件,所以不允许在条件部分定义变量。

如果允许,则变量的使用出现在定义之前。

跳转语句

跳转语句可以中断当前的执行过程。C++ 语言提供了 4 种跳转语句:breakcontinuegotoreturn

break 语句

break 语句(break statement)可以终止离它最近的 whiledo...whileforswitch 语句,并从这些语句之后的第一条语句开始执行。

break 语句只能出现在迭代语句或 switch 语句内部(包括嵌套在此类循环中的语句或块的内部),break 语句的作用范围仅限于最近的循环或 switch

continue 语句

continue 语句(continue statement)终止离得最近的循环的迭代并立即开始下一次迭代:

  • 对于 whiledo...while 语句,继续判断当前的值;
  • 对于传统 for 循环,继续执行 expression
  • 而对于范围 for 语句,用序列中的下一个元素初始化循环控制变量。

continue 语句只能出现在 forwhiledo...while 循环的内部,或者嵌套在此类循环里的语句或块的内部。

goto 语句

goto 语句(goto statement)的作用是无条件跳转到同一函数中的另一条语句。

不要使用 goto 语句,它使得程序难以理解且难以修改。

goto label;

其中,label 是用于标识一条语句的标识符,带标签的语句(labeled statement)形式与下面类似:

end: return;

标签标识符独立于变量或其它标识符的名字,两者间不会有命名冲突。同样地,goto 语句跳转时,也不能越过初始化从变量的有效区域外部跳转到内部。但允许向后跳过一个已执行的定义,跳回变量定义之前意味着系统将销毁该变量。

try 语句块和异常处理

异常是指存在于运行时的反常行为,这些行为超出了函数正常功能的范围。处理反常行为可能是设计所有系统中最难的一部分。当程序的某部分检测到无法处理的问题时,需要用到异常处理。此时,检测出问题的部分应该发出某种信号以表明程序遇到故障。

如果程序中含有可能引发异常的代码,那么通常也会有专门的代码处理这些问题。异常处理机制为程序中异常检测和异常处理这两部分的协作提供支持。C++ 中的异常处理包括:

  • throw 表达式,异常检测部分使用 throw 表达式来表示遇到了无法处理的问题,称作 throw 引发了异常。
  • try 语句块用于异常处理。它以关键字 try 开始,并以一个或多个 catch 子句结束。catch 子句处理异常,所以它们也称为异常处理代码(exception handler)。
  • 异常类,用于在 throw 表达式和相关 catch 子句之间传递异常的具体信息。

throw 表达式

throw 表达式包含关键字 throw 和紧随其后的一个表达式,该表达式的类型就是抛出的异常类型。

throw runtime_error("Wrong!");

抛出异常将终止当前函数,并把控制权转移给能处理该异常的代码。

runtime_error 是标准库异常类型的一种,定义在 stdexcept 头文件中。初始化 runtime_error 对象时,需要给它提供一个 string 对象或者一个 C 风格字符串,包含了关于异常的辅助信息。

try 语句块

try 语句块的通用语法形式如下:

try {
  program-statements
} catch (exception-declaration) {
  handler-statements
} catch (exception-declaration) {
  handler-statements
} // ...

try 关键字后面紧跟一个块,称为 try 语句块,其中的 program-statements 是程序的正常逻辑。try 语句块是一个单独的作用域。try 语句块后面有一个或多个 catch 子句。catch 子句包含三个部分:关键字 catch异常声明(exception declaration)——一个(可能未命名的)对象的声明、语句块。执行完 try 语句块之后,找到第一个与抛出异常类型匹配的 catch 子句,该 catch 块执行完之后跳转到最后一个 catch 子句的后面。

每个标准库异常类都定义了名为 what 的成员函数。这些函数没有参数,返回值是 C 风格字符串,runtime_errorwhat 成员返回的是初始化一个具体对象时所用的 string 对象的副本。

在复杂系统中,程序遇到抛出异常的代码之前,执行路径可能已经经过了多个 try 语句块。寻找处理代码的过程与函数调用链刚好相反。异常抛出时,首先搜索抛出该异常的函数。如果没有匹配的 catch 子句,终止该函数,并在调用该函数的函数中继续寻找。以此类推,沿着程序的执行路径逐层回退,直到找到合适类型的 catch 子句为止。

如果最终还是没有找到匹配的 catch 子句,程序转到名为 terminate 的标准库函数。该函数的行为与系统有关,一般执行该函数将导致程序非正常退出。

异常中断了程序的正常流程。异常发生时,调用者请求的一部分计算可能已经完成,另一部分尚未完成。导致对象处于无效或未完成的状态,或者资源没有正常释放,等等。异常发生期间正确执行 “清理” 工作的程序被称作异常安全(exception safe)的代码。然而经验表明,编写异常安全的代码非常困难。

有些程序在异常发生时只是简单地终止程序,无需担心异常安全的问题。那些确实需要处理异常并继续执行的程序,必须时刻清楚异常触发时机,异常发生后程序应如何确保对象有效、资源无泄漏、程序处于合理状态等。

标准异常

C++ 标准库定义了一组类,用于报告标准库函数遇到的问题。这些异常类也可以在用户编写的程序中使用,它们分别定义在 4 个头文件中:

  • exception 头文件定义了最通用的异常类 exception,只报告异常的发生,不提供任何额外信息。
  • stdexcept 头文件定义了几种常用的异常类。
  • new 头文件定义了 bad_alloc 异常类型。
  • type_info 头文件定义了 bad_cast 异常类型。

stdexcept.png

标准库异常类只定义了几种运算,包括创建或拷贝异常类型的对象,以及为异常类型的对象赋值。

exceptionbad_allocbad_cast 对象只能默认初始化,不能为其提供初始值。其它异常类型刚好相反,应使用 string 对象或 C 风格字符串初始化,不允许默认初始化。

异常类型只定义了一个名为 what 的成员函数,该函数没有参数,返回值是一个指向 C 风格字符串的 const char*,该字符串的目的是提供关于异常的一些文本信息。what 函数返回的 C 风格字符串的内容与异常对象的类型有关。如果异常类型有一个字符串初始值,则 what 返回该字符串,对于其它无初始值的异常类型,what 返回的内容由编译器决定。