笨辦法學C 練習13:Switch語句

飛龍發表於2019-05-10

練習13:Switch語句

原文:Exercise 13: Switch Statement

譯者:飛龍

在其它類似Ruby的語言中,switch語句可以處理任意型別的表示式。一些語言比如Python沒有switch語句,因為帶有布林表示式的if語句可以做相同的事情。對於這些語言,switch語句比if語句更加靈活,然而內部的機制是一樣的。

C中的switch語句與它們不同,實際上是一個“跳轉表”。你只能夠放置結果為整數的表示式,而不是一些隨機的布林表示式,這些整數用於計算從swicth頂部到匹配部分的跳轉。下面有一段程式碼,我要分解它來讓你理解“跳轉表”的概念:

#include <stdio.h>

int main(int argc, char *argv[])
{
    if(argc != 2) {
        printf("ERROR: You need one argument.
");
        // this is how you abort a program
        return 1;
    }

    int i = 0;
    for(i = 0; argv[1][i] != ` `; i++) {
        char letter = argv[1][i];

        switch(letter) {
            case `a`:
            case `A`:
                printf("%d: `A`
", i);
                break;

            case `e`:
            case `E`:
                printf("%d: `E`
", i);
                break;

            case `i`:
            case `I`:
                printf("%d: `I`
", i);
                break;

            case `o`:
            case `O`:
                printf("%d: `O`
", i);
                break;

            case `u`:
            case `U`:
                printf("%d: `U`
", i);
                break;

            case `y`:
            case `Y`:
                if(i > 2) {
                    // it`s only sometimes Y
                    printf("%d: `Y`
", i);
                }
                break;

            default:
                printf("%d: %c is not a vowel
", i, letter);
        }
    }

    return 0;
}

在這個程式中我們接受了單一的命令列引數,並且用一種極其複雜的方式列印出所有原因,來向你演示switch語句。下面是swicth語句的工作原理:

  • 編譯器會標記swicth語句的頂端,我們先把它記為地址Y。

  • 接著對switch中的表示式求值,產生一個數字。在上面的例子中,數字為argv[1]中字母的原始的ASCLL碼。

  • 編譯器也會把每個類似case `A`case程式碼塊翻譯成這個程式中距離語句頂端的地址,所以case `A`就在Y + `A`處。

  • 接著計算是否Y+letter位於switch語句中,如果距離太遠則會將其調整為Y+Default

  • 一旦計算出了地址,程式就會“跳”到程式碼的那個位置並繼續執行。這就是一些case程式碼塊中有break而另外一些沒有的原因。

  • 如果輸出了`a`,那它就會跳到case `a`,它裡面沒有break語句,所以它會貫穿執行底下帶有程式碼和breakcase `A`

  • 最後它執行這段程式碼,執行break完全跳出switch語句塊。

譯者注:更常見的情況是,gcc會在空白處單獨構建一張跳轉表,各個偏移處存放對應的case語句的地址。Y不是switch語句的起始地址,而是這張表的起始地址。程式會跳轉到*(Y + `A`)而不是Y + `A`處。

這是對swicth語句工作原理的一個深究,然而實際操作中你只需要記住下面幾條簡單的原則:

  • 總是要包含一個default:分支,可以讓你接住被忽略的輸入。

  • 不要允許“貫穿”執行,除非你真的想這麼做,這種情況下最好新增一個//fallthrough的註釋。

  • 一定要先編寫casebreak,再編寫其中的程式碼。

  • 如果能夠簡化的話,用if語句代替。

你會看到什麼

下面是我執行它的一個例子,也演示了傳入命令列引數的不同方法:

$ make ex13
cc -Wall -g    ex13.c   -o ex13
$ ./ex13
ERROR: You need one argument.
$
$ ./ex13 Zed
0: Z is not a vowel
1: `E`
2: d is not a vowel
$
$ ./ex13 Zed Shaw
ERROR: You need one argument.
$
$ ./ex13 "Zed Shaw"
0: Z is not a vowel
1: `E`
2: d is not a vowel
3:   is not a vowel
4: S is not a vowel
5: h is not a vowel
6: `A`
7: w is not a vowel
$

記住在程式碼的開始有個if語句,當沒有提供足夠的引數時使用return 1返回。返回非0是你提示作業系統程式出錯的辦法。任何大於0的值都可以在指令碼中測試,其它程式會由此知道發生了什麼。

如何使它崩潰

破壞一個switch語句塊太容易了。下面是一些方法,你可以挑一個來用:

  • 忘記寫break,程式就會執行兩個或多個程式碼塊,這些都是你不想執行的。

  • 忘記寫default,程式會在靜默中忽略你所忘記的值。

  • 無意中將一些帶有預料之外的值的變數放入switch中,比如帶有奇怪的值的int

  • switch中是否未初始化的值。

你也可以使用一些別的方法使這個程式崩潰。試著看你能不能自己做到它。

附加題

  • 編寫另一個程式,在字母上做算術運算將它們轉換為小寫,並且在switch中移除所有額外的大寫字母。

  • 使用`,`(逗號)在for迴圈中初始化letter

  • 使用另一個for迴圈來讓它處理你傳入的所有命令列引數。

  • 將這個switch語句轉為if語句,你更喜歡哪個呢?

  • 在“Y”的例子中,我在if程式碼塊外面寫了個break。這樣會產生什麼效果?如果把它移進if程式碼塊,會發生什麼?自己試著解答它,並證明你是正確的。

相關文章