《C++ Primer》學習筆記(五):迴圈、分支、跳轉和異常處理語句

我是管小亮發表於2020-01-29

歡迎關注WX公眾號:【程式設計師管小亮】

專欄C++學習筆記

《C++ Primer》學習筆記/習題答案 總目錄

——————————————————————————————————————————————————————

  • 《C++ Primer》習題參考答案:第5章 - 迴圈、分支、跳轉和異常處理語句

?? Cpp-Prime5 + Cpp-Primer-Plus6 原始碼和課後題

迴圈、分支和跳轉語句

1、簡單語句

通常情況下,語句是順序執行的,但除非是最簡單的程式,否則僅有順序執行遠遠不夠。因此, C++語言提供了一組 控制流(flow-of-control) 語旬以支援更復雜的執行路徑。

C++語言中的大多數語句都以分號結束,一個表示式加上 ; 就變成了 表示式語句(expression statement)。如果在程式的某個地方,語法上需要一條語句但是邏輯上不需要,則應該使用 空語句(null statement),空語句中只含有一個單獨的分號 ;

ival + 5; 		// 一條沒什麼實際用處的表示式語句
cout << ival; 	// 一條有用的表示式語句
// 重複讀入資料直至到達檔案末尾或某次輸入的值等於sought
while (cin >> s && s != sought)
    ;   		// 空語句

使用空語句時應該加上註釋,從而令讀這段程式碼的人知道該語句是有意省略的。

不要漏寫分號 ;!!!不要漏寫分號 ;!!!不要漏寫分號 ;!!!重要的事情說三遍,另外,也不要多寫分號,即空語句,多餘的空語句並非總是無害的。

ival = vl + v2;;	 			// 正確: 第二個分號表示一條多餘的空語句
// 出現了糟糕的情況:額外的分號,迴圈體是那條空語句
while (iter != svec.end()) ;    // while迴圈體是那條空語句
    ++iter;     				// 遞增運算不屬於迴圈的一部分

複合語句(compound statement) 是指用花括號括起來的(可能為空)語句和宣告的序列,也叫做塊(block)一個塊就是一個作用域,在塊中引入的名字只能在塊內部以及巢狀在塊中的子塊裡訪問。通常,名字在有限的區域內可見,該區域從名字定義處開始,到名字所在(最內層)塊的結尾處為止。

如果在程式的某個地方,語法上需要一條語句,但是邏輯上需要多條語句,則應該使用複合語句。把要執行的語句用花括號括起來, 就將其轉換成了一條(複合〉語句。

while (val <= 10) {
	sum += val;		// 把sum+val的值賦給sum
	++val;			// 給val加1
}

語句塊不以分號作為結束。

所謂空塊, 是指內部沒有任何語句的一對花括號。空塊的作用等價於空語句:

while (cin >> s && s != sought)
	{ } // 空塊

2、語句作用域

可以在 ifswitchwhilefor 語句的控制結構內定義變數,這些變數只在相應語句的內部可見,一旦語句結束,變數也就超出了其作用範圍。

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、條件語句

1)if語句

if 語句的形式:

if (condition)
    statement

if-else 語句的形式:

if (condition)
    statement
else
    statement2

其中 condition 是判斷條件,可以是一個表示式或者初始化了的變數宣告,condition 必須用圓括號括起來。

  • 如果 condition 為真,則執行 statement。執行完成後,程式繼續執行 if 語句後面的其他語句。
  • 如果 condition 為假,則跳過 statement。對於簡單 if 語句來說,程式直接執行 if 語句後面的其他語句;對於 if-else 語句來說,程式先執行 statement2,再執行 if 語句後面的其他語句。

if 語句可以巢狀,其中 else 與離它最近的尚未匹配的 if 相匹配。

記得用花括號 {},否則很容易出現一個錯誤,就是本來程式中有幾條語句應該作為一個塊來執行,最後沒有。。。這一點要和 python 區別開,因為 python 是以縮排分塊的。為了避免此類問題,強烈建議在 ifelse 之後必須寫上花括號(對 whilefor 語句的迴圈體兩端也有同樣的要求),這麼做的好處是可以避免程式碼混亂不惰,以後修改程式碼時如果想新增別的語旬,也可以很容易地找到正確位置。

