看完這些問題後,你還會說自己懂 C 語言麼?

xxmen發表於2015-10-12

這篇文章的目的是讓每個程式設計師(特別是 C 程式設計師)說:我真的不懂 C。我想要讓大家看到 C 語言的那些陰暗角落比我們想象中更近,甚至那些平常的程式碼中就包含著未定義的行為。

這篇文章設定了一系列的問題和答案。所有的例子都是從原始碼中單獨分離出來的。

1.

Q:這段程式碼正確嗎?是否會因為變數被定義了兩次而導致錯誤的出現?注意這是源於同一個原始碼檔案,而不是函式體或程式碼段的一部分。

A:是的,這段程式碼是正確的。第一行是臨時的定義直到編譯器處理了第二行的定義之後才成為正式的“定義”。

2.

Q: 這樣寫的結果是即使 x 是空指標 bar() 函式都會被呼叫,並且程式不會崩潰。這是否是優化器的錯誤,或者全部是正確的?

A: 全部都是正確的。如果 x 是空指標,未定義的行為出現在第 (1) 行, 沒有人欠程式設計師什麼,所以程式並不會在第 (1) 行崩潰, 也不會試圖在第 (2) 行返回假如已經成功執行第 (1) 行。讓我們來探討編譯器遵循的規則,它都按如下的方式進行。在對第 (1) 行的分析之後,編譯器認為 x 不會是一個空指標,於是第 (2) 行和 第 (3) 行就被認定為是沒用的程式碼。變數 y 被當做沒用的變數去除。從記憶體中讀取的操作也會被去除,因為 *x 並不符合易變型別(volatile)。

這就是無用的變數如何導致空指標檢查失效的例子。

3.有這樣一個函式:

有人想要按如下方式來優化它:

Q:呼叫原始的函式和呼叫優化後的函式,對於變數 zp 是否有可能獲得不同的結果?

A:這是可能的,當 yp == zp 時結果就不同。

4.

Q: 這個函式是否可能返回最大下界(inf) ?假設浮點數運算是按照IEEE 754 標準(大部分機器遵循)執行的, 並且斷言語句是可用的(NDEBUG 並沒有被定義)。

A:是的,這是可以的。通過傳入一個非規範化的 x 的值,比如 1e-309.

5.

Q: 上面提供的函式應該返回以空終止字元結尾的字串長度,找出其中存在的一個 bug 。

A: 使用 int 型別來儲存物件的大小是錯誤的,因為無法保證 int 型別能夠存下任何物件的大小,應該使用 size_t。

6.

Q: 這個迴圈是死迴圈。這是為什麼?

A: size_t 是無符號型別。 如果 i 是無符號型別, 那麼 i >= 0 永遠都是正確的。

7.

這個程式分別用兩個不同的編譯器編譯並且在一臺小位元組序的機器上執行。獲得瞭如下兩種不同的結果:

Q:你如何解釋第二種結果?

A:所給程式存在未定義的行為。程式違反了編譯器的強重疊規則(strict aliasing)。雖然 int 在第 (2) 行被改變了,但是編譯器可以假設任何的 long 都沒有改變。我們不能間接引用那些和其他不相容型別指標相重名的指標。這就是編譯器之所以可以傳遞和在第一行的執行過程中被讀取的相同的 long (第(3)行)的原因。

8.

Q: 這個程式碼是否是正確的?如果不存在未定義行為,那麼它會輸出什麼?

A: 是的, 這裡使用了逗號運算子。首先,逗號左邊的引數被計算後丟棄,然後,右邊的引數經過計算後被當做整個運算子的值使用,所以輸出是 10 2 10。

注意在函式呼叫中的逗號符號(比如 f(a(), b()))並不是逗號運算子,因此也就不會保證運算的順序,a() 和 b() 會以隨機的順序計算。

9.

Q: 函式 add(UINT_MAX, 1) 的結果是什麼?

A:對於無符號數的溢位結果是有定義的,結果是 2^(CHAR_BIT * sizeof(unsigned int)) ,所以函式 add 的結果是 0 。

10.

Q:函式 add(INT_MAX, 1) 的結果是什麼?

A:有符號整數的溢位結果是未定義的行為。

11.

Q:這裡是否可能出現未定義的行為?如果是的話,是在輸入什麼引數時發生的?

A:neg(INT_MIN)。如果 ECM 用補碼錶示負整數, 那麼 INT_MIN 的絕對值比 INT_MAX 的絕對值大一。在這種情況下,-INT_MIN 造成了有符號整數的溢位,這是一種未定義的行為。

12.

Q:這裡是否可能出現未定義的行為?如果是的話,是在什麼引數上發生的?

A:如果 ECM 用補碼錶示負數, 那麼 div(INT_MIN, -1) 導致了與上一個例子相同的問題。

相關文章