美思 [C 語言] 程式設計教學:使用控制結構 (Control Structure) 改變程式執行順序

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

預設情形下,程式執行的順序是由上至下,但我們可以透過控制結構 (control structure) 來改變程式執行的流程,讓程式有基本的判斷能力。本文介紹 C 語言可用的控制結構。

if 敘述

if 算是最基礎的選擇結構,由於英文中的 if 的語義相當符合這個情境,幾乎所有的程式語言都保留 if 這個保留字。if 的 C 虛擬碼如下:

if (condition_a) {
    // Do something_a.
} else if (condition_b) {
    // Do something_b.
} else {
    // Do something_c.
}

以本例來說,若程式符合 condition_a,程式會執行 something_a 內的程式碼,然後跳出整個 if 敘述。若程式不符合 condition_a,會檢查下一個條件,若程式符合 condition_b,則會執行 something_b 內的程式碼,然後跳出整個 if 敘述。若前述條件皆不符合,則會執行 else 區塊內的程式碼。

除了 if 區塊本身是必需的,else ifelse 都是選擇性的。else if 可以多個,而 else 僅有一個,且需放最後。這用邏輯思考來想即可,不要硬背。

由於 C 沒有強制空格,else ifelse 可以換行,這種方式很適合搭配註解一起使用:

// Comment_a
if (condition_a) {
    // Do something_a.
}
// Comment_b
else if (condition_b) {
    // Do something_b.
}
// Comment_c.
else {
    // Do something_c.
}

讀者可從中自行選擇喜好的風格。

以下是實例:

#include <stdio.h>
#include <stdlib.h>

