2011年12月14日 星期三

C 語言入門 - 條件式路線分歧

控制程式的行進流程與路線

  到目前為止,我們已能夠讀取使用者的輸入,做出簡單的處理並輸出最後結果了。然而仍有許多問題沒有解決。我們不會希望我們的程式毫無判斷能力,好比你的女僕不會分辨訪客和不速之客,所以會用同一套方式對待他們。所以你會對於是要一律友善對待,還是一律踢飛感到相當糾結。這是相當嚴重的問題,所以我們必須更進一步了解,如何控制程式的運作流程。

  現在我們要介紹 if 這個流程控制用的語法。它能夠讓你有條件地執行某一段程式碼。也就是說,在滿足某些條件的情形下,才執行某一段程式碼。這讓你的程式具有基本的判斷能力。比如說你可以寫個借錢程式,看使用者能借你多少錢。如果他輸入負數就取個絕對值混過去,如果他輸入太小的數字,比如說不到一百,就可以罵他小氣。然後依借錢多寡決定要罵他還是感謝他,以及程度。這時就會需要依賴 if 按照使用者的輸入做判斷,決定要執行哪些程式碼。if 的語法大致如下:

if (a < 20)
    printf("WTF!?\n");

if (a > 2000)
    printf("Oh, you are a GOOD man!\n");

  這是 if 最基本的樣子。要讓 if 為我們條件性地執行某些程式碼,理所當然地要告訴他條件,因此後面必須有小括號。當 if 後面的小括號內的條件成立之時,就會執行後面的程式碼。你可能會想問,所謂的「後面的程式碼」它的範圍是?定義上是 if 的下一個敘述 (statement) 而已。敘述可以是一個流程控制 (例如 if),一段以分號為結尾的運算式,或是一個以大括號圍起來的區塊 (block)。

  如果沒有條件成立時要執行的程式碼會很奇怪,這會讓 if 懷疑它的存在意義,所以即使你不說,他也會自己伸手去抓住下一個敘述。他認為這是他的工作,也認為下一個敘述會是你希望他做的事。所以條件不成立時,if 將為你確保它抓住的這個敘述不會被執行。換句話說,它會為你嚴格把關一個敘述是否會被執行。只有在得到你允許的情形下,才讓由它把關的程式碼被執行。

  如果你覺得很難理解何謂敘述,沒關係。我們一律使用大括號將我們希望 if 在條件成立時執行的程式碼包起來,這樣就萬無一失了。即使只有一行也使用大括號,會導致程式碼稍微冗長一點,但比較不易發生錯誤,日後要追加程式碼也方便。因此建議寫成這樣:

if (a < 20)
{
    printf("WTF!?\n");
}

if (a > 2000)
{
    printf("Oh, you are a GOOD man!\n");
}

  這樣做的話不只自己看的時候比較清楚明白,就連 if 也會感謝你的用心,因為它也樂得免去許多誤會的產生,也較不會被怪罪。這種寫法最不容易發生因誤解 if 魔爪的影響範圍,而造成程式執行結果不如預期。這相當的重要,待會會提個有名的例子。

  平常可能只是像咖啡要不要加糖,這種做與不做的事。然而有時,我們希望在條件成立時做一件事,不成立時做另一件事。像是你家女僕問你要喝可樂還是白開水時,會按照你的選擇做不一樣的事,而且不論你怎麼選都有事要做。這時可以請 if 伸出另一隻手,也就是 else 來幫忙。所以會變成這樣:

if (a < 1)
{
    printf("ok. Here is the cola.\n");
}
else
{
    printf("ok. Here is the juice.\n");
}

  由於 else 本身算是 if 延伸出去的一隻手,所以不能夠脫離 if 單獨存在。但是你可以自由決定一個 if 要不要伸出 else 這隻手。也就是說它並非必要的存在。這時在 if 的條件式成立時,會執行 if 的下一個敘述;若不成立,則執行 else 的下一個敘述。else 已經註定是條件式不成立時執行,沒有別的可能,所以不需要小括號。同樣地,出於想要忠實地為你完成任務,else 這隻手也必定會去抓住它的下一個敘述。


數量即力量、同心協力的 if 如何合作

  儘管你可以用相反的條件,使用另一個 if 達成 else 的效果,但這在條件複雜時相當方便。但是若是你的女僕今天心情特別好,準備了比平常更多種飲料的話,怎麼辦?一個比較直覺的方法是這樣:

if (a < 5)
{
    printf("COLA!!\n");
}
else
{
    if (a < 10)
    {
        printf("JUICE!!\n");
    }
    else
    {
        printf("BLOOD!!\n");
    }
}

  雖然飲料是平等的,不過仍然可以先二分成「要可樂」與「不要可樂」,然後把其它選項放在「不要可樂」的前提底下,再去用一樣的手段做二分法,如此就能把這些分歧並列起來。如果硬要將所有選擇平等擺在一起,反而使問題變得過於複雜而難以處理。我們先處理其中一個選項,也就是說,先考慮你要的是不是可樂,不是時再去考慮別的飲料。不過這裡會告訴你更好的解決方案。

