程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> C語言中的序列點和副作用

C語言中的序列點和副作用

編輯:關於C語言

C 語言中,術語副作用(side effect)是指對數據對象或者文件的修改。例如,以下語句        var = 99;
的副作用是把 var 的值修改成 99。對表達式求值也可能產生副作用,例如:
        se = 100
對這個表達式求值所產生的副作用就是 se 的值被修改成 100。
   序列點(sequence point)是指程序運行中的一個特殊的時間點,在該點之前的所有副作用已經結束,並且後續的副作用還沒發生。
    C 語句結束標志——分號(;)是序列點。也就是說,C 語句中由賦值、自增或者自減等引起的副作用在分號之前必須結束。我們以後會說到一些包含序列點的運算符。任何完整表達式(full expression)運算結束的那個時間點也是序列點。所謂完整表達式,就是說這個表達式不是子表達式。而所謂的子表達式,則是指表達式中的表達式。例如:
        f = ++e % 3
這整個表達式就是一個完整表達式。這個表達式中的 ++e、3 和 ++e % 3 都是它的子表達式。
    有了序列點的概念,我們下面來分析一下一個很常見的錯誤:
        int x = 1, y;
        y = x++ + x++;
這裡 y = x++ + x++ 是完整表達式,而 x++ 是它的子表達式。這個完整表達式運算結束的那一點是一個序列點,int x = 1, y; 中的 ; 也是一個序列點。也就是說,x++ + x++ 位於兩個序列點之間。標准規定,在兩個序列點之間,一個對象所保存的值最多只能被修改一次。但是我們清楚可以看到,上面這個例子中,x 的值在兩個序列點之間被修改了兩次。這顯然是錯誤的!這段代碼在不同的編譯器上編譯可能會導致 y 的值有所不同。比較常見的結果是 y 的值最後被修改為 2 或者 3。在此,我不打算就這個問題作更深入的分析,各位只要記住這是錯誤的,別這麼用就可以了。有興趣的話,可以看看以下列出的相關資料。
C 語言標准對副作用和序列點的定義如下:
    Accessing a volatile object, modifying an object, modifying a file, or calling a function that does any of those operations are all side effects, which are changes in the state of the execution environment. Evaluation of an expression may produce side effects. At certain specified points in the execution sequence called sequence points, all side effects of previous evaluations shall be complete and no side effects of subsequent evaluations shall have taken place.
翻譯如下:
    訪問易變對象,修改對象或文件,或者調用包含這些操作的函數都是副作用,它們都會改變執行環境的狀態。計算表達式也會引起副作用。執行序列中某些特定的點被稱為序列點。在序列點上,該點之前所有運算的副作用都應該結束,並且後繼運算的副作用還沒發生。

----------------------------------------------------------------------------------------------
讓我們來看看下面的代碼:
int i=7; printf(”%d\n”, i++ * i++);
你認為會返回什麼?56?no。正確答案是返回 49?很多人會問為什麼?難道不該打印出56嗎?在ccfaq中有非常詳盡的解釋,根本原因在於c中的序列點。
請注意,盡管後綴自加和後綴自減操作符 ++ 和 — 在輸出其舊值之後才會執行運算,但這裡的“之後”常常被誤解。沒有任何保證確保自增或自減會在輸出變量原值之後和對表達式的其它部分進行計算之前立即進行。也不能保證變量的更新會在表達式 “完成” (按照 ANSI C 的術語, 在下一個”序列點”之前) 之前的某個時刻進行。本例中, 編譯器選擇使用變量的舊值相乘以後再對二者進行自增運算。只有到達一個序列點之後,自增運算才能保證真正被執行。
包含多個不確定的副作用的代碼的行為總是被認為未定義。(簡單而言, “多個不確定副作用”是指在同一個表達式中使用導致同一對象修改兩次或修改以後又被引用的自增,自減和賦值操作符的任何組合。這是一個粗略的定義。) 甚至都不要試圖探究這些東西在你的編譯器中是如何實現的 (這與許多 C 教科書上的弱智練習正好相反);正如 K&R 明智地指出,”如果你不知道它們在不同的機器上如何實現, 這樣的無知可能恰恰會有助於保護你”。
那麼,所謂的序列點是什麼意思呢?
序列點是一個時間點(在整個表達式全部計算完畢之後或在 ||、 &&、 ? : 或逗號 運算符處, 或在函數調用之前), 此刻塵埃落定,所有的副作用都已確保結束。 ANSI/ISO C 標准這樣描述:
在上一個和下一個序列點之間,一個對象所保存的值至多只能被表達式的計算修改一次。而且前一個值只能用於決定將要保存的值。
第二句話比較費解。它說在一個表達式中如果某個對象需要寫入, 則在同一表達式中對該對象的訪問應該只局限於直接用於計算將要寫入的值。這條規則有效地限制了只有能確保在修改之前才訪問變量的表達式為合法。
例如 i = i+1 合法,而 a[i] = i++ 則非法。為什麼這樣的代碼:a[i] = i++; 不能工作?子表達式 i++ 有一個副作用 — 它會改變 i 的值 — 由於 i 在同一表達式的其它地方被引用,這會導致無定義的結果,無從判斷該引用(左邊的 a[i] 中)是舊值還是新值。那麼,對於 a[i] = i++; 我們不知道 a[] 的哪一個分量會被改寫,但 i 的確會增加 1,對嗎?
不一定!如果一個表達式和程序變得未定義,則它的所有方面都會變成未定義。
為什麼&& 和 || 運算符可以產生序列點呢?這些運算符在此處有一個特殊的例外:如果左邊的子表達式決定最終結果 (即,真對於 || 和假對於 && ) ,則右邊的子表達式不會計算。因此,從左至右的計算可以確保,對逗號表達式也是如此。而且,所有這些運算符 (包括 ? : ) 都會引入一個額外的內部序列點。

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