一个表达式,末尾加上分号就变成了表达式语句(expression statement)。表达式语句的作用是执行表达式并丢弃掉求值结果。最简单的语句是空语句(null statement),空语句中只含有一个单独的分号。如果在程序的某个地方,语法上需要一条语句但是逻辑上不需要,此时应该使用空语句。一种常见的情况是,当循环的全部工作在条件部分就可以完成时,我们通常会用到空语句。例如,我们想读取输入流的内容直到遇到一个特定的值为止,除此之外什么事情也不做:
1
2
3// 重复读入数据直至到达文件末尾或某次输入的值等于 sought
while (cin >> s && s != sought)
; // 空语句使用空语句时应该加上注释,从而令读这段代码的人知道该语句是有意省略。
下面的片段包含两条语句:表达式语句和空语句。
1
ival = v1 + v2;; // 正确:第二个分号表示一条多余的空语句
多余的空语句一般来说是无害的,但是如果在
if
或者while
的条件后面跟了一个额外的分号就可能完全改变我们的初衷。例如,下面的代码将无休止地循环下去:1
2
3// 出现了糟糕的情况:额外的分号,循环体是那条空语句
while (iter != svec.end()) ; // while 循环体是那条空语句
++iter; // 递增运算不属于循环的一部分虽然从形式上来看执行递增运算的语句前面有缩进,但它并不是循环的一部分。循环条件后面跟着的分号构成了一条空语句,它才是真正的循环体。
多余的空语句并非总是无害的。
复合语句(compound statement)是指用花括号括起来的(可能为空的)语句和声明的序列,复合语句也被称作块(block)。一个块就是一个作用域,在块中引入的名字只能在块内部以及嵌套在块中的子块里访问。 通常,名字在有限的区域内可见,该区域从名字定义处开始,到名字所在的(最内层)块的结尾为止。所谓空块,是指内部没有任何语句的一对花括号。空块的作用等价于空语句,空语句显然没有空块更加直白。
定义在控制结构当中的变量只在相应语句的内部可见,一旦语句结束,变量也就超出其作用范围了,如果其他代码也需要访问控制变量,则变量必须定义在语句的外部。
C++ 为解决悬垂
else
(dangling else)问题,规定else
与离它最近的尚未匹配的if
匹配,从而消除了程序的二义性。switch
语句首先对括号里的表达式求值,该表达式紧跟在关键字switch
的后面,可以是一个初始化的变量声明。表达式的值转换成整数类型,然后与每个case
标签的值比较。case
关键字和它对应的值一起被称为case
标签(case label)。case
标签必须是整型常量表达式。任何两个case
标签的值不能相同,否则就会引发错误。另外,default
也是一种特殊的case
标签。如果某个
case
标签匹配成功,将从该标签开始往后顺序执行所有case
分支,除非程序显式地中断了这一过程,否则直到switch
的结尾处才会停下来。要想避免执行后续case
分支的代码,我们必须显式地告诉编译器终止执行过程。大多数情况下,在下一个case
标签之前应该有一条break
语句。每个case
标签只能对应一个值,但是有时候我们希望两个或更多个值共享同一组操作。此时,我们就故意省略掉break
语句,使得程序能够连续执行若干个case
标签。例如,我们想统计所有元音字母出现的总次数:1
2
3
4
5
6
7
8
9
10
11
12
13unsigned vowelCnt = 0;
// ...
switch (ch)
{
// 出现了 a、e、i、o 或 u 中的任意一个都会将 vowelCnt 的值加 1
case 'a':
case 'e':
case 'i':
case 'o':
case 'u':
++vowelCnt;
break;
}上述代码片段中
switch
语句还可以写成下面这种较为简洁的形式:1
2
3
4
5
6switch (ch)
{
case 'a': case 'e': case 'i': case 'o': case 'u':
++vowelCnt;
break;
}一般不要省略
case
分支最后的break
语句。如果没写break
语句,最好加一段注释说清楚程序的逻辑。尽管switch
语句不是非得在最后一个标签后面写上break
,但是为了安全起见,最好这么做。因为这样的话,即使以后再增加新的case
分支,也不用再在前面补充break
语句了。如果没有任何一个
case
标签能匹配上switch
表达式的值,程序将执行紧跟在default
标签(default label)后面的语句。即使不准备在
default
标签下做任何工作,定义一个default
标签也是有用的。其目的在于告诉程序的读者,我们已经考虑到了默认的情况,只是目前什么也没做。如果在某处一个带有初值的变量位于作用域之外,在另一处该变量位于作用域之内,则从前一处跳转到后一处的行为是非法行为。C++ 语言规定,不允许跨过变量的初始化语句直接跳转到该变量作用域内的另一个位置。如果需要为某个
case
分支定义并初始化一个变量,我们应该把变量定义在块内,从而确保后面的所有case
标签都在变量的作用域之外。在
while
语句(while statement)中,定义在while
条件部分或者while
循环体内的变量每次选代都经历从创建到销毁的过程。for
语句中对循环控制变量的修改发生在每次循环迭代之后。for
语句头中定义的对象只在for
循环体内可见。和其他的声明一样,
for
语句的 init_statement 也可以定义多个对象。但是 init_statement 只能有一条声明语句,因此,所有变量的基础类型必须相同。举个例子,我们用下面的循环把vector
的元素拷贝一份添加到原来的元素后面:1
2
3// 记录 v 的大小,当到达原来的最后一个元素后结束循环
for (decltype(v.size()) i = 0, sz = v.size(); i != sz; ++i)
v.push_back(v[i]);在这个循环中,我们在 init_statement 里同时定义了索引 i 和循环控制变量 sz。
for
语句头能省略掉init_statement
、condition
和expression
中的任何一个(或者全部),省略掉 expression 时,就要求条件部分或者循环体必须改变迭代变量的值。范围
for
语句(range for statement)的语法形式是:1
2for (declaration : expression)
statementexpression 表示的必须是一个序列,比如用花括号括起来的初始值列表、数组或者
vector
或string
等类型的对象,这些类型的共同特点时拥有能返回迭代器begin
和end
成员。declaration 定义一个变量,序列中的每个元素都得能转换成该变量的类型,确保类型相容最简单的办法是使用auto
类型说明符,这个关键字可以令编译器帮助我们指定合适的类型。如果需要对序列中的元素执行写操作,循环变量必须声明成引用类型。 每次迭代都会重新定义循环控制变量,并将其初始化成序列中的下一个值,之后才会执行 statement。不能通过范围
for
语句增加vector
对象(或者其他容器)的元素,因为在范围for
语句中,预存了end()
的值。一旦在序列中添加(删除)元素,end
函数的值就可能变得无效了。do while
语句应该在括号包围起来的条件后面用一个分号表示语句结束。break
语句(break statement)负责终止离它最近的while
、do while
、for
或switch
语句,并从这些语句之后的第一条语句开始继续执行。break
语句只能出现在迭代语句或者switch
语句内部(包括嵌套在此类循环里的语句或块的内部)。break
语句的作用范围仅限于最近的循环或者switch
。continue
语句(continue statement)终止最近的循环中的当前迭代并立即开始下一次迭代。continue
语句只能出现在for
、while
和do while
循环的内部,或者嵌套在此类循环里的语句或块的内部。和break
语句类似的是,出现在嵌套循环中的continue
语句也仅作用于离它最近的循环。和break
语句不同的是,只有当switch
语句嵌套在迭代语句内部时,才能在switch
里使用continue
。continue
语句中断当前的迭代,但是仍然继续执行循环。对于while
或者do while
语句来说,继续判断条件的值;对于传统的for
循环来说,继续执行for
语句头的expression
;而对于范围for
语句来说,则是用序列中的下一个元素初始化循环控制变量。goto
语句(goto statement)的作用是从goto
语句无条件跳转到同一函数内的另一条语句。goto
语句的语法形式是:1
goto label;
其中,label 是用于标识一条语句的标示符。带标签语句(labeled statement)是一种特殊的语句,在它之前有一个标示符以及一个冒号:
1
end: return; // 带标签语句,可以作为 goto 的目标
标签标示符独立于变量或其他标示符的名字,因此,标签标示符可以和程序中其他实体的标示符使用同一个名字而不会相互干扰。
goto
语句和控制权转向的那条带标签的语句必须位于同一个函数之内。和switch
语句类似,goto
语句也不能将程序的控制权从变量的作用域之外转移到作用域之内:1
2
3
4
5
6// ...
goto end;
int ix = 10; // 错误:goto 语句绕过了一个带初始化的变量定义
end:
// 错误:此处的代码需要使用 ix,但是 goto 语句绕过了它的声明
ix = 42;向后跳过一个已经执行的定义是合法的。跳回到变量定义之前意味着系统将销毁该变量,然后重新创建它:
1
2
3
4
5
6
7// 向后跳过一个带初始化的变量定义是合法的
begin:
int sz = get_size();
if (sz <= 0)
{
goto begin;
}在上面的代码中,
goto
语句执行后将销毁 sz。因为跳回到 begin 的动作跨过了 sz 的定义语句,所以 sz 将重新定义并初始化。在 C++ 语言中,异常处理包括:
throw
表达式(throw expression),异常检测部分使用throw
表达式来表示它遇到了无法处理的问题。我们说throw
引发(raise)了异常。try
语句块(try block),异常处理部分使用try
语句块处理异常。try
语句块以关键字try
开始,并以一个或多个catch
子句(catch clause)结束。try
语句块中代码抛出的异常通常会被某个catch
子句处理。因为catch
子句“处理”异常,所以它们也被称作异常处理代码(exception handler)。- 一套异常类(exception class),用于在
throw
表达式和相关的catch
子句之间传递异常的具体信息。
程序的异常检测部分使用
throw
表达式引发一个异常。throw
表达式包含关键字throw
和紧随其后的一个表达式,其中表达式的类型就是抛出的异常类型。throw
表达式后面通常紧跟一个分号,从而构成一条表达式语句。类型
runtime_error
是标准库异常类型的一种,定义在stdexcept
头文件中。必须初始化runtime_error
的对象,方式是给它提供一个string
对象或者一个 C 风格的字符串,例如:1
throw runtime_error("Data must refer to same ISBN");
try
语句块的通用语法形式是:1
2
3
4
5
6
7
8
9
10
11
12try
{
program-statements
}
catch (exception-declaration)
{
handler-statements
}
catch (exception-declaration)
{
handler-statements
} // ...跟在
try
块之后的是一个或多个catch
子句。catch
子句包括三部分:关键字catch
、括号内一个(可能未命名的)对象的声明(称作异常声明,exception declaration)以及一个块。当选中了某个catch
子句处理异常之后,执行与之对应的块。catch
一旦完成,程序跳转到try
语句块最后一个catch
子句之后的那条语句继续执行。try
语句块内声明的变量在外部无法访问,特别是在catch
子句内也无法访问。每个标准库异常类都定义了名为
what
的成员函数,这些函数没有参数,返回值是 C 风格字符串(即const char*
)。其中,runtime_error
的what
成员返回的是初始化一个具体对象时所用的string
对象的副本。寻找处理代码的过程与函数调用链刚好相反。当异常被抛出时,首先搜索抛出该异常的函数。如果没找到匹配的
catch
子句,终止该函数,并在调用该函数的函数中继续寻找。如果还是没有找到匹配的catch
子句,这个新的函数也被终止,继续搜索调用它的函数。以此类推,沿着程序的执行路径逐层回退,直到找到适当类型的catch
子句为止。如果最终还是没能找到任何匹配的catch
子句,程序转到名为terminate
的标准库函数。该函数的行为与系统有关,一般情况下,执行该函数将导致程序非正常退出。对于那些没有任何try
语句块定义的异常,也按照类似的方式处理:毕竟,没有try
语句块也就意味着没有匹配的catch
子句。如果一段程序没有try
语句块且发生了异常,系统会调用terminate
函数并终止当前程序的执行。C++ 标准库定义了一组类,用于报告标准库函数遇到的问题,它们分别定义在 4 个头文件中:
exception
头文件定义了最通用的异常类exception
。它只报告异常的发生,不提供任何额外信息。stdexcept
头文件定义了几种常用的异常类,如下表所示。new
头文件定义了bad_alloc
异常类型。type_info
头文件定义了bad_cast
异常类型。
<stdexcept>定义的异常类 exception 最常见的问题 runtime_error 只有在运行时才能检测出的问题 range_error 运行时错误:生成的结果超出了有意义的值域范围 overflow_error 运行时错误:计算上溢 underflow_error 运行时错误:计算下溢 logic_error 程序逻辑错误 domain_error 逻辑错误:参数对应的结果值不存在 invalid_argument 逻辑错误:无效参数 length_error 逻辑错误:试图创建一个超出该类型最大长度的对象 out_of_range 逻辑错误:使用一个超出有效范围的值 标准库异常类只定义了几种运算,包括创建或接贝异常类型的对象,以及为异常类型的对象赋值。只能以默认初始化的方式初始化
exception
、bad_alloc
和bad_cast
对象,不允许为这些对象提供初始值。其他异常类型的行为则恰好相反:应该使用string
对象或者 C 风格字符串初始化这些类型的对象,但是不允许使用默认初始化的方式。当创建此类对象时,必须提供初始值,该初始值含有错误相关的信息。异常类型只定义了一个名为what
的成员函数,该函数没有任何参数,返回值是一个指向 C 风格字符串的const char*
。该字符串的目的是提供关于异常的一些文本信息。what
函数返回的 C 风格字符串的内容与异常对象的类型有关。如果异常类型有一个字符串初始值,则what
返回该字符串。对于其他无初始值的异常类型来说,what
返回的内容由编译器决定。编写一段程序,从标准输入读取两个整数,当第二个数是 0 时抛出异常,使用
try
语句块捕获异常,catch
子句为用户输出一条提示信息,询问是否输入新数并重新执行try
语句块的内容。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20int a, b;
std::invalid_argument err("Wrong input for the 2nd argument!");
while (cin >> a >> b)
{
try
{
if (0 == b)
throw err;
std::cout << "a/b = " << a / b;
}
catch (const std::invalid_argument &e)
{
std::cerr << e.what() << '\n';
std::cout << "Try again? Enter y or n" << std::endl;
char c;
cin >> c;
if (!cin || 'n' == c)
break;
}
}
C++ Primer - 第 5 章 语句
猜你喜欢
Thank you for your donate!
- 本文链接: https://blog.shipengx.com/archives/c7171333.html
- 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!