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