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