if (a < 5)
{
    printf("COLA!!\n");
}
else if (a < 10)
{
    printf("JUICE!!\n");
}
else
{
    printf("BLOOD!!\n");
}

  你可能會想抱怨說,什麼嘛,我們親愛的 if 不是能伸很多隻手的嗎,怎麼不早說就好了還用到兩隻 if 才解決問題?好吧大概又有人要哭了。且讓我們換個方式,讓那位哭泣中的 if 顯眼一點。


if (a < 5)
{
    printf("COLA!!\n");
}
else
    if (a < 10)
    {
        printf("JUICE!!\n");
    }
    else
    {
        printf("BLOOD!!\n");
    }

  不知你是否還記得,流程控制語法也是敘述的一種。和上面使用兩隻 if 的解決方法很像吧?實際上也是有兩隻 if,只是第二隻被第一隻用 else 那手抓得緊緊的貼在一起,所以你沒注意到它的存在,以為其實有很多隻手。這是個天大的誤會。一隻 if 只有一隻本來的手和一隻 else 的手而已,想要並列更多選擇還是得靠很多隻 if 同心協力才行。如果你認為只有一隻那其它的可是會傷心的。

  如果需要多重條件時,比如說既要 a < 3 又要 b < 3 的話,也可以先把它簡化後拆成許多小步驟,再使用多重的 if 來處理。我們可以這樣子寫:

if (a < 3)
{
    if (b < 3)
    {
        printf("wafuuuuuuu\n");
    }
}

  我們先不要考慮什麼 a < 3 與 b < 3 要「同時成立」這種想法。換個方式想,可以看成是「在 a < 3 成立時」,也就是進入 if (a < 3) 條件成立的情形時,「如果 b < 3 也成立的話」那不就是 a < 3 且 b < 3 的情形了嗎?也就是說,a < 3 且 b < 3。

  總結一下,在出現多重選擇時,像是將家裡女僕依照表現評價分成 S、A、B、C、D 五種等級時,只會有其中一種成立,所以這時的 if 會是透過 else 來並列的形式。else 本來就與原本 if 的條件是互斥的,並不可能同時成立,所以透過 else 並列時,會在出現第一個條件成立的 if 並執行完屬於它的程式碼後,結束這一整串並列的 if。後面即使有滿足條件的,也會因為是掛在前面 if 的 else 底下,而無法成立。比如說,

if (a < 10)
{
    printf("D\n");
}
else if (a < 20)
{
    printf("C\n");
}

  這時如果 a 是 5 的話,只會輸出 D,雖然也滿足後面的 a < 20,但是因為滿足了 a < 10 所以 else 根本不成立,掛在 else 底下的 a < 20 就會被前面的 if 伸出來的 else 抓住並確保不會執行到。也就是說第二個 if 其實因為 else 的關係,帶有隱藏的條件 a >= 10。

  在出現多重條件時,也就是多個條件要同時滿足,所以必須多個 if 層層把關才行。這稱為巢狀 if (nested if) 也就是 if 條件成立後又有其它 if 來確保每個條件都符合,這樣層層包起來。像是一個好的女僕必須同時具備高忠誠度、高智力、高反應和高體力,這種就是多重條件。比如說,

if (a > 100)
{
    if (b > 70)
    {
        if (c > 90)
        {
            if (d > 130)
            {
                printf("This is a perfect maid!\n");
            }
        }
    }
}

  當忠誠確認 > 100 之後,還必須經過確認智力 > 70,之後還得要反應 > 90,最後體力 > 130 才能被認定是好的女僕。雖然多重條件每個都一樣重要,但因為都很重要所以都必須成立,先後順序就比較沒關係,確保每個都成立即可。


if 之間的爭吵與互不相讓

  當然 if 也不總是同心協力的,多少也會因為爭寵之類的原因吵架,比如說下面的例子。

if (a < 5)
    if (a < 3)
        printf("so small..\n");
else
    printf("so large!!\n");

  實際上那隻 else 應該是要誰伸出去好呢?這時就會吵架了。第一隻 if 會說光看主人的排版,就知道應該要它伸出 else 這隻手。但是第二隻 if 會說明明是離它比較近,應該是要它伸出去才對,於是爭吵不休。最後大法官編譯器判決,應該給第二隻 if 來伸出 else 才對。這讓第一隻 if 傷心欲絕,同時它也擔心要是主人其實是希望他伸出 else 的話該怎麼辦。實際上這種可能性相當大,第二隻可能也因為最後結果不符主人預期而感到失落。

  這種情形稱為 dangling else,編譯器會在這種難以判定的例子,把 else 交給最接近的 if。這也是為什麼我會推薦不管怎樣一律加大括號。如果出現這種情形,沒有經驗的話是很難看出錯誤的,也就是說這個錯誤非常地難找。如果寫成以下的形式就不會引起爭吵,也不會產生非預期中的結果了。

if (a < 5)
{
    if (a < 3)
    {
        printf("so small..\n");
    }
}
else
{
    printf("so large!!\n");
}

  現在我們可以試著寫寫看更富變化的程式。比如說輸入兩個整數,輸出它們的差。或是輸入兩個整數,比較看看誰比較大。甚至,可以透過讓使用者輸入數字,代表選擇的選項,來寫個簡單的文字冒險遊戲了呢。依使用者選擇的路線決定故事的發展,最後邁向感人的或悲劇的或慘敗的結局。有耐心的話已經足以完成了喔。

沒有留言:

張貼留言