《C++ Primer 5th》笔记(5 / 19):语句
文章目錄
- 簡單語句
- 空語句
- 別漏寫分號,也別多寫分號
- 復合語句(塊)
- 語句作用域
- 條件語句
- 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 iterationcout << 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)statementif else語句的形式是
if(condition)statement elsestatement2在這兩個版本的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]; elselettergrade = 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 gradeif (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 curlylettergrade = 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的成績添加一個加號 elselettergrade += '-';//末尾是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的成績添加一個加號elselettergrade += '-';//末尾是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 counterswitch (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 vowelCntcase 'a':case 'e':case 'i':case 'o':case 'u':++vowelCnt;break; }在上面的代碼中,幾個case標簽連寫在一起,中間沒有break語句。因此只要ch是元音字母,不管到底是五個中的哪一個都執行相同的代碼。
C++程序的形式比較自由,所以case標簽之后不一定非得換行。把幾個case標簽寫在一行里,強調這些case代表的是某個范圍內的值:
switch (ch) {// alternative legal syntaxcase '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 statementcase 'e':++eCnt; // oops: should have a break statementcase 'i':++iCnt; // oops: should have a break statementcase 'o':++oCnt; // oops: should have a break statementcase '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 bypassedstring file_name; // error: control bypasses an implicitly initialized variableint ival = 0; // error: control bypasses an explicitly initialized variableint jval; // ok: because jval is not initializedbreak; case false:// ok: jval is in scope but is uninitializedjval = next_num(); // ok: assign a value to jvalif (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 blockstring 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]) ;//將當前字符改成大寫形式求值的順序如下所示:
字符不是空白,則執行for循環體的內容。否則,循環終止。如果第一次迭代時條件就為假,for循環體一次也不會執行。
這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)statementexpression表示的必須是一個序列,比如:
- 用花括號括起來的初始值列表
- 數組
- 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語句的語法形式如下所示:
dostatement 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 blankfor (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 statementcase '+':// . . .}// 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 paircout << 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頭文件定義了幾種常用的異常類,下表列出:
| 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返回的內容由編譯器決定。
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的《C++ Primer 5th》笔记(5 / 19):语句的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Pytorch(5)-梯度反向传播
- 下一篇: 推荐算法--利用用户标签数据(04)