C 語言程式設計教學:控制結構 (Control Structure)

PUBLISHED ON JUL 4, 2018 — PROGRAMMING
FacebookTwitter 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 引導使用者輸入整數。但我們不能一廂情願地認定輸入的資料格式正確,仍要檢查使用者輸入的內容。以本例來說,若輸入的格式正確,我們就檢查該數字是奇數或偶數;若不正確,則跳出錯誤訊息。

    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.
    }
    

    以下是實例:

    #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 something
        if (error) {
            break;
        }
    
        // do something else
    } 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 則會執行完所有的函式敘述後自動跳出函式。詳見後文有關函式的介紹。