未定義、未指定與實作定義行為
人們隨口會說某支 C 程式「不保證」會怎樣,但標準比這精確得多。它把語言沒有釘死的行為分成「三個」不同的桶子,而把它們搞混是學習者最常犯的錯誤之一。想像三扇門。第一扇門後,規則一片沉默,什麼都可能發生。第二扇門後,有幾種被允許的結果,而你就是不知道會拿到哪一個。第三扇門後,你那台特定的編譯器做了固定的選擇並寫了下來。
第一扇門是未定義行為:標準完全不施加任何要求,編譯器可以假設它永不發生(解參考空指標、有號數溢位)。第二扇門是未指定行為(unspecified behavior):標準提供兩種以上被允許的可能,但不要求實作記載它選了哪個,甚至可以每次出現都不同。經典例子是函式引數的求值順序——在 f(g(), h()) 中,標準沒說 g() 或 h() 誰先跑,但兩者「都會」跑,程式仍然是良好定義的。第三扇門是實作定義行為(implementation-defined behavior):和未指定一樣是在允許的可能中做選擇,但實作「必須」記載它的選擇並一以貫之。純 char 是有號或無號、int 有幾個位元,都是實作定義——你編譯器的手冊會告訴你答案。
為什麼重要:這個差別就是「可攜性顧慮」與「災難」之間的差別。倚賴未指定或實作定義行為的程式碼不可攜——它在別處可能表現不同,但在每個平台上仍有「某種」已定義行為,你只要不去依賴那個選擇就能寫出可攜的程式。帶有未定義行為的程式碼則是到處都壞掉,沒有例外,因為根本沒有任何行為可依賴。所以當你讀到「結果是實作定義」時,該想的是「我不該假設某個特定值」;當你讀到「未定義」時,該想的是「我絕不能讓這件事發生,永遠不行」。
/* 未指定:a() 與 b() 誰先跑並未固定 */ int r = a() + b(); /* 實作定義:純 char 是有號嗎?由編譯器決定並記載 */ char c = 200; /* 值取決於你編譯器記載的選擇 */
兩行都不可攜但良好定義。兩者都不是未定義行為——它們在每個平台上仍有某種已定義結果。
三者要分清楚。實作定義:有記載的選擇。未指定:被允許但未記載、且可能變動的選擇。未定義:完全沒有要求,什麼都可能發生。只有第三種是必須消除的錯誤,前兩者只是可攜性的提醒。