位元詩人 [C++] 程式設計教學:控制結構 (Control Structures)

C++控制結構
Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

前言

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

許多 C++ 教材會使用以下方式來排列 else if 敘述:

if (a) {
    /* statement_a. */
} else if (b) {
    /* statement_b. */
}

做為 C 家族語言,Golang 甚至把這種風格寫死在程式碼重排軟體中,沒得修改。

然而,筆者會建議用以下的方式排列程式碼:

if (a) {
    /* statement_a. */
}
else if (b) {
    /* statement_b. */
}

因為 ifelse 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,避免無窮迴圈。實際的行為是從 101 逐行印出數字。

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 的上下限 maxmin (第 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 行)。

關於作者

身為資訊領域碩士,位元詩人 (ByteBard) 認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

位元詩人喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,位元詩人將所學寫成文章,放在這個網站上和大家分享。