C 程式設計/副作用和順序點
外觀
< C 程式設計
在 C 中,更普遍地說,在計算機科學中,如果函式或表示式修改了其範圍之外的狀態,或者與其呼叫函式或外部世界有可觀察的互動,則該函式或表示式被稱為具有副作用。按照慣例,返回值會影響呼叫函式,但這通常不被認為是副作用。
一些副作用是
- 修改全域性變數或靜態變數
- 修改函式引數
- 將資料寫入顯示器或檔案
- 讀取資料
- 呼叫其他具有副作用的函式
在存在副作用的情況下,程式的行為可能取決於歷史;也就是說,求值的順序很重要。瞭解和除錯具有副作用的函式需要了解上下文及其可能的歷史。[1][2]
順序點定義了計算機程式執行中的任何一點,在該點,保證所有先前求值的副作用都已執行,並且尚未執行後續求值的任何副作用。它們在 C 的引用中經常被提及,因為它們是確定表示式的有效性以及如果有效,其可能結果的核心概念。有時需要新增更多順序點來使表示式定義,並確保唯一的有效求值順序。
- 一個表示式的求值可以先於另一個表示式的求值,或者等效地,另一個表示式的求值後於第一個表示式的求值。
- 表示式的求值是不確定順序的,這意味著一個是先於另一個,但哪個是未指定的。
- 表示式的求值是無序的。
無序求值的執行可能重疊,如果它們共享狀態,則會導致災難性的未定義行為。這種情況可能出現在平行計算中,導致競爭條件。
考慮兩個函式f()和g()。在 C 中,+運算子沒有與順序點相關聯,因此在表示式f()+g()中,f()或g()有可能先執行。逗號運算子引入了順序點,因此在程式碼f(),g()中,求值的順序是定義的:首先呼叫f(),然後呼叫g()。
當在單個表示式中多次修改同一個變數時,順序點也會發揮作用。一個經常被引用的例子是 C 表示式i=i++,它明顯地將i分配給它之前的值,並遞增i。i的最終值是不明確的,因為根據表示式求值的順序,遞增可能發生在賦值之前、之後或與賦值交織在一起。特定語言的定義可能指定一種可能的行為,或者只是說行為是未定義的。在 C 中,求值此類表示式會導致未定義行為。[3]
在 C[4]中,順序點出現在以下位置。
- 在
&&(邏輯與)、||(邏輯或)(作為短路求值的一部分)和逗號運算子的左側和右側運算元的求值之間。例如,在表示式*p++ != 0 && *q++ != 0中,*p++ != 0子表示式的所有副作用在嘗試訪問q之前完成。 - 在三元“問號”運算子的第一個運算元和第二個或第三個運算元的求值之間。例如,在表示式
a = (*p++) ? (*p++) : 0中,在第一個*p++之後有一個順序點,這意味著它在執行第二個例項之前已經被遞增。 - 在完整表示式結束時。此類別包括表示式語句(例如賦值
a=b;)、return 語句、if、switch、while或do-while語句的控制表示式,以及for語句中的所有三個表示式。 - 在函式呼叫中,函式被進入之前。引數求值的順序沒有指定,但此順序點意味著所有引數的副作用在函式被進入之前都已完成。在表示式
f(i++) + g(j++) + h(k++)中,f被呼叫,引數為i的原始值,但在進入f的主體之前,i被遞增。類似地,在進入g和h之前,分別更新j和k。但是,沒有指定f()、g()、h()執行的順序,也沒有指定i、j、k遞增的順序。如果f的主體訪問變數j和k,它可能會發現這兩個變數都被遞增了,或者都沒有被遞增,或者只有一個變數被遞增。(函式呼叫f(a,b,c)不是逗號運算子的用法;a、b和c的求值順序是未指定的。) - 在函式返回時,返回值被複制到呼叫上下文之後。(此順序點僅在 C++ 標準中指定;它僅在 C 中隱式存在。)
- 在初始化程式結束時;例如,在宣告
int a = 5;中,在對5求值之後。 - 在每個宣告序列中的每個宣告符之間;例如,在
int x = a++, y = a++中,在對a++的兩次求值之間。(這不是逗號運算子的例子。) - 在與輸入/輸出格式說明符關聯的每個轉換之後。例如,在表示式
printf("foo %n %d", &a, 42)中,在%n被求值之後,並在列印42之前,有一個順序點。
- ↑ “函數語言程式設計研究主題” D. Turner 編著,Addison-Wesley,1990 年,第 17-42 頁。檢索自:Hughes, John, 為什麼函數語言程式設計很重要 (PDF)
- ↑ Collberg, CSc 520 程式語言原理,亞利桑那大學計算機科學系
- ↑ C99 規範的第 6.5 章 #2 條:“在先前和下一個順序點之間,物件在其儲存值最多被表示式求值修改一次。此外,僅訪問先前值以確定要儲存的值。”
- ↑ C99 規範的附錄 C 列出了可以假設順序點的情況。
- 問題 3.8 的 comp.lang.c 常見問題解答