close

一切罪惡的開始

i = 10;
i = i++ + ++i;

請問 i 的值在上面C語言程式執行完後答案為多少?

 

相信可能很多人都見過這樣的題目,在台灣資訊相關的教育界,教到C/C++很多人都喜歡考這樣的題目,似乎這麼考好像很有深度,好像如果不這麼做不 能考出學生到底懂不懂C語言似的,當學生啃著筆想答案時,出題的老師似乎覺得自己出了一題相當有程度的題目而洋洋得意時,卻沒發現這題目背後卻藏著邪惡的 氣息,它到底有多邪惡? 我們先暫且不論它到底有多邪惡,先來想想看它的答案到底是多少

很直覺的答案

我們第一眼看到i++,很顯然地它是先回傳自身的值再加上1,因此此時我們把這式子看成這樣

i = 10 + ++i;

在算完前面的i++後,i++的加一動作發生了,因此在此刻 i = 11,接著看到右邊的 ++i,是先將自身的數值+1後回傳,右邊的結果應該是 11 + 1 = 12,所以這時式子被展開成這樣

i = 10 + 12

好了,這下簡單了,10 + 12 = 22,然後再指定給i,因此i = 22,答對了嗎?

答案是 : 錯!

答案是錯的,你可能會想到,何不用寫個小程式直接測試看看最準確,我們先寫一個小程式來跑跑看這樣程式的結果會是多少,我們這就寫一個小程式來試試看

#include 

int main(int argc, char **argv) {
    int i = 10;
    i = i++ + ++i;
    printf("The answer : %dn", i);
    system("pause");
    return 0;
}

Visual C++ 6.0下編譯跑出來的結果是

The answer : 23

令人意外對吧? 你以為這就是正確答案了對吧?

還是錯!

很可惜的是,連程式算出來的答案都是錯的,這或許令人覺得納悶,何以連程式算出來的結果都是錯的,正確的答案到底是多少? 很遺憾的是,這樣的題目並沒有答案,如果真的要給它一個答案,應該寫Undefined behavior,說得淺白一點,那樣的題目是錯的,錯誤的程式碼會有錯誤的結果,因此根本就不會有正確的答案,或許這樣的說法還是無法令人信服,那程式確實算出了一個數字,但是考慮一下別的編譯器如何? 很不幸的,市面上常見的編譯器經過我測試都會剛好有23的答案,或許找其它的編譯器會有不同的結果,但是為了能更容易的測試出這樣的問題,為此我們考慮一下更複雜、更邪惡、更噁心的例子,來進行測試

#include 

int main(int argc, char **argv) {
    int i = 3;
    int r = (i++*++i+i--*--i);
    printf("The answer : %dn", r);
    system("pause");
    return 0;
}

讓我們看看不同編譯器的Debug模式下執行的結果

Visual C++ 6.0

The answer : 25

Visual C++ 2008 Express

The answer : 18

MinGW(GCC)

The answer : 25

我們試試看在Release下執行的結果

Visual C++ 6.0

The answer : 18

Visual C++ 2008 Express

The answer : 18

MinGW(GCC)

The answer : 25

很奇怪對吧? 一模一樣的程式碼,在不同編譯器、不同編譯參數下可能會有不同的結果,為什麼會這樣?

未定義行為(Undefined behavior)

答案就是Undefined behavior,詳細的說明請看Wiki百科 – Undefined behavior,我們用白話一點的方式來說明,就是語言規格在定義時為了編譯器實做上的彈性和效率考量,會刻意不去規定某些規格,因此如果寫出來的程式依賴或著錯用某些沒有在規格內所規定的特性時,我們就稱之為Undefined behavior,而這樣的程式在不同的編譯器實作所編出來的程式執行結果可能會有所不同,即使這樣解釋還是一樣令人難以理解,後面的例子過於複雜,我們拿一開始的例子來進行說明

i = i++ + ++i;

問題就出在於,C語言沒有規定 i++ 或 ++i 的 加1 動作到底是在一個句子裡的哪個時刻執行,因此,不同編譯器如果在不同的位置 + 1,就有可能會有不同的結果,我們假設把上面的式子拆成幾個動作來看

  • i++ 產生一份暫時物件 (供參考用,否則+1後值就改變了)
  • i++ 的 i 遞增 1
  • ++i 的 i 遞增 1
  • (i++) 加上 (++i)
  • 將右邊運算的結果指派給i

直覺上 i++ 似乎會在i++被參考或產生暫時物件後進行加1,但是這只是直覺上是如此,也有可能是在更後面甚至是在 i = 右值 指定完之後才執行,答案就在於要看編譯器如何實作,因此不同的編譯器、編譯參數可能會有不同的答案就是如此,而這樣的程式的行為就叫做Undefined behavior

另一個更簡單易懂的例子

我最早遇見Undefined behavior,是當我同學傳一個程式給我,他說這個程式不是按照他想的執行,為什麼是反的順序? 他覺得相當奇怪,在經過我測試後發現不同編譯器會有不同結果的奇怪行為,才發現原來有Undefined behavior這回事,以下是該程式的類似範例