當程式碼中 if 分支多下 else 分支時,C++規定 else 與離它最近的尚未匹配的 if 匹配,從而消除了程式的二義性。

// 錯誤:實際的執行過程並非像縮排格式顯示的那樣,else分支匹配的是內層if語句
if (grade % 10 >= 3)
	if (grade %10 > 7)
		lettergrade +='+'; 		// 末尾是8或者9的成績新增一個加號
else
	lettergrade += '-';

// 等價於 <=>
// 違背了初衷
if (grade % 10 >= 3)
	if (grade %10 > 7)
		lettergrade +='+'; 		// 末尾是8或者9的成績新增一個加號
	else
		lettergrade += '-';		// 末尾是3、4、5、6或者7的成績新增一個減號!

要想 else 分支和外層的 if 語句匹配起來,可以在內層 if 語句的兩端加上花括號, 使其成為一個塊:

// 錯誤:實際的執行過程並非像縮排格式顯示的那樣,else分支匹配的是內層if語句
if (grade % 10 >= 3) {
	if (grade %10 > 7)
		lettergrade +='+'; 		// 末尾是8或者9的成績新增一個加號
} else
	lettergrade += '-'; 		// 末尾是1或者2的成績新增一個減號!

最初的想法是如果 >=3,則進入巢狀判斷——如果 >7,那麼輸出一個帶 + 的結果;如果 <3,則輸出一個帶 - 的結果。寫成一個分段函式的話就是:

out={x(grade%10<3)x(7>=grade%10>=3)x+(grade%10>7) out=\left\{ \begin{array}{rcl} x- & & {(grade \% 10 < 3)}\\ x & & {(7 >= grade \% 10 >= 3)}\\ x+ & & {(grade \% 10 > 7)} \end{array} \right.

寫錯之後就變成了:

out={x(7>=grade%10>=3)x+(grade%10>7) out=\left\{ \begin{array}{rcl} x- & & {(7 >= grade \% 10 >= 3)}\\ x+ & & {(grade \% 10 > 7)} \end{array} \right.

測試用例:

#include <iostream>
#include <string>
using namespace std;
int main()
{
	int grade = 0;
	string lettergrade;

	cin >> grade;
	lettergrade =  to_string(grade);

// error program:
	if (grade % 10 >= 3)
		if (grade % 10 > 7)
			lettergrade += '+'; 	// 末尾是8或者9的成績新增一個加號
	else
		lettergrade += '-'; 		// 末尾是3、4、5、6或者7的成績新增一個減號!
// input:22, out:22

// right program:
	//if (grade % 10 >= 3) {
	//	if (grade % 10 > 7)
	//		lettergrade += '+'; 		// 末尾是8或者9的成績新增一個加號
	//}
	//else
	//	lettergrade += '-'; 		// 末尾是1或者2的成績新增一個減號!
// input:22, out:22-

	cout << lettergrade << endl;
	system("pause");
	return 0;
}

如上面的程式,可以使用 22 進行測試:

  • 如果不是最初的想法的話,輸出結果應該是22。
    在這裡插入圖片描述
  • 如果是最初的想法的話,輸出結果應該是22-;
    在這裡插入圖片描述

2)switch語句

switch 語句的形式:
在這裡插入圖片描述
舉一個例子,輸入文字,統計五個母音字母在文字中出現的次數,程式如下:

#include <iostream>
#include <string>
using namespace std;
int main()
{
	// 為每個母音字母初始化其計數值
	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;

	system("pause");
	return 0;
}

在這裡插入圖片描述
switch 語句先對括號裡的表示式求值,值轉換成整數型別後再與每個 case 標籤(case label)的值進行比較(case 標籤必須是整型常量表示式)。如果表示式的值和某個 case 標籤匹配,程式從該標籤之後的第一條語句開始執行,直到到達 switch 的結尾或者遇到 break 語句為止。

通常情況下每個 case 分支後都有 break 語句,如果確實不應該出現 break 語句,最好寫一段註釋說明程式的邏輯。儘管 switch 語句沒有強制一定要在最後一個 case 標籤後寫上 break,但為了安全起見,最好新增 break,這樣即使以後增加了新的 case 分支,也不用再在前面補充 break 語句了。

另外,switch 語句中可以新增一個 default 標籤(default label),如果沒有任何一個 case 標籤能匹配上 switch 表示式的值,程式將執行 default 標籤後的語句。即使不準備在 default 標籤下做任何操作,程式中也應該定義一個 default 標籤,其目的在於告訴他人我們已經考慮到了預設情況,只是目前不需要實際操作。

不允許跨過變數的初始化語句直接跳轉到該變數作用域內的另一個位置。如果需要為 switch 的某個 case 分支定義並初始化一個變數,則應該把變數定義在塊內。

case true:
	{
	    // 正確:宣告語句位於語句塊內部
	    string file_name = get_file_name();
    	// ...
	}
	break;
	
case false:
	if (file_name.empty()) // 錯誤:file_name不在作用域之內

4、迭代語句

迭代語句通常稱為迴圈,它重複執行操作直到滿足某個條件才停止。whilefor 語句在執行迴圈體之前檢查條件,do-while 語句先執行迴圈體再檢查條件。

1)while語句

while 語句的形式:

while (condition)
    statement

只要 condition 的求值結果為 true,就一直執行 statement(通常是一個塊)。condition 不能為空,如果 condition 第一次求值就是 falsestatement 一次都不會執行。

定義在 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

2)傳統的for語句

for 語句的形式:

for (initializer; condition; expression)
    statement

一般情況下,initializer 負責初始化一個值,這個值會隨著迴圈的進行而改變。condition 作為迴圈控制的條件,只要 condition 的求值結果為 true,就執行一次 statement,執行後再由 expression 負責修改 initializer 初始化的變數,修改發生在每次迴圈迭代之後,這個變數就是 condition 檢查的物件。如果 condition 第一次求值就是 falsestatement 一次都不會執行。statement 可以是一條單獨的語句也可以是一條複合語句。

initializer 中也可以定義多個物件,但是隻能有一條宣告語句,因此所有變數的基礎型別必須相同。

注意for 語句頭中定義的物件只在 for 迴圈體內可見。

for 語句頭能省略掉 initializerconditionexpression 中的任何一個(或者全部)。

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);

