《C++ primer》chapter 5:语句
1. 简单语句
C++语言的大多数语句都以分号结束,一个表达式末尾加上分号就变成了表达式语句。表达式语句都作用是执行表达式并丢弃掉求值结果。
空语句 空语句是最简单的语句,只含有一个单独的分号。如果程序的某个地方,语法上需要一条语句但逻辑上不需要,此时应该使用空语句,一种常见的情况是,当循环的全部工作在条件部分就可以完成,我们通常会用到空语句。
// 重复读入数据直到文件末尾或某次输入的值等于sought
while (cint >> s && s != sought)
; // 空语句
使用空语句时应该加上注释,从而令读这段代码的人知道该语句时有意省略的
不要漏写也别多写分号
复合语句(块) 用花括号括起来的语句和声明序列,复合语句也被称作块(block)。一个块就是一个作用域,在块中引入的名字只能在块内部以及嵌套在块中的子块里访问。
如果在程序的某个地方,语法上需要一条语句但是逻辑上需要多条语句,则应该使用复合语句。
while (val <= 10) {
sum += val;
++val;
}
块不以分号作为结束。
空块上指内部没有任何语句的一对花括号。空块的作用等价于空语句。
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
3. 条件语句
if语句
if else语句
嵌套if语句
switch语句
switch语句提供了一条便利的途径使得我们能在若干固定选项中作出选择。
// 为每个元音字母出现的次数计数
unsigned aCnt = 0, eCnt = 0, iCnt = 0, oCnt = 0, uCnt = 0;
char ch;
while (cin >> ch) {
switch (ch) {
case 'a':
++aCnt;
break;
case 'e':
++eCnt;
break;
case 'i':
++iCnt;
break;
case 'o':
++oCnt;
break;
case 'u':
++uCnt;
break;
}
}
case关键字和它对应的值一起被称为case标签,case标签必须上整型常量表达式:
char ch = getVal();
int ival = 42;
switch (ch) {
case 3.14; // 错误,case标签不是一个整数
case ival; // 错误,case标签不是一个常量
}
理解程序在case标签之间的执行流程非常重要,如果某个case标签匹配成功,则从该case标签开始往后顺序执行所有case分支,除非程序显示地中断这一过程,否则知道switch语句结尾处才会停下来,大多数情况下,在下一个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;
}
// 或者写成一行
switch (ch)
{
case 'a': case 'e': case 'i': case 'o': case 'u':
++vowelCnt;
break;
}
一般不要省略case分支最后的break语句。如果没写break语句,最好加一段注释说清楚程序的逻辑。
漏写break容易引发缺陷
// 警告:不正确的程序逻辑
switch (ch) {
case 'a':
++aCnt; // 此次应该有一条break语句
case 'e':
++eCnt; // 此次应该有一条break语句
...
case 'u':
++uCnt;
}
// 如果ch的值时‘e’,代码会将eCnt,以及后面的iCnt, oCnt, uCnt都加1
default标签 如果没有任何一个case标签能匹配上switch表达式的值,程序将执行紧跟在default标签后面的语句。
switch (ch) {
case 'a': case 'e': case 'i': case 'o': case 'u':
++vowelCnt;
break
default:
// 如果ch不是元音字母,就从default标签开始执行并将otherCnt加 1
++otherCnt;
break;
}
switch内部的变量定义
如果在某处一个带初始值的变量位于作用域之外,在另一处该变量位于作用域之内,则从前一处跳到后一处的行为时非法的:
case true:
// 因为程序的执行流程可能绕开下面的初始化语句,所以该switch语句不合法
string file_name; // 错误,控制流绕过一个隐式初始化的变量
int ival = 0; // 错误,控制流绕过一个显示初始化的变量
int jval; // 正确,因为jval没有被初始化
break;
case false:
jval = next_num(); // 正确,jval虽然在作用域内,但没有被初始化
if (file_name.empty()) // file_name在作用域内,但没有被初始化
//
上述代码,一旦控制流直接跳到false分支,也就同时略过了变量file_name和ival的初始化,此时这两个变量位于作用域之内,跟在false之后的代码试图在未初始化的情况下使用它们,这显然行不通。因此,C++语言规定,不允许跨过变量的初始化语句直接跳到该变量作用域的另一个位置。
如果需要为某个case分支定义并初始化一个变量,应该把变量定义在块内,从而确保后面的所有case标签都在变量作用域之外。
case true:
{
// 正确,声明语句位于语句块内部
string file_name = get_file_name();
// ...
}
break;
case false:
if (file_name.empty()) // 错误,file_name不在作用域之内
4. 迭代语句
4.1 while语句
只要条件为真,while语句就重复执行循环体,它的语法形式为
while (condition)
statement
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
4.2 传统的for语句
for语句的语法形式是
for (init-statement; condition; expression)
statement
Init-statement必须是以下三种形式的一种,声明语句,表达式语句,或者空语句。
传统for循环:
for (decltype(s.size()) index = 0;
index != s.size() && !isspace(s[index]); ++index)
s[index] = toupper(s[index]); // 将当前字符改成大写形式
牢记for语句头中定义的对象只在for循环体内可见,因此上面的例子中,for循环结束后index就不可用了。
for语句头多重定义
和其他声明一样,init-statement也可以定义多个对象,但init-statement只能有一条声明语句,因此所有变量的基础类型必须相同。
// 记录v的大小,当达到原来的最后一个元素后结束循环
for (decltype(v.size()) i = 0, sz = v.size(); i != sz; ++i)
v.push_back(v[i]);
省略for语句头的某些部分
for语句头能省略掉init-statement, condition和expression中的任何一个。
// 如果无须初始化,则可以使用一条空语句作为init-statement
auto beg = v.begin();
for (/*空语句*/; beg != v.end() && *beg >= 0; ++beg)
; // 什么也不做
// 省略condition的效果等价于条件部分写了一个true
for (int i = 0; /*条件为空*/; ++i) {
// 对i进行处理,循环内部的代码必须负责终止迭代过程
}
// 省略expression,这样就应该让条件部分或者循环体必须改变迭代变量的值
vector<int> v;
for (int i; cin >> i; /*表达式为空*/)
v.push_back(i); // 这个实现等价于前面讲整数读入vector的while循环
4.3 范围for语句
范围for语句可以用来遍历容器或其他序列的所有元素,其语法形式是:
for (declaration: expression)
statement
expression表示的必须是一个序列,比如用花括号括起来的初始值列表,数组,vector或string等类型的对象,这些类型的共同点是拥有能返回迭代器的begin和end成员。
declaration定义一个变量,序列中的每个元素都能转换为该变量的类型,确保类型相容最简单的办法是使用auto类型说明符,令编译器帮我们指定合适的类型。如果要对序列元素执行写操作,循环变量应该声明为引用类型。
vector<int> v = {0,1,2,3,4,5,6,7,8,9};
for (auto &r:v )
r *= 2; // 将v中的每个元素的值翻倍
// 与之等价的传统for循环
for (auto beg = v.begin(), end = v.end(); beg != end; ++beg) {
auto &r = *beg; // r必须是引用类型
r *= 2; // 将v中每个元素值翻倍
}
4.4 do while语句
do while语句与while语句相似,唯一的区别只在于,do while语句先执行循环体后检查条件,不管条件如何,我们至少执行一次循环。
do
statement
while (condition);
// 不断提示用户输入一对数,然后求和
string rsp; // 作为循环条件,不能定义在do内部
do {
cout << "please enter 2 values: ";
int val1 = 0, val2 = 0;
cin >> val1 >> val2;
cout << "The sum of " << val1 << " and " << val2
<< " = " << val1 + val2 << "\n\n"
<< "More?Enter yes or no: ";
cin >> rsp;
} while (!rsp.empty() && rsp[0] != 'n');
5. 跳转语句
跳转语句中断当前的执行过程。C++语言提供了4种跳转语句:break,continue,goto和return。
5.1 break语句
break语句负责终止离它最近的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; // 离开for循环
// ...
}
// break #1 将控制权转移到这里
// 剩余的‘-’处理:
break; // #2,离开switch语句
case '+':
// ...
} // 结束switch:break #2将控制权转移到这里
} // 结束while
5.2 continue语句
continue语句终止最近的循环中的当前迭代并立即开始下一次迭代,continue语句只能出现在for,while和do while循环的内部,或者嵌套在此类循环里的语句或块的内部。和break语句不同的是,只有当switch语句嵌套在迭代语句内部时,才能在switch里使用continue。
string buf;
while (cin >> buf && !buf.empty()) {
if (buf[0] != '_')
continue; // 接着读取下一个输入
// 程序执行到这里,说明当前的输入是以下划线开始的
}
5.3 goto语句
goto语句的作用是从goto语句无条件跳转到同一函数的一条语句。
不要在程序中使用goto语句,因为它使得程序即难理解又难修改。
6. try语句块和异常处理
异常是指存在于运行时的反常行为,这些行为超出了函数正常功能范围。处理反常行为可能是设计所有系统最难的一部分。
异常处理机制为程序中异常检测和异常处理这两部分的协作提供支持。在C++语言中,异常处理包括:
- throw表达式(throw expression),异常检测部分使用throw表达式来表示它遇到了无法处理的问题,我们说throw引发了异常。
- try语句块(try block),异常处理部分使用try语句块处理异常,try语句块以关键字try开始,并以一个或多个catch子句结束。try语句块中代码抛出的异常通常会被某一个catch子句处理。
- 一套异常类(exception class),用于在throw表达式和相关的catch子句之间传递异常的具体信息。
throw表达式
程序的异常检测部分用throw表达式引发一个异常,throw表达式包含关键字throw和紧随其后的一个表达式,其中表达式的类型就是抛出的异常类型。
Sales_item item1, item2;
cin >> item1 >> item2;
if (item1.isbn() != item2.isbn())
throw runtime_error("Data must refer to smae ISBN");
cout << item1 + item2 << endl;
类型runtime_error是标准库异常类型的一种,定义在stdexcept头文件中。我们必须初始化runtime_error的对象,方式是给它提供一个string对象或一个C风格的字符串,这个字符串中有一些关于异常的辅助信息。
try语句块
try语句块的通用语法是
try {
program-statements
} catch (exception-declaration) {
handle-statements
} catch (exception-declaration) {
handle-statements
} // ...
// 不同书籍的Sales_items相加
while (cin >> item1 >> item2) {
try {
// 如果添加失败,代码抛出一个runtime_error异常
} catch(runtime_error err) {
cout << err.what()
<< "\nTry Again? Enter y or n" << endl;
char c;
cin >> c;
if (!cin || c=='n')
break;
}
}
标准异常
C++标准库定义了一组类,用于报告标准库函数遇到的问题,这些异常也可以在用户编写的程序中使用,它们分别定义在4个头文件中:
- exception头文件定义了最通用的异常类exception,它只报告异常的发生,不提供任何额外信息。
- stdexcept头文件中定义了几种常用的异常类。
- new头文件中定义了bad_alloc异常类型。
- type_info头文件定义了bad_cast异常类型。