前言
程式預設的執行順序是逐行執行敘述。但程式設計者可以用控制結構改變程式執行的順序。本文介紹 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 行)。