int main()
{
    // Prompt for input.
    printf("Input an integer: ");

    int num;
    // Valid input.
    if (scanf("%d", &num) == 1) {
        if (num % 2 == 0) {
            printf("%d is even\n", num);
        } else {
            printf("%d is odd\n", num);
        }
    }
    // Invalid input.
    else {
        fprintf(stderr, "Invalid input\n");
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

一開始,我們用一個 prompt 引導使用者輸入整數。但我們不能一廂情願地認定輸入的資料格式正確,仍要檢查使用者輸入的內容。以本例來說,若輸入的格式正確,我們就檢查該數字是奇數或偶數;若不正確,則跳出錯誤訊息。

(選擇性) 排列 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 算是一種小小的語法糖,主要是用來簡化 if 敘述。switch 的 C 虛擬碼如下:

switch (value) {
case a:
    // Do something.
    break;
case b:
case c:
    // Fallthrough.
    // Do something.
    break;
default:
    // Do something.
    break;
}

要注意在 switch 敘述中,若沒寫 break 則會繼續前往下一個條件,這種特性叫做 fallthrough。由於這種特性有時會造成 bug,在 Go 等現代語言中將其修改掉了。

以下是實例:

#include <stdio.h>
#include <time.h>

int main(void)
{
    // Get the object to `tm *` for current time.
    time_t t = time(NULL);
    struct tm *now = localtime(&t);

    switch (now->tm_wday) {
    case 0:  // Sunday.
    case 6:  // Saturday.
        // Fallthrough.
        printf("Weekend\n");
        break;
    case 5:  // Friday.
        printf("Thank God. It's Friday.\n");
        break;
    case 3:  // Wednesday.
        printf("Hump day\n");
        break;
    default:  // All other days of week.
        printf("Week\n");
        break;
    }

    return 0;
}

一開始,我們建立一個時間物件,接著,取得當下時間。根據當下時間來吐出相對應的訊息,藉由 switch 來進行判斷。讀者目前不用在糾結在時間物件的使用細節,目前就當固定用法即可,學到指標後自然會這種寫法。

switchif 是相通的,讀者可試著用 if 改寫這個例子。

while 敘述

while 是一種未定次數的迴圈,主要是用來反覆執行某一區塊的程式碼。while 的 C 虛擬碼如下:

while (condition) {
    // Do something.
}

condition 的狀況為真時,就會執行區塊內的程式碼;反之,則不執行。

以下是實例:

#include <stdio.h>

int main(void)
{
    int i = 10;

    while (i > 0) {
        printf("Counting down %d\n", i);

        i--;
    }

    printf("Blast\n");

    return 0;
}

如果寫成 while(1) 或是 while(true) 等,代表該迴圈是無限迴圈。C 虛擬碼如下:

// Run infinitely.
while (1) {
  // Do something.
}

初學者可能會在無意間寫出無限迴圈,這也算是一種常見的 bug。不過,無限迴圈並不是什麼可怕的事,電腦遊戲的 game loop 或是視窗程式的 event loop 基本上都是某種無限迴圈。無限迴圈會搭配 break 敘述以跳出此迴圈,詳見下文。

do ... while 敘述

do ... while 算是 while 的變體,和 while 不同的是 do ... while 至少會執行一次。其虛擬碼如下:

do {

} while (condition);

相對於 while 來說,do ... while 比較少用,一些使用的時機像是減少重覆程式碼。

有一個小技巧是用 do ... while(0) 執行單次迴圈,像是以下 C 虛擬碼:

do {
    // do some things.

    if (error) {
        break;
    }

    // do more things.
} while (0);

這個小技巧會有用是因為我們不能在 if 敘述中加上 break,但在 while 中可以。在這個技巧中,我們實質上是使用一個可以加上 break 的單一區塊,這樣可以簡化錯誤處理的步驟。

for 敘述

for 迴圈是用來執行有特定次數的程式區塊,在 C 語言中,for 只有一種形式,就是用計數器來控制。以下是 for 的虛擬碼:

for (start; end; adjustment) {
    // Do something.
}

start 中要將計數器初始化,end 為計數中止條件,adjustment 為每次調整計數器的步驟。

其實 for 可以改寫成等義的 while

start;
while (end) {
    // Do something.

    adjustment;
}

for 改寫成等義的 while 或反過來也是一種基本的程式練習,讀者可自行嘗試。

我們將先前 while 的例子改寫:

#include <stdio.h>

int main(void)
{
    // C99
    for (int i = 10; i > 0; i--) {
        printf("Counting down %d\n", i);
    }

    printf("Blast\n");

    return 0;
}

要注意在 for 條件內初始化變數的方法僅限 C99 後可用,在 C89 前只能用以下寫法:

int i;
for (i = 0; i < 10; i++) {
    // Do something.
}

但是命名空間中會多一個 i 變數,而 i 又是迴圈中慣用的變數。如果為了某些原因需停留在 C89,可將整個 for 敘述包在區塊中以解決命名空間汙染的問題:

{
    int i;
    for (i = 0; i < 10; i++) {
        // Do something.
    }
}

for (;;) 也代表無限迴圈:

// Run infinitely.
for (;;) {
    // Do something.
}

但語義上不若 while (true) 自然。

continuebreak

這兩種語法算是限縮版本的 goto,用來改變迴圈的行進,差別如下:

  • continue:重新開始迴圈
  • break:跳出迴圈

由於有 continuebreak,我們現在很少需要撰寫 goto 語句。

以下是實例:

#include <stdbool.h>
#include <stdio.h>

int main()
{
    int num;
    char b[256];
    // Run infinitely until the user input a valid number.
    while(true) {
        // Prompt for user input.
        printf("Input a number: ");

        if (scanf("%d", &num) != 1) {
            // Trick to prevent infinite loop.
            scanf("%s", b);

            // Show error message to the user.
            fprintf(stderr, "Invalid input\n");

            continue;  // Re-start the loop.
        }

        // Show info message to the user.
        printf("You input %d\n", num);

        break;  // Exit the loop.
    }

    return 0;
}

goto 敘述

goto 是一種短程跳躍的語法,在同函式內可前往任意的位置。結構化的程式設計 (structured programming) 會盡量避免使用 goto,因過度使用 goto 會使得程式難以維護。不過,goto 偶爾可以使語法更簡潔,像是在程式運行結束或中止後釋放系統資源。我們會於後續相關章節會展示其用法。

return 敘述

return 用於跳出函式敘述並回到函式呼叫所在的位置,若是跳出主函式 (main) 則結束程式,若函式中沒有使用 return 則會執行完所有的函式敘述後自動跳出函式。詳見後文有關函式的介紹。

關於作者

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

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