#include 

int changeNum(int *n) {
    ++*n;
    return*n;
}

int main(int argc, char **argv) {
    int n = 0;
    printf("%d %d %dn", changeNum(&n), changeNum(&n), changeNum(&n));
    system("pause");
    return 0;
}

你的直覺告訴你,答案應該是

1 2 3

但是其實是錯的,在VC、MinGW下跑出來都是3 2 1,因為和直覺不一樣所以令人覺得奇怪,深入探討後才知道原來有Undefined behavior在 搞鬼,因為參數被計算的順序是沒有定義的,我們直覺上認為,從左到右是很合理的順序,因此,從左到右呼叫changeNum應該是正確的,但是事實上C語 言並沒有規定哪個參數要先賦值,以VC和MinGW的實做,他們應該都是從右邊賦值到左邊,因此不管怎麼說都是錯誤的結果,你不能依賴這樣剛好算出來的結 果,試想一下你為一台噴射客機寫程式,你的程式依賴了這樣未定義的行為,但是飛機的電腦用了不同的編譯器,而有了不同的行為,我想你可能已經聽見了在你腦 中的暴炸聲響,因此不管如何,Undefined behavior都是應該被避免的

諷刺的是…

諷刺的是,這樣錯誤的程式碼,在台灣常被當做考題、教材來使用,而且廣範到不可至信的地步,原因可能就出在於台灣的教育喜歡刁鑽的題目,來顯示出題目的深度,但卻不知道這樣的題目是錯誤的,在我們見到的第一個例子

i = i++ + ++i;

出現在研究所的考題中,大學的考題中,或許還有其它更多考題是我沒看見的

(i++*++i+i--*--i)

你或許認為這是我寫出來用來測試的程式片段,很遺憾的是,這是我在教家教學生時,看見他們老師給的投影片上面所寫的血淋淋的真實例子

投影片片段:

C 語言-運算敘述
++, --運算子
main()
{ int a=1,b=1;
printf(“++a=%d,b++=%dn”,++
printf(“a=%d,b=%dn”,a,b);
printf(“--a=%d,b--=%dn”,--a,b--);
printf(“a=%d,b=%dn”,a,b);
}
 執行結果:
.. 考慮: i的初值為3,
表達式(i++*++i+i--*--i)=?

回到罪惡的原點

回到罪惡的源頭,我並不是一開始就了解這樣的事情,但是我卻不曾寫過這樣的程式碼,原因何在? 原因在於這樣的程式碼在實質開發中沒有任何的好處,只有數不清的壞處,從一個最簡單最基本的原則出發就可以避免這樣的錯誤

KISS (Keep it simple and stupid)

這樣的原則就是,盡可能的保持簡單,除了程式碼需要供人閱讀以外,簡單的程式也較不容易出錯,除了參加國際C語言混亂代碼大賽的比賽選手,和台灣為了刁鑽而出題目的老師們,有誰會寫出這種會讓老闆開除你的程式碼? 所以當你再次見到這樣的程式碼出現在考題或教材中,不妨把這篇文章的網址丟給那老師

參考資料

除了以上提到的未定義行為之外,還有很多常見的變形,C++規格書中有提到

C++ 規格書 第五章 Expressions

Except where noted, the order of evaluation of operands of individual operators and subexpressions of individual expressions, and the order in which side effects take place, is unspecified.53) Between the previous and next sequence point a scalar object shall have its stored value modified at most once by the evaluation of an expression. Furthermore, the prior value shall be accessed only to determine the value to be stored. The requirements of this paragraph shall be met for each allowable ordering of the subexpressions of a full expression; otherwise the behavior is undefined.

[Example:

i = v[i++]; // the behavior is unspecified
i = 7, i++, i++; // i becomes 9
i = ++i + 1; // the behavior is unspecified
i = i + 1; // the value of i is incremented

—end example]

PTT的C_AND_CPP版的 “C 語言新手十誡 第九項” 也有提到

九、你不可以在一個運算式(expression)中,對一個基本型態的變數修改其值
超過一次以上。否則,將導致未定義的行為(undefined behavior)。

錯誤例子:
int i = 7;
int j = ++i + i++;

正確例子:
int i = 7;
int j = ++i;
j += i++;

你也不可以在一個運算式(expression)中,對一個基本型態的變數修改其值,
而且還在同一個式子的其他地方為了其他目的而存取該變數的值。(其他目的,
是指不是為了計算這個變數的新值的目的)。否則,將導致未定義的行為。

錯誤例子:
int arr[5];
int i = 0;
arr[i] = i++;

正確例子:
int arr[5];
int i = 0;
arr[i] = i;
i++;

[C++程式]
錯誤例子:
int i = 10;
cout << i << “==” << i++;

正確例子:
int i = 10;
cout << i << “==”;
cout << i++;

除此之外,可以Google “side effect sequence point”來了解更多關於這方面的資料

 

 

 

轉至 : http://blog.ez2learn.com/2008/09/27/evil-undefined-behavior/

arrow
arrow
    全站熱搜

    Eric 發表在 痞客邦 留言(0) 人氣()