《C++ Primer 5th》笔记(5 / 19):语句 墨蓝 2023-01-18 04:28 128阅读 0赞 ### 文章目录 ### * * 简单语句 * * 空语句 * 别漏写分号,也别多写分号 * 复合语句(块) * 语句作用域 * 条件语句 * * if语句 * * 使用if else语句 * 嵌套if语句 * 注意使用花括号 * 悬垂else * 使用花括号控制执行路径 * switch语句 * * switch内部的控制流 * 漏写break容易引发缺陷 * default标签 * switch内部的变量定义 * 迭代语句 * * while语句 * * 使用while循环 * 传统for语句 * * 传统for循环的执行流程 * for语句头中的多重定义 * 省略for语句头的某些部分 * 范围for语句 * do while语句 * 跳转语句 * * break语句 * continue语句 * goto语句 * try语句块和异常处理 * * throw表达式 * try语句块 * * 编写处理代码 * 函数在寻找处理代码的过程中退出 * 提示:编写异常安全的代码非常困难 * 标准异常 通常情况下,语句是顺序执行的。但除非是最简单的程序,否则仅有顺序执行远远不够。因此,C++语言提供了一组**控制流**(flow-of-control)语句以支持更复杂的执行路径。 ## 简单语句 ## C++语言中的大多数语句都以分号结束,一个表达式,比如ival + 5,末尾加上分号就变成了**表达式语句**(expression statement)。表达式语句的作用是执行表达式并丢弃掉求值结果: ival + 5;//一条没什么实际用处的表达式语句 cout<< ival;//一条有用的表达式语句 第一条语句没什么用处,因为虽然执行了加法,但是相加的结果没被使用。比较普遍的情况是,表达式语句中的表达式在求值时附带有其他效果,比如给变量赋了新值或者输出了结果。 ### 空语句 ### 最简单的语句是**空语句**(null statement),空语句中只含有一个单独的分号: ; //空语句 如果在程序的某个地方,语法上需要一条语句但是逻辑上不需要,此时应该使用空语句。一种常见的情况是,当循环的全部工作在条件部分就可以完成时,我们通常会用到空语句。 例如,我们想读取输入流的内容直到遇到一个特定的值为止,除此之外什么事情也不做: //重复读入数据直至到达文件末尾或某次输入的值等于sought while (cin >> s && s != sought) ;//空语句 **Best Practices:使用空语句时应该加上注释,从而令读这段代码的人知道该语句是有意省略的。** ### 别漏写分号,也别多写分号 ### 因为空语句是一条语句,所以可用在任何允许使用语句的地方。由于这个原因,某些看起来非法的分号往往只不过是一条空语句而已,从语法上说得过去。下面的片段包含两条语句:表达式语句和空语句。 ival = vl + v2; ; //正确:第二个分号表示一条多余的空语句 多余的空语句一般来说是无害的,但是如果在if或者while的条件后面跟了一个额外的分号就可能完全改变程序员的初衷。 (多余分号情景)例如,下面的代码将无休止地循环下去: //出现了糟糕的情况:额外的分号,循环体是那条空语句 while (iter != svec.end()) ;// while循环体是那条空语句 ++iter;//递增运算不属于循环的一部分 虽然从形式上来看执行递增运算的语句前面有缩进,但它并不是循环的一部分。循环条件后面跟着的分号构成了一条空语句,它才是真正的循环体。 **WARNING:多余的空语句并非总是无害的。** ### 复合语句(块) ### **复合语句**(compound statement)是指用花括号括起来的(可能为空的)语句和声明的序列,复合语句也被称作**块**(block)。**一个块就是一个作用域**,在块中引入的名字只能在块内部以及嵌套在块中的子块里访问。通常,名字在有限的区域内可见,该区域从名字定义处开始,到名字所在的(最内层)块的结尾为止。 如果在程序的某个地方,语法上需要一条语句,但是逻辑上需要多条语句,则应该使用复合语句。例如,while或者for的循环体必须是一条语句,但是我们常常需要在循环体内做很多事情,此时就需要将多条语句用花括号括起来,从而把语句序列转变成块。 举个例子,一个while循环: while (val<= 10){ sum += val;//把sum + val的值赋给sum。 ++val;//给val加1 } 程序从逻辑上来说要执行两条语句,但是 while循环只能容纳一条。此时,把要执行的语句用花括号括起来,就将其转换成了一条(复合)语句。 **块不以分号作为结束。** 所谓空块,是指内部没有任何语句的一对花括号。空块的作用等价于空语句: while (cin >> s &&s != sought) { }//空块 ## 语句作用域 ## 可以在if、switch、while和 for语句的控制结构内定义变量。定义在控制结构当中的变量只在相应语句的内部可见,一旦语句结束,变量也就超出其作用范围了: while (int i = get_num()) // i is created and initialized on each iteration cout << i << endl; i = 0; // error: i is not accessible outside the loop 如果其他代码也需要访问控制变量,则变量必须定义在语句的外部: // find the first negative element auto beg = v.begin(); while (beg != v.end() && *beg >= 0) ++beg; if (beg == v.end()) // we know that all elements in v are greater than or equal to zero 因为控制结构定义的对象的值马上要由结构本身使用,所以这些变量必须初始化。 (Note:最后还是用花括号括住相关语句吧。) ## 条件语句 ## C++语言提供了两种按条件执行的语句: * 一种是 if语句,它根据条件决定控制流; * 另外一种是switch语句,它计算一个整型表达式的值,然后根据这个值从几条执行路径中选择一条。 ### if语句 ### **if语句**(if statement)的作用是:判断一个指定的条件是否为真,根据判断结果决定是否执行另外一条语句。if语句包括两种形式:一种含有else分支,另外一种没有。简单if语句的语法形式是 if (condition) statement **if else语句**的形式是 if(condition) statement else statement2 在这两个版本的if语句中,condition都必须用圆括号包围起来。condition可以是一个表达式,也可以是一个初始化了的变量声明。不管是表达式还是变量,其类型都必须能转换成布尔类型。通常情况下,statement和statement2是块语句。 如果condition为真,执行statement。当statement执行完成后,程序继续执行if语句后面的其他语句。 如果condition为假,跳过statement。对于简单if语句来说,程序继续执行if语句后面的其他语句;对于if else语句来说,执行statement2。 #### 使用if else语句 #### 我们举个例子来说明if语句的功能,程序的目的是把数字形式表示的成绩转换成字母形式。 假设数字成绩的范围是从0到100(包括100在内),其中 100分对应的字母形式是“A++”,低于60 分的成绩对应的字母形式是“F”。其他成绩每10个划分成一组: 60到69(包括69在内)对应字母“D”、70到79对应字母“C”,以此类推。使用vector对象存放字母成绩所有可能的取值: // if grade is less than 60 it's an F, otherwise compute a subscript string lettergrade; if (grade < 60) lettergrade = scores[0]; else lettergrade = scores[(grade - 50)/10]; #### 嵌套if语句 #### 接下来让我们的程序更有趣点儿,试着给那些合格的成绩后面添加一个加号或减号。如果成绩的末位是8或者9,添加一个加号;如果末位是0、1或2,添加一个减号: // if failing grade, no need to check for a plus or minus if (grade < 60) lettergrade = scores[0]; else { lettergrade = scores[(grade - 50)/10]; // fetch the letter grade if (grade != 100) // add plus or minus only if not already an A++ //关键 if (grade % 10 > 7) lettergrade += '+'; // grades ending in 8 or 9 get a + else if (grade % 10 < 3) lettergrade += '-'; // grades ending in 0, 1, or 2 get a - } #### 注意使用花括号 #### 有一种常见的错误:本来程序中有几条语句应该作为一个块来执行,但是我们忘了用花括号把这些语句包围。在下面的例子中,添加加号减号的代码将被无条件地执行,这显然违背了我们的初衷: if (grade < 60) lettergrade = scores[0]; else // WRONG: missing curly lettergrade = scores[(grade - 50)/10]; // despite appearances, without the curly brace, this code is always executed // failing grades will incorrectly get a - or a + if (grade != 100) if (grade % 10 > 7) lettergrade += '+'; // grades ending in 8 or 9 get a + else if (grade % 10 < 3) lettergrade += '-'; // grades ending in 0, 1, or 2 get a - 要想发现这个错误可能非常困难,毕竟这段代码“看起来”是正确的。 为了避免此类问题,有些编码风格要求在if或else之后**必须写上花括号**(对while和for语句的循环体两端也有同样的要求)。这么做的好处是可以避免代码混乱不清,以后修改代码时如果想添加别的语句,也可以很容易地找到正确位置。 #### 悬垂else #### 当一个if语句嵌套在另一个if语句内部时,很可能if分支会多于else分支。事实上,之前那个成绩转换的程序就有4个if分支,而只有2个else分支。这时候问题出现了:我们怎么知道某个给定的else是和哪个if 匹配呢? 这个问题通常称作**悬垂else** (dangling else),在那些既有if语句又有if else语句的编程语言中是个普遍存在的问题。不同语言解决该问题的思路也不同,就C++而言,它规定else 与离它最近的尚未匹配的if 匹配,从而消除了程序的二义性。 当代码中if分支多于else分支时,程序员有时会感觉比较麻烦。举个例子来说明,对于添加加号减号的那个最内层的if else语句,我们用另外一组条件改写它: //错误:实际的执行过程并非像缩进格式显示的那样;else分支匹配的是内层if语句 if (grade % 10 >= 3) if (grade % 10 >7) lettergrade += '+' ;//末尾是8或者9的成绩添加一个加号 else lettergrade += '-';//末尾是3、4、5、6或者7的成绩添加一个减号! 从代码的缩进格式来看,程序的初衷应该是希望else和外层的if 匹配,也就是说,我们希望当grade 的末位小于3时执行else分支。然而,不管我们是什么意图,也不管程序如何缩进,这里的else分支其实是内层if 语句的一部分。最终,上面的代码将在末位大于3小于等于7的成绩后面添加减号!它的执行过程实际上等价于如下形式: //缩进格式与执行过程相符,但不是程序员的意图 if(grade % 10 >= 3) if (grade % 10 >7) lettergrade += '+';//末尾是8或者9的成绩添加一个加号 else lettergrade += '-';//末尾是3、4、5、6或者7的成绩添加一个减号! (Note:花括号的重要性) #### 使用花括号控制执行路径 #### (Note:解决”悬垂else“问题,用花括号) 要想使else分支和外层的if语句匹配起来,可以在内层if语句的两端加上花括号,使其成为一个块: //末尾是8或者9的成绩添加一个加号,末尾是0、1或者2的成绩添加一个减号 if (grade % 10 >= 3){ if (grade % 10 > 7) lettergrade += '+';//末尾是8或者9的成绩添加一个加号 }else //花括号强迫else与外层if匹配 lettergrade += '-';//末尾是0、1或者2的成绩添加一个减号 语句属于块,意味着语句一定在块的边界之内,因此内层if语句在关键字else前面的那个花括号处已经结束了。else不会再作为内层if 的一部分。此时,最近的尚未匹配的if是外层if,也就是我们希望else 匹配的那个。 ### switch语句 ### switch语句(switch statement)提供了一条便利的途径使得我们能够在若干固定选项中做出选择。 举个例子,假如我们想统计五个元音字母在文本中出现的次数,程序逻辑应该如下所示: * 从输入的内容中读取所有字符。 * 令每一个字符都与元音字母的集合比较。 * 如果字符与某个元音字母匹配,将该字母的数量加1。 * 显示结果。 例如,以(原书中)本章的文本作为输入内容,程序的输出结果将是: Number of vowel a: 3195 Number of vowel e: 6230 Number of vowel i: 3102 Number of vowel o: 3289 Number of vowel u: 1033 要想实现这项功能,直接使用switch语句即可: // initialize counters for each vowel unsigned aCnt = 0, eCnt = 0, iCnt = 0, oCnt = 0, uCnt = 0; char ch; while (cin >> ch) { // if ch is a vowel, increment the appropriate counter switch (ch) { case 'a': ++aCnt; break; case 'e': ++eCnt; break; case 'i': ++iCnt; break; case 'o': ++oCnt; break; case 'u': ++uCnt; break; } } // print results 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 << endl; switch语句首先对括号里的表达式求值,该表达式紧跟在关键字switch的后面,可以是一个初始化的变量声明。表达式的值转换成整数类型,然后与每个case标签的值比较。 如果表达式和某个case标签的值匹配成功,程序从该标签之后的第一条语句开始执行,直到到达switch的结尾或者是遇到一条break语句为止。 break语句的作用是中断当前的控制流。此例中,break语句将控制权转移到switch语句外面。因为switch是while循环体内唯一的语句,所以从switch语句中断出来以后,程序的控制权将移到while语句的右花括号处。此时while语句内部没有其他语句要执行,所以 while会返回去再一次判断条件是否满足。 如果switch语句的表达式和所有case都没有匹配上,将直接跳转到switch结构之后的第一条语句。刚刚说过,在上面的例子中,退出switch 后控制权回到while语句的条件部分。 case关键字和它对应的值一起被称为case标签(case label)。**case标签必须是整型常量表达式**: char ch = getval ( ) ; int ival =42; switch(ch) { case 3.14: //错误:case标签不是一个整数 case ival: //错误:case标签不是一个常量 // ... 任何两个 case标签的值不能相同,否则就会引发错误。另外,default也是一种特殊的case标签,等下介绍。 #### switch内部的控制流 #### 理解程序在case标签之间的执行流程非常重要。如果某个case标签匹配成功,将从该标签开始往后顺序执行所有case分支,除非程序显式地中断了这一过程,否则直到switch的结尾处才会停下来。要想避免执行后续case分支的代码,我们必须显式地告诉编译器终止执行过程。大多数情况下,在下一个 case标签之前应该有一条 break语句。 然而,也有一些时候默认的switch行为才是程序真正需要的。每个case标签只能对应一个值,但是有时候我们希望两个或更多个值共享同一组操作。此时,我们就故意省略掉break语句,使得程序能够连续执行若干个case标签。 例如,也许我们想统计的是所有元音字母出现的总次数: unsigned vowelCnt = 0; // ... switch (ch) { // any occurrence of a, e, i, o, or u increments vowelCnt case 'a': case 'e': case 'i': case 'o': case 'u': ++vowelCnt; break; } 在上面的代码中,几个case标签连写在一起,中间没有break语句。因此只要ch是元音字母,不管到底是五个中的哪一个都执行相同的代码。 C++程序的形式比较自由,所以case标签之后不一定非得换行。把几个case标签写在一行里,强调这些case代表的是某个范围内的值: switch (ch) { // alternative legal syntax case 'a': case 'e': case 'i': case 'o': case 'u': ++vowelCnt; break; } **Best Practise:一般不要省略case分支最后的break语句。如果没写break语句,最好加一段注释说清楚程序的逻辑。** #### 漏写break容易引发缺陷 #### 有一种常见的错觉是程序只执行匹配成功的那个case分支的语句。例如,下面程序的统计结果是错误的: // warning: deliberately incorrect! switch (ch) { case 'a': ++aCnt; // oops: should have a break statement case 'e': ++eCnt; // oops: should have a break statement case 'i': ++iCnt; // oops: should have a break statement case 'o': ++oCnt; // oops: should have a break statement case 'u': ++uCnt; } 要想理解这段程序的执行过程,不妨假设ch的值是’ e’。此时,程序直接执行case 'e’标签后面的代码,该代码把ecnt的值加1。接下来,程序将跨越case标签的边界,接着递增iCnt、oCnt和 uCnt。 **Best Practise:尽管switch语句不是非得在最后一个标签后面写上 break,但是为了安全起见,最好这么做。因为这样的话,即使以后再增加新的case分支,也不用再在前面补充break语句了。** #### default标签 #### 如果没有任何一个 case标签能匹配上 switch 表达式的值,程序将执行紧跟在**default标签**(default label)后面的语句。例如,可以增加一个计数值来统计非元音字母的数量,只要在 default分支内不断递增名为otherCnt的变量就可以了: // if ch is a vowel, increment the appropriate counter switch (ch) { case 'a': case 'e': case 'i': case 'o': case 'u': ++vowelCnt; break; default: ++otherCnt; break; } 在这个版本的程序中,如果 ch 不是元音字母,就从 default标签开始执行并把otherCnt加 1。 **Best Practises:即使不准备在default标签下做任何工作,定义一个default标签也是有用的。其目的在于告诉程序的读者,我们已经考虑到了默认的情况,只是目前什么也没做。** 标签不应该孤零零地出现,它后面必须跟上一条语句或者另外一个case标签。如果switch 结构以一个空的default标签作为结束,则该default标签后面必须跟上一条空语句或一个空块。 #### switch内部的变量定义 #### 如前所述,switch 的执行流程有可能会跨过某些case标签。如果程序跳转到了某个特定的case,则switch 结构中该case标签之前的部分会被忽略掉。这种忽略掉一部分代码的行为引出了一个有趣的问题:**如果被略过的代码中含有变量的定义该怎么办?** 答案是:如果在某处一个带有初值的变量位于作用域之外,在另一处该变量位于作用域之内,则从前一处跳转到后一处的行为是非法行为。 case true: // this switch statement is illegal because these initializations might be bypassed string file_name; // error: control bypasses an implicitly initialized variable int ival = 0; // error: control bypasses an explicitly initialized variable int jval; // ok: because jval is not initialized break; case false: // ok: jval is in scope but is uninitialized jval = next_num(); // ok: assign a value to jval if (file_name.empty()) // file_name is in scope but wasn't initialized // ... 假设上述代码合法,则一旦控制流直接跳到false分支,也就同时略过了变量file\_name和 ival的初始化过程。此时这两个变量位于作用域之内,跟在false之后的代码试图在尚未初始化的情况下使用它们,这显然是行不通的。因此C++语言规定,**不允许跨过变量的初始化语句直接跳转到该变量作用域内的另一个位置**。 如果需要为某个case分支定义并初始化一个变量,我们应该把变量定义在块内,从而确保后面的所有case标签都在变量的作用域之外。 case true: { // ok: declaration statement within a statement block string file_name = get_file_name(); // ... } break; case false: if (file_name.empty()) // error: file_name is not in scope ## 迭代语句 ## 迭代语句通常称为循环,它重复执行操作直到满足某个条件才停下来。while和for语句在执行循环体之前检查条件,do while语句先执行循环体,然后再检查条件。 ### while语句 ### 只要条件为真,**while语句**(while statement)就重复地执行循环体,它的语法形式是: while (condition) statement 在while结构中,只要condition 的求值结果为真就一直执行statement(常常是一个块)。condition不能为空,如果condition第一次求值就得false,statement一次都不执行。 while的条件部分可以是一个表达式或者是一个带初始化的变量声明。通常来说,应该由条件本身或者是循环体设法改变表达式的值,否则循环可能无法终止。 **Note:定义在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 >= o) ++beg; if(beg == v.end ()) //此时我们知道v中的所有元素都大于等于0 第一个循环从标准输入中读取数据,我们**一开始不清楚循环要执行多少次**,当cin读取到无效数据、遇到其他一些输入错误或是到达文件末尾时循环条件失效。 第二个循环重复执行直到遇到一个负值为止,循环终止后,beg或者等于v.end(),或者指向v中一个小于0的元素。可以在while循环外继续使用beg的状态以进行其他处理。 ### 传统for语句 ### for语句的语法形式是 for (init-statemen; condition; expression) statement 关键字for及括号里的部分称作for语句头。 init-statement必须是以下三种形式中的一种:声明语句、表达式语句或者空语句,因为这些语句都以分号作为结束,所以for语句的语法形式也可以看做 for (initializer; condition; expression) statement 一般情况下,init-statement负责初始化一个值,这个值将随着循环的进行而改变。condition作为循环控制的条件,只要condition为真,就执行一次 statement。如果condition第一次的求值结果就是 false,则statement一次也不会执行。expression负责修改init-statement初始化的变量,这个变量正好就是condition检查的对象,修改发生在每次循环迭代之后。statement可以是一条单独的语句也可以是一条复合语句。 #### 传统for循环的执行流程 #### 一个的for循环为例: //重复处理s中的字符直至我们处理完全部字符或者遇到了一个表示空白的字符 for (decltype(s.size()) index = 0; index != s.size() && !isspace(s[index] ); ++index) s[index] = toupper(s[index]) ;//将当前字符改成大写形式 求值的顺序如下所示: 1. 循环开始时,首先执行一次init-statement。此例中,定义index并初始化为0。 2. 接下来判断condition。如果index不等于s.size()而且在s\[index\]位置的 字符不是空白,则执行for循环体的内容。否则,循环终止。如果第一次迭代时条件就为假,for循环体一次也不会执行。 3. 如果条件为真,执行循环体。此例中,for循环体将s\[index\]位置的字符改写大写形式。 4. 最后执行expression。此例中,将index的值加1。 这4步说明了for循环第一次迭代的过程。其中第1步只在循环开始时执行一次,第2、3、4步重复执行直到条件为假时终止,也就是在s中遇到一个空白字符或者index大于s.size ()时终止。 Note:牢记for语句头中定义的对象只在 for循环体内可见。因此在上面的例子中,for循环结束后index就不可用了。 #### for语句头中的多重定义 #### 和其他的声明一样,init-statement 也可以定义多个对象。但是init-statement只能有一条声明语句,因此,所有变量的基础类型必须相同。举个例子,我们用下面的循环把vector的元素拷贝一份添加到原来的元素后面: //记录下v的大小,当到达原来的最后一个元素后结束循环 for (decltype (v.size()) i = 0,sz = v.size() ; i != sz; ++i) v.push_back (v[i]); 在这个循环中,我们在init-statement里同时定义了索引i和循环控制变量sz。 #### 省略for语句头的某些部分 #### for语句头能省略掉init-statement、condition和 expression中的任何一个(或者全部)。 **省略init-statement** 如果无须初始化,则我们可以使用一条空语句作为init-statement。例如,对于在vector对象中寻找第一个负数的程序,完全能用for循环改写: auto beg = v.begin (); for (/*空语句*/ ; beg != v.end() && *beg >= 0; ++beg) ; //什么也不做 注意,分号必须保留以表明我们省略掉了init-statement。说得更准确一点,分号表示的是一个空的init-statement。在这个循环中,因为所有要做的工作都在for语句头的条件和表达式部分完成了,所以 for 循环体也是空的。其中,条件部分决定何时停止查找,表达式部分递增迭代器。 **省略condition** 省略condition的效果等价于在条件部分写了一个true。因为条件的值永远是true,所以在循环体内必须有语句负责退出循环,否则循环就会无休止地执行下去: for (int i = 0; /*条件为空*/; ++i){ //对i进行处理,循环内部的代码必须负责终止迭代过程! } **省略expression** 我们也能省略掉for语句头中的expression,但是在这样的循环中就要求条件部分或者循环体必须改变迭代变量的值。举个例子,之前有一个将整数读vector的while循环,我们使用for语句改写它: vector<int> v; for (int i; cin >> i; /*表达式为空*/) v.push_back (i); 因为条件部分能改变i的值,所以这个循环无须表达式部分。其中,条件部分不断检查输入流的内容,只要读取完所有的输入或者遇到一个输入错误就终止循环。 ### 范围for语句 ### C++11 新标准引入了一种更简单的for 语句,这种语句可以遍历容器或其他序列的所有元素。范围for语句(range for statement)的语法形式是: for (declaration : expression) statement **expression**表示的必须是一个序列,比如: * 用花括号括起来的初始值列表 * 数组 * vector或string等类型的对象 这些类型的共同特点是拥有能返回迭代器的begin和 end成员。 **declaration**定义一个变量,序列中的每个元素都得能转换成该变量的类型。确保类型相容最简单的办法是使用auto类型说明符,这个关键字可以令编译器帮助我们指定合适的类型。如果需要对序列中的元素执行写操作,循环变量必须声明成引用类型。 每次迭代都会重新定义循环控制变量,并将其初始化成序列中的下一个值,之后才会执行**statement**。像往常一样,statement可以是一条单独的语句也可以是一个块。所有元素都处理完毕后循环终止。 之前我们已经接触过几个这样的循环。接下来的例子将把vector对象中的每个元素都翻倍,它涵盖了范围for语句的几乎所有语法特征: vector<int> v = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; //范围变量必须是引用类型,这样才能对元素执行写操作 for (auto &r : v) //对于v中的每一个元素 r *= 2; //将v中每个元素的值翻倍 for语句头声明了循环控制变量r,并把它和v关联在一起,我们使用关键字auto令编译器为r指定正确的类型。**由于准备修改v的元素的值,因此将r声明成引用类型**。(MyNote:r声明成引用类型,r成为元素的别名,否则改不了vector元素原值)此时,在循环体内给r赋值,即改变了r所绑定的元素的值。 范围for语句的定义来源于与之等价的传统for语句: for (auto beg = v.begin(), end = v.end(); beg != end; ++beg){ auto &r = *beg; // r必须是引用类型,这样才能对元素执行写操作 r*= 2;//将v中每个元素的值翻倍 } 学习了范围for语句的原理之后,我们也就不难理解为什么强调**不能通过范围for语句增加vector对象(或者其他容器)的元素**了。 在范围for语句中,预存了end()的值。一旦在序列中添加(删除)元素,end函数的值就可能变得无效了。在第九章,会有更详细的介绍。 ### do while语句 ### do while语句(do while statement)和 while语句非常相似,唯一的区别是,do while语句先执行循环体后检查条件。不管条件的值如何,我们都至少执行一次循环。do while语句的语法形式如下所示: do statement while (condition); Note:do while语句应该在括号包围起来的条件后面用一个分号表示语句结束。 在 do语句中,求condition的值之前首先执行一次 statement,condition不能为空。如果condition 的值为假,循环终止;否则,重复循环过程。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') ; 循环首先提示用户输入两个数字,然后输出它们的和并询问用户是否继续。条件部分检查用户做出的回答,如果用户没有回答,或者用户的回答以字母n开始,循环都将终止。否则循环继续执行。 因为对于do while来说先执行语句或者块,后判断条件,所以不允许在条件部分定义变量: do { // . . . mumble(foo); } while (int foo = get_foo()); // error: declaration in a do condition 如果允许在条件部分定义变量,则变量的使用出现在定义之前,这显然是不合常理的! ## 跳转语句 ## 跳转语句中断当前的执行过程。C++语言提供了4种跳转语句:break、continue、goto和return。return语句将在第六章进行介绍。 ### break语句 ### break语句(break statement)负责终止离它最近的while、do while、for或switch语句,并从这些语句之后的第一条语句开始继续执行。 break 语句只能出现在迭代语句或者switch语句内部(包括嵌套在此类循环里的语句或块的内部)。break 语句的作用范围仅限于最近的循环或者switch: string buf; while (cin >> buf && !buf.empty()) { switch(buf[0]) { case '-': // process up to the first blank for (auto it = buf.begin()+1; it != buf.end(); ++it) { if (*it == ' ') break; // #1, leaves the for loop // . . . }// break #1 transfers control here // remaining '-' processing: break; // #2, leaves the switch statement case '+': // . . . }// end switch // end of switch: break #2 transfers control here } // end while 标记为\#1的break语句负责终止连字符case标签后面的for循环。它不但不会终止switch语句,甚至连当前的case分支也终止不了。接下来,程序继续执行for循环之后的第一条语句,这条语句可能接着处理连字符的情况,也可能是另一条用于终止当前分支的break语句。 标记为\#2的 break 语句负责终止switch语句,但是不能终止 while循环。执行完这个break 后,程序继续执行while的条件部分。 ### continue语句 ### continue语句(continue statement)终止最近的循环中的当前迭代并立即开始下一次迭代。continue语句只能出现在for、while和 do while循环的内部,或者嵌套在此类循环里的语句或块的内部。和 break 语句类似的是,出现在嵌套循环中的continue语句也仅作用于离它最近的循环。和 break语句不同的是,只有当switch语句嵌套在迭代语句内部时,才能在switch里使用continue。 continue语句中断当前的迭代,但是仍然继续执行循环。对于while或者do while语句来说,继续判断条件的值;对于传统的 for 循环来说,继续执行 for语句头的expression;而对于范围for语句来说,则是用序列中的下一个元素初始化循环控制变量。 例如,下面的程序每次从标准输入中读取一个单词。循环只对那些以下画线开头的单词感兴趣,其他情况下,我们直接终止当前的迭代并获取下一个单词: string buf; while (cin >> buf && !buf.empty()) { if (buf[0] != '_') continue; // get another input // still here? the input starts with an underscore; process buf . . . } ### goto语句 ### goto语句(goto statement)的作用是从goto语句无条件跳转到同一函数内的另一条语句。 **Best Practices:不要在程序中使用 goto语句,因为它使得程序既难理解又难修改。** goto语句的语法形式是 goto label; 其中,label是用于标识一条语句的标示符。带标签语句(labeled statement)是一种特殊的语句,在它之前有一个标示符以及一个冒号: end: return; //带标签语句,可以作为goto的目标 标签标示符独立于变量或其他标示符的名字,因此,标签标示符可以和程序中其他实体的标示符使用同一个名字而不会相互干扰。goto 语句和控制权转向的那条带标签的语句必须位于同一个函数之内。 和switch 语句类似,goto语句也不能将程序的控制权从变量的作用域之外转移到作用域之内: //... goto end ; int ix = 10;//错误:goto语句绕过了一个带初始化的变量定义 end: //错误:此处的代码需要使用ix,但是goto语句绕过了它的声明 ix =42; 向后跳过一个已经执行的定义是合法的。跳回到变量定义之前意味着系统将销毁该变量,然后重新创建它: //向后跳过一个带初始化的变量定义是合法的 begin: int sz = get_size(); if(sz <=0){ goto begin; } 在上面的代码中,goto语句执行后将销毁sz。因为跳回到 begin 的动作跨过了sz的定义语句,所以sz将重新定义并初始化。 My Note:Java的for循环前加个标签以break出多重循环,在C++中不管用。 ## try语句块和异常处理 ## My Note:跟Java的类似。 异常是指存在于运行时的反常行为,这些行为超出了函数正常功能的范围。典型的异常包括失去数据库连接以及遇到意外输入等。处理反常行为可能是设计所有系统最难的一部分。 当程序的某部分检测到一个它无法处理的问题时,需要用到异常处理。此时,检测出问题的部分应该发出某种信号以表明程序遇到了故障,无法继续下去了,而且信号的发出方无须知道故障将在何处得到解决。一旦发出异常信号,检测出问题的部分也就完成了任务。 如果程序中含有可能引发异常的代码,那么通常也会有专门的代码处理问题。例如,如果程序的问题是输入无效,则异常处理部分可能会要求用户重新输入正确的数据;如果丢失了数据库连接,会发出报警信息。 异常处理机制为程序中异常检测和异常处理这两部分的协作提供支持。在CH+语言中,异常处理包括: * **throw表达式**(throw expression),异常检测部分使用throw表达式来表示它遇到了无法处理的问题。我们说throw**引发**(raise)了异常。 * **try语句块**(try block),异常处理部分使用try语句块处理异常。try语句块以关键字try开始,并以一个或多个**catch子句**(catch clause)结束。try语句块中代码抛出的异常通常会被某个catch子句处理。因为 catch子句“处理”异常,所以它们也被称作**异常处理代码**(exception handler)。 * 一套**异常类**(exception class),用于在throw表达式和相关的catch子句之间传递异常的具体信息。 在本节的剩余部分,我们将分别介绍异常处理的这三个组成部分。在第18章还将介绍更多关于异常的知识。 ### throw表达式 ### 程序的异常检测部分使用throw表达式引发一个异常。throw表达式包含关键字throw和紧随其后的一个表达式,其中表达式的类型就是抛出的异常类型。throw表达式后面通常紧跟一个分号,从而构成一条表达式语句。 举个简单的例子,第1章把两个sales\_item对象相加的程序。这个程序检查它读入的记录是否是关于同一种书籍的,如果不是,输出一条信息然后退出。 Sales_item item1, item2; cin >> item1 >> item2; // first check that item1 and item2 represent the same book if (item1.isbn() == item2.isbn()) { cout << item1 + item2 << endl; return 0; // indicate success } else { cerr << "Data must refer to same ISBN" << endl; return -1; // indicate failure } 在真实的程序中,应该把对象相加的代码和用户交互的代码分离开来。此例中,我们改写程序使得检查完成后不再直接输出一条信息,而是抛出一个异常: // first check that the data are for the same item if (item1.isbn() != item2.isbn()) throw runtime_error("Data must refer to same ISBN"); // if we're still here, the ISBNs are the same cout << item1 + item2 << endl; 在这段代码中,如果ISBN不一样就抛出一个异常,该异常是类型runtime\_error的对象。抛出异常将终止当前的函数,并把控制权转移给能处理该异常的代码。 类型runtime\_error是标准库异常类型的一种,**定义在stdexcept头文件中**。我们必须初始化runtime\_error的对象,方式是给它提供一个string对象或者一个C风格的字符串,这个字符串中有一些关于异常的辅助信息。 ### try语句块 ### try语句块的通用语法形式是 try { program-statements } catch (exception-declaration) { handler-statements } catch (exception-declaration) { handler-statements } // . . . try语句块的一开始是关键字try,随后紧跟着一个块,这个块就像大多数时候那样是花括号括起来的语句序列。 跟在try块之后的是一个或多个catch子句。catch子句包括三部分:关键字catch、括号内一个(可能未命名的)对象的声明(称作**异常声明**,exception declaration)以及一个块。当选中了某个catch子句处理异常之后,执行与之对应的块。catch一旦完成,程序跳转到try语句块最后一个catch子句之后的那条语句继续执行。 try语句块中的program-statements组成程序的正常逻辑,像其他任何块一样,program-statements可以有包括声明在内的任意C++语句。一如往常,try语句块内声明的变量在块外部无法访问,特别是在catch子句内也无法访问。 #### 编写处理代码 #### 在之前的例子里,我们使用了一个 throw表达式以避免把两个代表不同书籍的sales\_item相加。我们假设执行sales\_item对象加法的代码是与用户交互的代码分离开来的。其中与用户交互的代码负责处理发生的异常,它的形式可能如下所示: while (cin >> item1 >> item2) { try { // execute code that will add the two Sales_items // if the addition fails, the code throws a runtime_error exception } catch (runtime_error err) { // remind the user that the ISBNs must match and prompt for another pair cout << err.what() << "\nTry Again? Enter y or n" << endl; char c; cin >> c; if (!cin || c == 'n') break; // break out of the while loop } } 程序本来要执行的任务出现在 try语句块中,这是因为这段代码可能会抛出一个runtime\_error类型的异常。 try语句块对应一个catch子句,该子句负责处理类型为runtime\_error的异常。如果try语句块的代码抛出了runtime\_error异常,接下来执行catch块内的语句。在我们书写的 catch子句中,输出一段提示信息要求用户指定程序是否继续。如果用户输入’n’,执行 break 语句并退出 while循环;否则,直接执行while循环的右侧花括号,意味着程序控制权跳回到while条件部分准备下一次迭代。 给用户的提示信息中输出了err.what()的返回值。我们知道err的类型是runtime\_error,因此能推断what是runtime\_error类的一个成员函数。每个标准库异常类都定义了名为what 的成员函数,这些函数没有参数,返回值是C风格字符串(即 const char\* )。其中,runtime\_error的what成员返回的是初始化一个具体对象时所用的string对象的副本。如果上一节编写的代码抛出异常,则本节的catch子句输出 Data must refer to same ISBN Try Again? Enter y or n #### 函数在寻找处理代码的过程中退出 #### 在复杂系统中,程序在遇到抛出异常的代码前,其执行路径可能已经经过了多个try语句块。例如,一个try语句块可能调用了包含另一个try语句块的函数,新的try语句块可能调用了包含又一个try语句块的新函数,以此类推。 寻找处理代码的过程与函数调用链刚好相反。当异常被抛出时,首先搜索抛出该异常的函数。如果没找到匹配的 catch子句,终止该函数,并在调用该函数的函数中继续寻找。如果还是没有找到匹配的catch子句,这个新的函数也被终止,继续搜索调用它的函数。以此类推,沿着程序的执行路径逐层回退,直到找到适当类型的catch子句为止。 如果最终还是没能找到任何匹配的catch子句,程序转到名为**terminate**的标准库函数。该函数的行为与系统有关,一般情况下,执行该函数将导致程序非正常退出。 对于那些没有任何try语句块定义的异常,也按照类似的方式处理;毕竟,没有try语句块也就意味着没有匹配的catch子句。如果一段程序没有try语句块且发生了异常,系统会调用terminate函数并终止当前程序的执行。 #### 提示:编写异常安全的代码非常困难 #### 要好好理解这句话:异常中断了程序的正常流程。异常发生时,调用者请求的一部分计算可能已经完成了,另一部分则尚未完成。通常情况下,略过部分程序意味着某些对象处理到一半就臻然而止,从而导致对象处于无效或未完成的状态,或者资源没有正常释放等等。 那些在异常发生期间正确执行了“清理”工作的程序被称作异常安全(exception safe)的代码。然而经验表明,编写异常安全的代码非常困难。 * 对于一些程序来说,当异常发生时只是简单地终止程序。此时,我们不怎么需要担 心异常安全的问题。 * 但是对于那些确实要处理异常并继续执行的程序,就要加倍注意了。我们必须时刻清楚异常何时发生,异常发生后程序应如何确保对象有效、资源无泄漏、程序处于合理状态,等等。 未来会介绍一些比较常规的提升异常安全性的技术,仅供参考。如果你的程序要求非常鲁棒的异常处理,那么仅有即将介绍的这些技术恐怕还是不够的。 ### 标准异常 ### C++标准库定义了一组类,用于报告标准库函数遇到的问题。这些异常类也可以在用户编写的程序中使用,它们分别定义在4个头文件中: * exception头文件定义了最通用的异常类exception。它只报告异常的发生,不提供任何额外信息。 * type\_info头文件定义了bad\_cast 异常类型,这种类型将在第19章详细介绍。 * new头文件定义了bad\_alloc异常类型,这种类型将在第12章详细介绍。 * stdexcept头文件定义了几种常用的异常类,下表列出: <table> <thead> <tr> <th>异常类</th> <th>说明</th> </tr> </thead> <tbody> <tr> <td>exception</td> <td>最常见的问题</td> </tr> <tr> <td>runtime_error</td> <td>只有在运行时才能检测出的问题</td> </tr> <tr> <td>range_error</td> <td>运行时错误:生成的结果超出了有意义的值域范围</td> </tr> <tr> <td>overflow_error</td> <td>运行时错误:计算上溢</td> </tr> <tr> <td>underflow_error</td> <td>运行时错误:计算下溢</td> </tr> <tr> <td>logic_error</td> <td>程序逻辑错误</td> </tr> <tr> <td>domain_error</td> <td>逻辑错误:参数对应的结果值不存在</td> </tr> <tr> <td>invalid_argument</td> <td>逻辑错误:无效参数</td> </tr> <tr> <td>length_error</td> <td>逻辑错误:试图创建一个超出该类型最大长度的对象</td> </tr> <tr> <td>out_of_range</td> <td>逻辑错误:使用一个超出有效范围的值</td> </tr> </tbody> </table> 标准库异常类只定义了几种运算,包括创建或拷贝异常类型的对象,以及为异常类型的对象赋值。 * 我们只能以默认初始化的方式初始化 exception、bad\_alloc和 bad\_cast对象,不允许为这些对象提供初始值。 * 其他异常类型的行为则恰好相反:应该使用string 对象或者C风格字符串初始化这些类型的对象,但是不允许使用默认初始化的方式。当创建此类对象时,必须提供初始值,该初始值含有错误相关的信息。 异常类型只定义了一个名为what 的成员函数,该函数没有任何参数,返回值是一个指向C风格字符串的const char\*。该字符串的目的是提供关于异常的一些文本信息。 what函数返回的C风格字符串的内容与异常对象的类型有关。如果异常类型有一个字符串初始值,则what返回该字符串。对于其他无初始值的异常类型来说,what返回的内容由编译器决定。
相关 《C++ Primer 5th》笔记(10 / 19):泛型算法 文章目录 概述 算法如何工作 迭代器令算法不依赖于容器 但算法依赖于元素类型的操作 我会带着你远行/ 2023年01月22日 08:53/ 0 赞/ 14 阅读
相关 《C++ Primer 5th》笔记(9 / 19):顺序容器 文章目录 顺序容器概述 确定使用哪种顺序容器 容器库概览 迭代器 迭代器范围 淩亂°似流年/ 2023年01月21日 02:21/ 0 赞/ 14 阅读
相关 《C++ Primer 5th》笔记(8 / 19):IO库 文章目录 IO类 IO类型间的关系 IO对象无拷贝或赋值 条件状态 素颜马尾好姑娘i/ 2023年01月19日 08:19/ 0 赞/ 20 阅读
相关 《C++ Primer 5th》笔记(7 / 19):类 文章目录 定义抽象数据类型 设计Sales\_data类 关键概念:不同的编程角色 缺乏、安全感/ 2023年01月19日 04:47/ 0 赞/ 20 阅读
相关 《C++ Primer 5th》笔记(5 / 19):语句 文章目录 简单语句 空语句 别漏写分号,也别多写分号 复合语句(块) 语句作用域 墨蓝/ 2023年01月18日 04:28/ 0 赞/ 129 阅读
相关 《C++ Primer 5th》笔记(4 / 19):表达式 文章目录 基础 基本概念 组合运算符和运算对象 运算对象转换 缺乏、安全感/ 2023年01月17日 15:00/ 0 赞/ 157 阅读
相关 《C++ Primer 5th》笔记(1 / 19):C++基础 文章目录 编写一个简单的C++程序 编译、运行程序 初识输入输出 注释简介 控制流 爱被打了一巴掌/ 2023年01月14日 15:56/ 0 赞/ 165 阅读
相关 《C++ Primer 5th》笔记(12 / 19):动态内存 文章目录 动态内存与智能指针 shared\_ptr类 make\_shared函数 痛定思痛。/ 2022年10月16日 07:33/ 0 赞/ 215 阅读
相关 《C++ Primer 5th》笔记(11 / 19):关联容器 文章目录 使用关联容器 使用map 使用set 关联容器概述 定义关联容器 一时失言乱红尘/ 2022年10月14日 05:41/ 0 赞/ 357 阅读
还没有评论,来说两句吧...