文章目录
五、语句
和大多数语言一样,C++
提供了条件执行语句、重复执行相同代码的循环语句和用于中断当前控制流的跳转语句。
通常情况下,语句是顺序执行的。但除非是最简单的程序,否则仅有顺序执行远远不够。因此,C++
语言提供了一组控制流语句以支持更复杂的执行路径。
5.1 简单语句
C++
语言中的大多数语句都以分号结束,一个表达式末尾加上分号就变成了表达式语句。表达式语句的作用是执行表达式并丢弃掉求值结果:
int ival = 0;
ival + 5; // 一条没什么实际用处的表达式语句
std::cout << ival; // 一条有用的表达式语句
-
空语句:最简单的语句是空语句,空语句中只含有一个单独的分号:
int main() { ; // 空语句 }
-
别漏写分号,也别多写分号:多余的空语句一般来说是无害的,但是如果在
if
或者while
的条件后面跟了一个额外的分号就可能完全改变编程的初衷:std::vector<int> vec; auto iter = vec.begin(); // 死循环 while (iter != vec.end()) ; // while 循环体是一条空语句 ++iter; // 递增运算不属于循环的一部分
-
复合语句(块):复合语句是指用花括号括起来的(可能为空的)语句和声明的序列,复合语句也被称作块。一个块就是一个作用域,在块中引入的名字只能在块内部以及嵌套在块中的子块里访问。通常,名字在有限的区域内可见,该区域从名字定义处开始,到名字所在的(最内层)块的结尾为止。
所谓空块,是指内部没有任何语句的一对花括号,空块的作用等价于空语句。
5.2 语句作用域
可以在 if
、switch
、while
和 for
语句的控制结构内定义变量。定义在控制结构当中的变量只在相应语句的内部可见,一旦语句结束,变量也就超出其作用范围了:
while (int i = get_num()) { // 每次迭代时创建并初始化 i
std::cout << i << std::endl;
}
i = 0; // 错误:在循环外部无法访问 i
因为控制结构定义的对象的值马上要由结构本身使用,所以这些变量必须初始化。
5.3 条件语句
C++
语言提供了两种按条件执行的语句。一种是 if
语句,它根据条件决定控制流;另外一种是 switch
语句,它计算一个整型表达式的值,然后根据这个值从几条执行路径中选择一条。
5.3.1 if 语句
简单 if
语句的语法形式是:
if (condition) {
statement
}
if-else
语句的形式是:
if (condition) {
statement
} else {
statement2
}
-
使用
if-else
语句:int grade; if (std::cin >> grade) { const std::vector<std::string> scores = {"F", "D", "C", "B", "A", "A++"}; std::string level; if (grade < 60) { level = scores[0]; } else { level = scores[(grade - 50) / 10]; } std::cout << "grade: " << grade << " level: " << level << std::endl; }
-
嵌套
if
语句:int grade; if (std::cin >> grade) { const std::vector<std::string> scores = {"F", "D", "C", "B", "A", "A++"}; std::string level; if (grade < 60) { level = scores[0]; } else { level = scores[(grade - 50) / 10]; if (grade != 100) { if (grade % 10 > 7) { level += '+'; // 末尾是 8 或 9 的成绩添加一个加号 } else if (grade % 10 < 3) { level += '-'; // 末尾是 0、1 或 2 的成绩添加一个减号 } } } std::cout << "grade: " << grade << " level: " << level << std::endl; }
-
注意使用花括号:有一种常见的错误,本来程序中有几条语句应该作为一个块来执行,但是我们忘了用花括号把这些语句包围
-
悬垂
else
:C++
语言规定,else
与离它最近的尚未匹配的if
匹配,从而消除了程序的二义性 -
使用花括号控制执行路径:要想使
else
分支和外层的if
语句匹配起来,可以在内层的if
语句的两端加上花括号,使其成为一个块。为了避免此类问题,可以在if
或else
之后都写上花括号,从而避免代码混乱不清
5.3.2 switch 语句
switch
语句提供了一条便利的途径使得我们能够在若干固定选择项中做出选择。
unsigned aCnt = 0, eCnt = 0, iCnt = 0, oCnt = 0, uCnt = 0;
char ch;
while (std::cin >> ch) {
switch (ch) {
case 'a':
++aCnt;
break;
case 'e':
++eCnt;
break;
case 'i':
++iCnt;
break;
case 'o':
++oCnt;
break;
case 'u':
++uCnt;
break;
}
}
std::cout
<< "Number of vowel a: \t" << aCnt << '\n'
<< "Number of vowel e: \t" << eCnt << '\n'
<< "Number of vowel i: \t" << iCnt << '\n'
<< "Number of vowel o: \t" << oCnt << '\n'
<< "Number of vowel u: \t" << uCnt << std::endl;
case
关键字和它对应的值一起被称为 case
标签。case
标签必须是整型常量表达式。
-
switch
内部的控制流:如果表达式和某个case
标签的值匹配成功,程序从该标签之后的第一条语句开始执行,直到到达了switch
的结尾或者是遇到一条break
语句为止unsigned vowelCnt = 0; char ch; while (std::cin >> ch) { // 出现元音字母 a e i o u 中的任意一个则 ++vowelCnt switch (ch) { case 'a': case 'e': case 'i': case 'o': case 'u': ++vowelCnt; break; } }
一般不要省略
case
分支最后的break
语句。如果没写break
语句,最好加一段注释说清楚程序的逻辑 -
漏写
break
容易引发缺陷:尽管switch
语句不是非得在最后一个标签后面写上break
,但是为了安全起见,最好这么做 -
default
标签:如果没有任何一个case
标签能匹配上switch
表达式的值,程序将执行紧跟在default
标签后面的语句unsigned vowelCnt = 0, nonVowelCnt = 0; char ch; while (std::cin >> ch) { switch (ch) { case 'a': case 'e': case 'i': case 'o': case 'u': ++vowelCnt; break; default: ++nonVowelCnt; break; } }
即使不准备在
default
标签下做任何工作,定义一个default
标签也是有用的。其目的在于告诉程序的读者,我们已经考虑到了默认的情况,只是目前什么也没做。标签不应该孤零零地出现,它后面必须跟上一条语句或者另外一个
case
标签。如果switch
结构以一个空的default
标签作为结束,则该default
标签后面必须跟上一条空语句或一个空块。 -
switch
内部的变量定义:如果在某处一个带有初值的变量位于作用域之外,在另一处该变量位于作用域之内,则从前一处跳转到后一处的行为是非法行为。C++
语言规定,不允许跨过变量的初始化语句直接跳转到该变量作用域内的另一个位置/* * 编译失败 */ switch (flag) { case true: // 因为程序的执行流程可能绕开下面的初始化语句,所以该 switch 语句不合法 int ival = 0; // 错误:控制流绕过一个显式初始化的变量 int jval; // 正确:因为 jval 没有初始化 break; case false: // 正确:jval 虽然在作用域内,但是它没有被初始化 jval = next_num(); // 正确:给 jval 赋值 }
如果需要为某个
case
分支定义并初始化一个变量,我们应该把变量定义在块内,从而确保后面的所有case
标签都在变量的作用域之外:switch (flag) { case true: { std::string hello = "Hello World"; } break; case false: { std::string hello = "Hello World"; } break; }
5.4 迭代语句
迭代语句通常称为循环,它重复执行操作直到满足某个条件才停下来。while
和 for
语句在执行循环体之前检查条件,do while
语句先执行循环体,然后再检查条件。
5.4.1 while 语句
while
的条件部分可以是一个表达式或者是一个带初始化的变量声明。通常来说,应该由条件本身或者是循环体设法改变表达式的值,否则循环可能无法终止。
定义在 while
条件部分或者 while
循环体内的变量每次迭代都经历从创建到销毁的过程。
-
使用
while
循环:当不确定到底要迭代多少次时,使用while
循环比较合适std::vector<int> vec; int i; while (std::cin >> i) { vec.push_back(i); } // 寻找第一个负值元素 auto beg = vec.begin(); while (beg != vec.end() && *beg >= 0) { ++beg; }
5.4.2 传统的 for 语句
for
语句的语法形式如下
for (init-statement; condition; expression) {
statement
}
关键字 for
及括号里的部分称作 for
语句头。init-statement
必须是以下三种形式中的一种:声明语句、表达式语句或者空语句。
-
for
语句头中的多重定义:和其他的声明一样,init-statement
也可以定义多个对象。但是init-statement
只能有一条声明语句,因此,所有变量的基础类型必须相同:std::vector<int> v; for (decltype(v.size()) i = 0, sz = v.size(); i != sz; ++i) { v.push_back(v[i]); }
-
省略
for
语句头的某些部分:for
语句头能省略掉init-statement
、condition
和expression
中的任何一个(或者全部),但分号必须保留。
5.4.3 范围 for 语句
C++11
新标准引入了一种更简单的 for
语句,这种语句可以遍历容器或其他序列的所有元素。范围 for
语句的语法形式是:
for (declaration : expression) {
statement
}
expression
表示的必须是一个序列,比如用花括号括起来的初始值列表、数组或者 vector
或 string
等类型的对象,这些类型的共同特点是拥有能返回迭代器的 begin
和 end
成员。
declaration
定义一个变量,序列中的每个元素都得能转换成该变量的类型。确保类型相容最简单的办法是使用 auto
类型说明符,这个关键字可以令编译器帮助我们指定合适的类型。如果需要对序列中的元素执行写操作,循环变量必须声明成引用类型。
每次迭代都会重新定义循环控制变量,并将其初始化成序列中的下一个值,之后才会执行 statement
。
std::vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
for (auto &item: v) {
item *= 2; // 将 v 中的每个元素的值翻倍
}
5.4.4 do while 语句
do while
语句和 while
语句非常相似,唯一的区别是 do while
语句先执行循环体后检查循环条件。不管条件的值如何,都至少执行一次循环。do while
语句的语法格式如下:
do {
statement
} while (condition); // do while 语句应该在括号包围起来的条件后面用一个分号表示语句结束
因为对于 do while
来说先执行语句或者块,后判断条件,所以不允许在条件部分定义变量:
// 不断提示用户输入一组数,然后求其和
std::string resp;
do {
std::cout << "please enter two values: ";
int val1, val2;
std::cin >> val1 >> val2;
std::cout << "The sum of " << val1 << " and " << val2
<< " = " << val1 + val2 << "\n\n" << "More? Enter yes or no: ";
std::cin >> resp;
} while (!resp.empty() && resp[0] != 'n');
5.5 跳转语句
跳转语句中断当前的执行过程。C++
语言提供了 4
种跳转语句:break
、continue
、goto
和 return
。
5.3.1 break 语句
break
语句负责终止离它最近的 while
、do while
、for
或 switch
语句,并从这些语句之后的第一条语句开始执行。
break
语句只能出现在迭代语句或者 switch
语句内部(包括嵌套在此类循环里的语句或块的内部)。break
语句的作用范围仅限于最近的循环或者 switch
。
std::string buf;
while (std::cin >> buf && !buf.empty()) {
switch (buf[0]) {
case '-':
// 处理到第一个空白为止
for (auto it = buf.begin() + 1; it != buf.end(); ++it) {
if (*it == ' ') {
break; // 离开 for 循环
}
}
break; // 离开 switch 语句
case '+':
// do something
default:
break; // 离开 switch 语句
}
if (buf == "exit") {
break; // 离开 while 语句
}
}
5.3.2 continue 语句
continue
语句终止最近的循环中的当前迭代并立即开始下一次迭代。continue
语句只能出现在 for
、while
和 do while
循环的内部,或者嵌套在此类循环里的语句或块的内部。
continue
语句中断当前的迭代,但是仍然继续执行循环。
// 只对那些以下划线开头的单词感兴趣
std::string buf;
while (std::cin >> buf && !buf.empty()) {
if (buf[0] != '_') {
continue;
}
// do something
}
5.3.3 goto 语句
goto
语句的作用是从 goto
语句无条件跳转到同一函数内的另一条语句。尽量不要在程序中使用 goto
语句,因为它使得程序既难理解又难修改。goto
语句的语法形式是:
goto label;
其中,label
是用于标识一条语句的标识符。带标签语句是一种特殊的语句,在它之前有一个标识符以及一个冒号。标签标识符独立于变量或其他标识符的名字,因此,标签标识符可以和程序中其他实体的标识符使用同一个名字而不会相互干扰。
#include <iostream>
int main() {
int outer_loop;
int inner_loop;
// 外层循环
for (outer_loop = 0; outer_loop < 5; ++outer_loop) {
// 内层循环
for (inner_loop = 0; inner_loop < 5; ++inner_loop) {
// 模拟一个错误条件
if (inner_loop == 2) {
// 跳转到错误处理
goto error_handler;
}
std::cout << "Outer loop: " << outer_loop << ", Inner loop: " << inner_loop << "\n";
}
}
// 资源清理
cleanup:
{
std::cout << "Cleaning up resources.\n";
return -1;
}
// 错误处理
error_handler:
{
std::cout << "An error occurred at outer loop: " << outer_loop << ", inner loop: " << inner_loop << "\n";
goto cleanup;
}
// 如果没有错误,则正常结束
std::cout << "Program completed successfully.\n";
return 0;
}
5.6 try 语句块和异常处理
异常是指存在于运行时的反常行为,这些行为超出了函数正常功能的范围。典型的异常包括失去数据库连接以及遇到意外输入等。处理反常行为可能是设计所有系统最难的一部分。
当程序的某部分检测到一个它无法处理的问题时,需要用到异常处理。此时,检测出问题的部分应该发出某种信号以表明程序遇到了故障,无法继续下去了,而且信号的发出方无须知道故障将在何处得到解决。一旦发出异常信号,检测出问题的部分也就完成了任务。
如果程序中含有可能引发异常的代码,那么通常也会有专门的代码处理问题。异常处理机制为程序中异常检测和异常处理这两部分的协作提供支持,在 C++
语言中,异常处理包括:
throw
表达式:异常检测部分使用throw
表达式来表示它遇到了无法处理的问题try
语句块:异常处理部分使用try
语句块处理异常。try
语句块以关键字try
开始,并以一个或多个catch
子句结束。try
语句块中代码抛出的异常通常会被某个catch
子句捕获并处理- 一套异常类,用于在
throw
表达式和相关的catch
子句之间传递异常的具体信息
5.6.1 throw 表达式
程序的异常检测部分使用 throw
表达式引发一个异常。throw
表达式包含关键字 throw
和紧跟其后的一个表达式,其中表达式的类型就是抛出的异常类型。
#include <iostream>
#include <stdexcept>
int main() {
int denominator, numerator;
std::cout << "Please enter the numerator and denominator: ";
std::cin >> numerator >> denominator;
if (denominator == 0) {
throw std::runtime_error("denominator can not be zero"); // raise an exception
}
std::cout << numerator << " / " << denominator << " = " << numerator / denominator;
return 0;
}
5.6.2 try 语句块
try
语句块的通用语法格式是:
try {
program-statements
} catch (exception-declaration) {
handler-statements
} catch (exception-declaration) {
handler-statements
} // more catch...
catch
子句包括三部分:关键字 catch
、括号内一个(可能未命名的)对象的声明(称作异常声明)以及一个块。catch
一旦完成,程序跳转到 try
语句块最后一个 catch
子句之后的那条语句继续执行。
- 编写处理代码:使用
catch
子句捕获异常并处理
#include <iostream>
#include <stdexcept>
int main() {
int denominator, numerator;
std::cout << "Please enter the numerator and denominator: ";
std::cin >> numerator >> denominator;
try {
if (denominator == 0) {
throw std::runtime_error("denominator can not be zero"); // raise an exception
}
} catch (std::runtime_error e) {
std::cerr << "encountered a runtime error: " << e.what() << ", set denominator to 1" << std::endl;
denominator = 1;
}
std::cout << numerator << " / " << denominator << " = " << 1.0 * numerator / denominator << std::endl;
return 0;
}
-
函数在寻找处理代码的过程中退出:在复杂系统中,程序在遇到抛出异常的代码前,其执行路径可能已经经过了多个
try
语句块。例如,一个try
语句块可能调用了包含另一个try
语句块的函数,新的try
语句块可能调用了包含又一个try
语句块的新函数,以此类推。寻找处理代码的过程与函数调用链刚好相反。当异常被抛出时,首先搜索抛出该异常的函数。如果没找到匹配的
catch
子句,终止该函数,并在调用该函数的函数中继续寻找。如果还是没有找到匹配的catch
子句,这个新的函数也被终止,继续搜索调用它的函数,以此类推,沿着程序的执行路径逐层回退,直到找到适当类型的catch
子句为止。如果最终还是没能找到任何匹配的
catch
子句,程序转到名为terminate
的标准库函数。该函数的行为与系统有关,一般情况下,执行该函数将导致程序非正常退出。 -
编写异常安全的代码非常困难:异常中断了程序的正常流程。异常发生时,调用者请求的一部分计算可能已经完成了,另一部分则尚未完成。通常情况下,略过部分程序意味着某些对象处理到一半就戛然而止,从而导致对象处于无效或未完成的状态,或者资源没有正常释放,等等。那些在异常发生期间正确地执行了清理工作的程序被称作异常安全的代码。然而经验表明,编写异常安全的代码非常困难。
5.6.3 标准异常
C++
标准库定义了一组类,用于报告标准库函数遇到的问题。这些异常类也可以在用户编写的程序中使用,它们分别定义在 4
个头文件中:
exception
:定义了最通用的异常类exception
,它只报告异常的发生,不提供任何额外信息stdexcept
:定义了几种常用的异常类new
:定义了bad_alloc
异常类型type_info
:定义了bad_cast
异常类型
头文件 <stdexcept>
定义的异常类:
类名 | 说明 |
---|---|
exception | 最常见的问题 |
runtime_error | 只有在运行时才能检测出的问题 |
range_error | 运行时错误:生成的结果超出了有意义的值域范围 |
overflow_error | 运行时错误:计算上溢 |
underflow_error | 运行时错误:计算下溢 |
login_error | 程序逻辑错误 |
domain_error | 逻辑错误:参数对应的结果值不存在 |
invalid_argument | 逻辑错误:无效参数 |
length_error | 逻辑错误:试图创建一个超出该类型最大长度的对象 |
out_of_range | 逻辑错误:使用一个超出有效范围的值 |
标准库异常类只定义了几种运算,包括创建或拷贝异常类型的对象,以及为异常类型的对象赋值。
我们只能以默认初始化的方式初始化 exception
、bad_alloc
和 bad_cast
对象,不允许为这些对象提供初始值。
其他异常类型的行为则恰好相反:应该使用 string
对象或者 C
风格字符串初始化这些类型的对象,但是不允许使用默认初始化的方式。当创建此类对象时,必须提供初始值,该初始值含有错误相关的信息。
异常类型只定义了一个名为 what
的成员函数,该函数没有任何参数,返回值是一个指向 C
风格字符串的 const char*
。该字符串的目的是提供关于异常的一些文本信息。