表達式求值順序不同於運算結合性和優先級。下面是一個經典例子,被 ISO C99/ C 98 /03 三大標准明確提到:他的結果是不確定(unspecified) 的。 更加可怕的是,如果i 是一個內建類型,並在下一個順序點之前被改寫超過一次,那麼結果是未定義(undefined)的!比如本例中如果有: int i = 0x1000fffe; 你也許會認為他的結果是加1 或者加2,其實更糟糕 —— 結果可能是 0x1001ffff 。他的高字節接受了一個副作用的內容,而低字節則接受了另一個副作用的內容! 如果i 是指針,那麼將很容易造成程序崩潰。 為什麼要這麼做呢?因為對於編譯器提供商來說,未確定的順序對優化有相當重要的作用。比如,一個常見的優化策略是“減少寄存器占用和臨時對象”。編譯器可以重新組織表達式的求值,以便盡量不使用額外的寄存器以及臨時變量。 更加嚴格的說,即使是編譯器提供商也無法完全徹底序列化指令(比如無法嚴格規定讀和寫的順序),因為CPU本身有權利修改指令順序,以便達到更高的速度。 下面的術語以 ISO C99 和 C 03為准。譯名為參考並附帶原術語對照,如有解釋不當或者錯誤望指正。 -------------------------------------------------------------------------------- 表達式有兩種功能。每個表達式都產生一個值( value ),同時可能包含副作用( side effect ),比如:他可能修改某些值。 規則的核心在於 順序點( sequence point ) [ C99 6.5 Expressions 條款2 ] [ C 03 5 Expressions 概述 條款4 ]。 這是一個結算點,語言要求這一側的求值和副作用(除了臨時對象的銷毀以外)全部完成,才能進入下面的部分。 C/C 中大部分表達式都沒有順序點,只有下面五種表達式有: 1 函數。函數調用之前有一個求值順序點。 2 && || 和 ?: 這三個包含邏輯的表達式。其左側邏輯完成後有一個求值順序點。 3 逗號表達式。逗號左側有一個求值順序點。 注意,他們都只有一個求值順序點,2和3的右側運算結束後並沒有求值順序點。 在兩個順序點之間,子表達式求值和副作用的順序是不確定的。假如代碼的結果與求值和副作用發生順序相關,我們稱這樣的代碼有不確定的行為 (unspecified behavior)。 而且,假如期間對一個內建類型執行一次以上的寫操作,則是未定義行為(undefined behavior)——我們知道,未定義行為帶來最好的後果是讓你的程序立即崩掉。 n = n ; // 兩個副作用,對於內建對象產生是未定義行為 n = f1() f2() f3(); // f1 f2 f3 調用順序任意 printf("%d",--a b,--b a); // --a b 和 --b a 這兩個子表達式,求值順序不確定 這個有四個表達式求值,同時每個表達式都有負作用。這八個操作順序是任意的,那麼結果如何?未定義。 請用 VC7.1 Debug和 Release 分別測試這同一份代碼,結果是不同的: 0 0 0 0 [release] 事實上,鑒於前面的討論,如果換一些其他初始值,這裡甚至會出現錯位而得到千奇百怪的詭異結果。 再看看C/C 標准中的其他經典例子: [C99] 6.5.2.2 Function call (*pf[f1()]) ( f2(), f3() f4() ) 函數 f1 f2 f3 和f4 可能以任何順序被調用。 但是,所有副作用都必須在那個 pf[ f1() ] 返回的函數指針產生的調用前完成。 [C 03] 5 Expressions 概論4 i = v[i ]; // the behavior is unspecified i = i 1; // the behavior is unspecified -------------------------------------------------------------------------------- More Effective C 告誡我們, 千萬不要重載 &&, || 和, 操作符[ MEC ,條款7 ]。為什麼? 以逗號操作符為例,每個逗號左側有一個求值順序點。假如ar是一個普通的對象,下面的做法是無歧義的: 但是,如果ar[ i ] 返回一個 class A 對象或引用,而它重載了 operator, 那麼結果不妙了。那麼,上面的語句實際上是一個函數調用: C/C 中,函數只在調用前有一個求值順序點。所以 ar[i] 和 i 的求值、以及 i 副作用的順序是任意的。這會引起混亂。 更可怕的是,重載 && 和 || 。 大家已經習慣了其速死算法: 如果左側求值已經決定了最終結果,則右側不會被求值。而且大家很依賴這個行為,比如是C風格字符串拷貝常常這樣寫: 假如p 為 0, 那麼 *p 的行為是未定義的,可能令程序崩潰。 而 && 的求值順序避免了這一點。 但是,如果我們重載 && 就等於下面的做法:
i = i 1; // The behavior is unspecified
在介紹概念之前,我們先解釋一下它的結果。這個表達式( expression )包含3個子表達式( subexpression ):
e1 = i
e2 = e1 1
i = e2
這三個子表達式都沒有順序點( sequence point ),而 i 和 i = e3 都是有副作用( side effect )的表達式。由於沒有順序點,語言不保證這兩個副作用的順序。
i = i 1; // The result is undefined!!
幾乎所有表達式,求值順序都不確定。比如,下面的加法, f1 f2 f3的調用順序是任意的:
而函數也只在實際調用前有一個求值順序點。所以,常見於早期 C 語言教材的這類題目,是錯題:
天啊,甚至可能出現未定義行為?那麼堅決不寫與實現相關的代碼是最好的對策。即使是不確定行為(比如函數調用時) 只要沒有順序點編譯器怎麼做方便就怎麼做。 有些人認為函數調用參數求值與入棧順序相關,這是一種誤導。這個東西要解釋,無異於事後諸葛亮:
void f( int i1, int i2, int i3, int i4 ){
cout<< i1 << << i2 << << i3 << << i4 << endl;
}
int main(){
int i = 0;
f( i , i , i , i );
}
3 2 1 0 [debug]
條款12 EXAMPLE 在下面的函數調用中:
i = 7, i , i ; // i becomes 9 ( 譯注: 賦值表達式比逗號表達式優先級高 )
i = i 1; // the value of i is incremented
ar[ i ], i ;
ar[ i ].operator, ( i );
while( p && *p )
*pd = *p ;
exp1 .operator && ( exp2 )
現在不僅僅是求值混亂了。無論exp1是什麼結果,exp2 必然會被求值。