3)範圍for語句

範圍 for 語句的形式:

for (declaration : expression)
    statement

其中 expression 表示一個序列,擁有能返回迭代器的 beginend 成員,比如用花括號括起來的初始值列表、陣列或者者 vectorstring 等型別的物件。declaration 定義一個變數,序列中的每個元素都應該能轉換成該變數的型別(可以使用 auto)。如果需要對序列中的元素執行寫操作,迴圈變數必須宣告成引用型別,每次迭代都會重新定義迴圈控制變數,並將其初始化為序列中的下一個值,之後才會執行 statement

vector<int> v = {O, 1, 2, 3, 4, 5, 6, 7, 8, 9};

// 範圍變數必須是引用型別,這樣才能對元素執行寫操作
for (auto &r : v) 		// 對於v中的每一個元素
	r *= 2; 			// 將v中每個元素的值翻倍

// 等價於<=>
for (auto beg = v.begin(), end = v.end(); beg != end; ++beg) {
	auto &r = *beg; 	// r必須是引用型別,這樣才能對元素執行寫操作
	r *= 2; 			// 將v中每個元素的值翻倍
}

4)do-while語句

do-while 語句的形式:

do
    statement
while (condition);

do while 語句應該在括號包圍起來的條件後面用一個分號表示語句結束。

計算 condition 的值之前會先執行一次 statementcondition 不能為空。如果 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、跳轉語句

跳轉語句中斷當前的執行過程。

1)break語句

break 語句只能出現在迭代語句或者 switch 語句的內部(包括巢狀在此類迴圈裡的語句或塊的內部),負責終止離它最近的 whiledo-whilefor 或者 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

2)continue語句

continue 語句只能出現在迭代語句的內部,負責終止離它最近的迴圈的當前一次迭代並立即開始下一次迭代。continue 語句只能出現在 forwhiledo while 迴圈的內部,或者巢狀在此類迴圈裡的語句或塊的內部。和 break 語句不同的是,只有當 switch 語句巢狀在迭代語句內部時,才能在 switch 中使用 continue

