程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> C/C++表達式求值順序優先級詳解

C/C++表達式求值順序優先級詳解

編輯:C++入門知識

表達式求值順序不同於運算結合性和優先級。下面是一個經典例子,被 ISO C99/ C 98 /03 三大標准明確提到:他的結果是不確定(unspecified) 的。
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 是一個內建類型,並在下一個順序點之前被改寫超過一次,那麼結果是未定義(undefined)的!比如本例中如果有:

int i = 0x1000fffe;
i = i 1;      // The result is undefined!!

   你也許會認為他的結果是加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 ;                               // 兩個副作用,對於內建對象產生是未定義行為
    幾乎所有表達式,求值順序都不確定。比如,下面的加法, f1 f2 f3的調用順序是任意的:

n = f1() f2() f3();            // f1 f2 f3 調用順序任意
    而函數也只在實際調用前有一個求值順序點。所以,常見於早期 C 語言教材的這類題目,是錯題:

printf("%d",--a b,--b a);    // --a b 和 --b a 這兩個子表達式,求值順序不確定
    天啊,甚至可能出現未定義行為?那麼堅決不寫與實現相關的代碼是最好的對策。即使是不確定行為(比如函數調用時) 只要沒有順序點編譯器怎麼做方便就怎麼做。 有些人認為函數調用參數求值與入棧順序相關,這是一種誤導。這個東西要解釋,無異於事後諸葛亮:
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 );
}

    這個有四個表達式求值,同時每個表達式都有負作用。這八個操作順序是任意的,那麼結果如何?未定義。

    請用 VC7.1 Debug和 Release 分別測試這同一份代碼,結果是不同的:

0 0 0 0    [release]
3 2 1 0    [debug]

    事實上,鑒於前面的討論,如果換一些其他初始值,這裡甚至會出現錯位而得到千奇百怪的詭異結果。

    再看看C/C 標准中的其他經典例子:

     [C99] 6.5.2.2 Function call
    條款12 EXAMPLE 在下面的函數調用中:

(*pf[f1()])  ( f2(), f3() f4() )

    函數 f1 f2 f3 和f4 可能以任何順序被調用。 但是,所有副作用都必須在那個 pf[ f1() ] 返回的函數指針產生的調用前完成。

    [C 03] 5 Expressions 概論4

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

--------------------------------------------------------------------------------

  More Effective C 告誡我們, 千萬不要重載 &&, || 和, 操作符[ MEC ,條款7 ]。為什麼?

    以逗號操作符為例,每個逗號左側有一個求值順序點。假如ar是一個普通的對象,下面的做法是無歧義的:
ar[ i ], i ;

    但是,如果ar[ i ] 返回一個 class A 對象或引用,而它重載了 operator,  那麼結果不妙了。那麼,上面的語句實際上是一個函數調用:
ar[ i ].operator, ( i );

    C/C 中,函數只在調用前有一個求值順序點。所以 ar[i] 和 i 的求值、以及 i 副作用的順序是任意的。這會引起混亂。

    更可怕的是,重載 && 和 || 。 大家已經習慣了其速死算法: 如果左側求值已經決定了最終結果,則右側不會被求值。而且大家很依賴這個行為,比如是C風格字符串拷貝常常這樣寫:
while( p && *p )
    *pd = *p ;

    假如p 為 0, 那麼 *p 的行為是未定義的,可能令程序崩潰。 而 && 的求值順序避免了這一點。 但是,如果我們重載 && 就等於下面的做法:
exp1 .operator && ( exp2 )
    現在不僅僅是求值混亂了。無論exp1是什麼結果,exp2 必然會被求值。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved