前言
程式預設的執行順序是逐行執行敘述。但程式設計者可以用控制結構改變程式執行的順序。本文介紹 C++ 的控制結構。
if 敘述
if 敘述是基本的選擇控制結構。以下是 if 敘述的虛擬碼:
if (condition) {
statement;
...
}
只有在 condition 為真時,才會執行 if 區塊內的 statement。反之,則略過該區塊。
除了單一的 if 敘述,還可以增加選擇性的 else 敘述,形成二元敘述。其虛擬碼如下:
if (condition) {
statement;
...
}
else {
statement;
...
}
當 if 敘述為真時,執行 if 區塊內的程式碼。反之,則執行 else 區塊內的程式碼。
除了前述的 else 敘述,還可以增加一至多個 else if 敘述,形成多元敘述。其虛擬碼如下:
if (condition_a) {
statement;
...
}
else if (condition_b) {
statement;
...
}
else {
statement;
...
}
當符合 condition_a 時,執行 if 敘述的程式碼區塊。當符合 condition_b 時,執行 else if 敘述的程式碼區塊。反之,兩者皆不符合時,執行 else 敘述的程式碼區塊。
注意 condition_a 和 condition_b 的順序是有意義的。當 condition_a 符合時,即會進入 if 敘述的區塊,即使 condition_b 也符合。所以,在寫多元 if 敘述時,要根據情境妥善地安排不同區塊的順序。
由於 else 敘述是選擇性的,這樣的程式碼也是可接受的:
if (condition) {
statement;
...
}
else if (condition) {
statement;
...
}
看夠了虛擬碼,來看一下實際的程式碼:
#include <cstdlib>
#include <ctime>
#include <iostream>
using std::cout;
using std::endl;
int main(void)
{
srand((unsigned) time(NULL));
auto state = rand() % 4 + 1;
if (state == 1) {
cout << "Slowly walk" << endl;
}
else if (state == 2) {
cout << "Run!" << endl;
}
else if (state == 3) {
cout << "Fall down" << endl;
}
else if (state == 4) {
cout << "Take a rest" << endl;
}
else {
throw "Invalid state";
}
return 0;
}
這個程式的 state 應該只有四種,所以我們刻意在 else 敘述拋出例外,實際上程式不會走到 else 區塊。
(選擇性) 排列 else if 和 else 敘述的方式
許多 C++ 教材會使用以下方式來排列 else if 敘述:
if (a) {
/* statement_a. */
} else if (b) {
/* statement_b. */
}
做為 C 家族語言,Golang 甚至把這種風格寫死在程式碼重排軟體中,沒得修改。
然而,筆者會建議用以下的方式排列程式碼:
if (a) {
/* statement_a. */
}
else if (b) {
/* statement_b. */
}
因為 if 和 else if 縮進在同一行上,做為一種視覺提示,閱讀程式碼時可立即知道這兩段敘述是同一層。在寫巢狀控制結構時,這樣的程式碼排列方式格外有用。
有些程式設計者甚至引入 Pascal 或 C# 的程式碼排列方式:
if (a)
{
/* statement_a. */
}
else if (b)
{
/* statement_b. */
}
寫巢狀控制結構時,這種風格也蠻不錯的,在視覺上可以區分層次。目前筆者沒有使用這樣的風格。
本節所建議的事項只是一種撰碼風格,不具強制性。讀者仍然可以使用自己喜歡的風格。
switch 敘述
switch 敘述是一種特化的選擇控制結構。其虛擬碼如下:
switch (value) {
case a:
statement;
...
break;
case b:
// Fallthrough.
case c:
statement;
...
break;
default:
statement;
...
break;
}
switch 敘述比較的對象是 value。當 value 符合 a 時,執行 a 區塊的程式碼。執行到 break 指令時結束該區塊,跳出 switch 敘述。
當 value 符合 b 時,由於 b 區塊沒有 break 敘述,會繼續往下執行 c 區塊的敘述。這種特性稱為 fallthrough 。這種特性也可能成為臭蟲的來源,要仔細確認 fallthrough 是否為預期中的行為。
當 value 符合 c 時,執行 c 區塊內的敘述。其行為和 a 區塊類似,不重述。
當 value 不符合所有的值時,執行 default 區塊內的敘述。由於 default 區塊是選擇性的,若不需要可略去。
看夠了虛擬碼,來看一下實際的範例程式:
#include <ctime>
#include <iostream>
using std::cout;
using std::endl;
int main(void)
{
// Get current time struct.
time_t rawtime;
tm * timeinfo;
time(&rawtime);
timeinfo = localtime(&rawtime);
// tm_wday ranges from 0 to 6
switch (timeinfo->tm_wday + 1) {
case 6:
case 7:
cout << "Weekend" << endl;
break;
case 5:
cout << "Thank God. It's Friday" << endl;
break;
case 3:
cout << "Hump day" << endl;
break;
default:
cout << "Week" << endl;
break;
}
return 0;
}
一開始先用 C 的 time.h (此處為 ctime) 求得時間物件 timeinfo。從該物件可以取得一星期的日子 (day of week)。
這裡將 tm_wday 放入 switch 敘述來比較。由於 tm_wday 是從 0 開始計算,和一般的習慣有差異,這裡將其偏移 1,比較符合大部分程式語言的值。
while 敘述
while 敘述是基本的迭代控制結構。所謂的迭代控制結構 (迴圈) 是可以反覆執行的程式碼。藉由迴圈,就不需要撰寫重覆的程式碼。其虛擬碼如下:
while (condition) {
// Do something
}
將進行每輪迭代前,會先檢查 condition 是否符合。當 condition 符合時,即執行一輪該區塊內的程式碼。然後再進行下一輪迭代。反之,當 condition 不符合時,就跳離 while 敘述。
只要 condition 一直是符合的狀態,while 敘述就會持續執行。在偶然的情形下,會造成無法跳離 while 敘述的程式,這種臭蟲稱為無窮迴圈 (infinite loop)。
看完虛擬碼後,來看一下實際的範例程式:
#include <iostream>
using std::cout;
using std::endl;
int main(void)
{
auto i = 10;
while (i > 0) {
cout << i << endl;
--i;
}
return 0;
}
一開始變數 i 的值為 10。當 i 大於 0 時,while 敘述會持續迭代。每輪迭代 i 的數值會減 1,避免無窮迴圈。實際的行為是從 10 至 1 逐行印出數字。
do ... while 敘述
do ... while 敘述是 while 的變體。其虛擬碼如下:
do {
// Do something
} while (condition);
不論 condition 是否為真,至少都會執行一次 do 區塊內的程式碼。其餘行為和一般的 while 敘述無異。
以下是實例:
#include <iostream>
using std::cout;
using std::endl;
int main(void)
{
auto n = 0.001;
do {
cout << n << endl;
n/= 2.0;
} while (n > 0.0001);
return 0;
}
此處的 n 是浮點數。浮點數在每次運算時有可能會產生微小誤差,不能用 == 來寫條件句,以免造成無窮迴圈。
for 敘述
for 敘述是特化的迭代控制結構。C++ 的 for 敘述有兩種使用方式。本節說明 C++ 的 for 敘述。
以計數器為基礎的 for
這種型式的 for 敘述承襲 C 的 for 敘述。其虛擬碼如下:
for (init; condition; update) {
// Do something
}
在 init 子區塊會初始化一至多個計數器。當計數器符合 condition 時,會進行一輸迭代。每輪迭代後,在 update 子區塊會增/減計數器。
只看虛擬碼會比較抽象,改看一下實例:
#include <iostream>
using std::cout;
using std::endl;
int main(void)
{
for (auto i = 10; i > 0; --i) {
cout << i << endl;
}
return 0;
}
本範例程式的計數器為 i。一開始該計數器的值是 10。當 i 大於 0 時,會持續迭代下去。每輪迭代後 i 減少 1。
每個 for 敘述都可以重新以 while 敘述改寫。讀者可自行嘗試看看。
以迭代器為基礎的 for
這部分會用到容器 (collections),留待後文說明。
改變迴圈行進的敘述
本節介紹可以改變迴圈行進的指令。
break 敘述
使用 break 敘述可以提前離開迴圈。以下是實例:
#include <iostream>
using std::cout;
using std::endl;
int main(void)
{
for (auto i = 10; i > 0; i--) {
if (i <= 5) {
break;
}
cout << i << endl;
}
return 0;
}
原本計數器會遞減到 0 時才跳離 for 迴圈,但此處的 i 小於等於 5 時會觸發 break 敘述,提早離開 for 迴圈。
continue 敘述
使用 continue 敘述可以略過該輪迭代,繼續下一輪迭代。以下是實例:
#include <iostream>
using std::cout;
using std::endl;
int main(void)
{
for (auto i = 10; i > 0; i--) {
if (i % 2 != 0) {
continue;
}
cout << i << endl;
}
return 0;
}
原本的 for 迴圈會印出十次數字。但在 i 為奇數 (i % 2 != 0) 時,會觸發 continue 敘述,略過該輪迭代。實際的效果是只印出偶數。
goto 敘述
使用 goto 敘述可無條件跳離當前敘述,直接前往標籤 (label) 所在的地方。以下短例展示 goto 敘述的使用方式:
#include <iostream>
int main(void)
{
goto LABEL;
throw "It won't run";
LABEL:
std::cout << "It will print" << std::endl;
return 0;
}
這個範例在 goto 敘述的下方拋出了例外,但 goto 敘述直接跳到 LABEL 所在的地方,故拋出例外的敘述不會執行。
胡亂地使用 goto 敘述會造成程式碼難以維護。但適度地使用 goto 敘述會讓程式碼更簡潔,像是在釋放系統資源 (system resources) 時。
(範例) 終極密碼
先前的範例偏短,不易感受控制結構在程式中的用法。本節展示一個稍長的範例程式。
這個程式是終端機版本的終極密碼 (猜數字)。編譯此程式的指令如下:
$ g++ -Wall -Wextra -o guessNumber guessNumber.cc
實際使用此程式的過程如下:
$ ./guessNumber
Input a number between 1 and 100: 50
Too small
Input a number between 50 and 100: 75
Too small
Input a number between 75 and 100: 87
Too small
Input a number between 87 and 100: 93
Too large
Input a number between 87 and 93: 90
Too small
Input a number between 90 and 93: 92
Too large
Input a number between 90 and 92: 91
You guess right
這裡列出完整的程式碼。後文會進一步說明:
#include <cstdlib> /* 1 */
#include <ctime> /* 2 */
#include <iostream> /* 3 */
#include <regex> /* 4 */
#include <string> /* 5 */
int main(void) /* 6 */
{ /* 7 */
/* Set bound value of an answer. */ /* 8 */
auto min = 1; /* 9 */
auto max = 100; /* 10 */
/* Set a random seed by current system time. */ /* 11 */
srand((unsigned) time(NULL)); /* 12 */
/* Get a random integer between min and max. */ /* 13 */
auto answer = rand() % (max - min + 1) + min; /* 14 */
bool gameOver = false; /* 15 */
while (!gameOver) { /* 16 */
bool hasGuess = false; /* 17 */
int guess; /* 18 */
std::string input; /* 19 */
while (!hasGuess) { /* 20 */
std::cout << "Input a number between " /* 21 */
<< min << " and " << max << ": "; /* 22 */
std::cin >> input; /* 23 */
/* Trim leading space(s). */ /* 24 */
input = std::regex_replace( /* 25 */
input, std::regex("^\\s+"), /* 26 */
std::string("")); /* 27 */
/* Trim trailing space(s). */ /* 28 */
input = std::regex_replace( /* 29 */
input, std::regex("\\s+$"), /* 30 */
std::string("")); /* 31 */
/* Check whether input is a valid number. */ /* 32 */
if (!std::regex_match( /* 33 */
input, std::regex("^[+-]?\\d+$"))) /* 34 */
{ /* 35 */
std::cout << "Invalid data: " /* 36 */
<< input << std::endl; /* 37 */
continue; /* 38 */
} /* 39 */
guess = std::stoi(input); /* 40 */
/* Check whether `guess` is within
our range. */
if (!(min <= guess && guess <= max)) { /* 41 */
std::cout << "Integer out of range: " /* 42 */
<< guess << std::endl; /* 43 */
continue; /* 44 */
} /* 45 */
hasGuess = true; /* 46 */
} /* 47 */
/* Check whether `guess` is equal to `answer`. */ /* 48 */
if (answer < guess) { /* 49 */
std::cout << "Too large" << std::endl; /* 50 */
max = guess; /* 51 */
} /* 52 */
else if (guess < answer) { /* 53 */
std::cout << "Too small" << std::endl; /* 54 */
min = guess; /* 55 */
} /* 56 */
else { /* 57 */
std::cout << "You guess right" << std::endl; /* 58 */
gameOver = true; /* 59 */
} /* 60 */
} /* 61 */
} /* 62 */
一開始先設定 answer 的上下限 max 和 min (第 9、10 行)。然後根據上下限產生隨機的 answer (第 12 至 14 行)。
在使用者還沒猜出數字前,遊戲迴圈會持續迭代。設置代表遊戲狀態的變數 gameOver (第 15 行)。然後開始遊戲迴圈 (第 16 行起)。
遊戲的前半部要收集使用者輸入的資料並檢查資料是否合理 (第 17 至 47 行)。在未收到正確資料前,會反覆詢問使用者,直到收到合理的資料。用 hasGuess 儲存這個迴圈的狀態 (第 17 行)。
每次請使用者輸入時,先給予使用者適當的提示 (第 21、22 行)。每輪遊戲迴圈的提示範圍會根據使用者先前的輸入而改變。
使用 std::cin 從標準輸入 (standard input) 接收使用者的輸入 (第 23 行)。接收輸入後,用兩個常規表示式 (regular expression) 去除輸入資料中頭尾的空白 (第 25 至 31 行)。然後再用一個常規表示式檢查輸入是否為符合整數樣子的字串 (第 33、34 行)。
輸入資料的型態是字串。在確認數字前,要先將資料轉換為整數 (第 40 行),然然和現存的值比較是否在範圍內 (第 41 行)。
當 guess 確認是合理的,就可以和 answer 比對是否相符 (第 49 至 60 行)。當兩者相等時,結束遊戲 (第 57 至 60 行)。反之,則調整邊界值 (第 49 至 56 行)。