continue 語句中斷當前迭代後,具體操作視迭代語句型別而定:

  • 對於 whiledo-while 語句來說,繼續判斷條件的值。
  • 對於傳統的 for 語句來說,繼續執行 for 語句頭中的第三部分,之後判斷條件的值。
  • 對於範圍 for 語句來說,是用序列中的下一個元素初始化迴圈變數。
string buf ;
while (cin >> buf && !buf.empty()) {
	if (buf[O] !='_')
		continue; // 接著讀取下一個輸入
	// 程式執行過程到了這裡?說明當前的輸入是以下畫線開始的;接著處理buf......

3)goto語句

goto 語句(labeled statement)是一種特殊的語句,它的作用是從 goto 語句無條件跳轉到同一函式內的另一條語句,在它之前有一個識別符號和一個冒號。

如果能不使用 goto 語句,強烈建議就不要使用了,因為它亂跳的緣故,使得程式既難理解又難修改。

goto 語句的形式:

goto label;    

goto 語句使程式無條件跳轉到標籤為 label 的語句處執行。

end: return; // 帶標籤語句,可以作為goto的目標

標籤識別符號獨立於變數和其他識別符號的名字,它們之間不會相互干擾。但兩者必須位於同一個函式內,同時 goto 語句也不能將程式的控制權從變數的作用域之外轉移到作用域之內。

	// ...
	goto end;
	int ix =10; // 錯誤:goto語句繞過了一個帶初始化的變數定義
end:
	// 錯誤:此處的程式碼需妥使用ix,但是goto語句繞過了它的宣告
	ix = 42;

6、try語句塊和異常處理

異常(exception) 是指程式執行時的反常行為,這些行為超出了函式正常功能的範圍。當程式的某一部分檢測到一個它無法處理的問題時,需要使用 異常處理(exception handling)

異常處理機制為程式中 異常檢測異常處理 這兩部分的協作提供支援,包括 throw 表示式(throw expression)、try 語句塊(try block)和異常類(exception class)。

  • 異常檢測部分使用 throw 表示式表示它遇到了無法處理的問題(throw 引發了異常)。
  • 異常處理部分使用 try 語句塊處理異常。try 語句塊以關鍵字 try開 始,並以一個或多個 catch 子句(catch clause)結束。try 語句塊中程式碼丟擲的異常通常會被某個 catch 子句處理,catch 子句也被稱作異常處理程式碼(exception handler)。
  • 異常類用於在 throw 表示式和相關的 catch 子句之間傳遞異常的具體資訊。

1)throw表示式

throw 表示式包含關鍵字 throw 和緊隨其後的一個表示式,其中表示式的型別就是丟擲的異常型別。throw 表示式後面通常緊跟一個分號,從而構成一條表示式語句。

// 首先檢查兩條資料是否是關於同一種書籍的
if (item1.isbn() != item2.isbn())
	throw runtime_error("Data must refer to same ISBN");
// 如果程式執行到了這裡,表示兩個ISBN
cout << item1 + item2 << endl;

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 函式並終止當前程式的執行。

3)標準異常

異常類分別定義在4個標頭檔案中:

  • 標頭檔案 exception 定義了最通用的異常類 exception。它只報告異常的發生,不提供任何額外資訊。

  • 標頭檔案 stdexcept 定義了幾種常用的異常類。

  • 標頭檔案 new 定義了 bad_alloc 異常類。

  • 標頭檔案 type_info 定義了 bad_cast 異常類。

在這裡插入圖片描述
標準庫異常類只定義了幾種運算,包括建立或拷貝異常型別的物件,以及為異常型別的物件賦值。

標準庫異常類的繼承體系:
在這裡插入圖片描述
只能以預設初始化的方式初始化 exceptionbad_allocbad_cast 物件,不允許為這些物件提供初始值。其他異常類的物件在初始化時必須提供一個 string 或一個C風格字串,通常表示異常資訊,但是不允許使用預設初始化的方式,且必須提供含有錯誤相關資訊的初始值。

參考文章

  • 《C++ Primer》

相關文章