C/C++ 语言中表达式的求值
作者:裘宗燕??? 北京大學(xué)數(shù)學(xué)學(xué)院信息科學(xué)系??? 本文基本內(nèi)容發(fā)表于《編程高手》雜志 2004 年第 12 期
經(jīng)常可以在一些討論組里看到下面的提問:“誰知道下面 C 語句給 n 賦什么值?”
? m = 1; n = m+++m++;
最近有位不相識(shí)的朋友發(fā) email 給我,問為什么在某個(gè) C++系統(tǒng)里,下面表達(dá)式打印出兩個(gè)4,而不是 4 和 5:
? a = 4; cout << a++ << a;
C++? 不是規(guī)定 <<? 操作左結(jié)合嗎?是 C++? 書上寫錯(cuò)了,還是這個(gè)系統(tǒng)的實(shí)現(xiàn)有問題??要弄清這些,需要理解的一個(gè)問題是:如果程序里某處修改了一個(gè)變量(通過賦值、增量/減量操作等),什么時(shí)候從該變量能夠取到新值?有人可能說,“這算什么問題!我修改了變量,再?gòu)倪@個(gè)變量取值,取到的當(dāng)然是修改后的值!”其實(shí)事情并不這么簡(jiǎn)單。
C/C++? 語言是“基于表達(dá)式的語言”,所有計(jì)算(包括賦值)都在表達(dá)式里完成。“x = 1;”就是表達(dá)式“x = 1”后加表示語句結(jié)束的分號(hào)。要弄清程序的意義,首先要理解表達(dá)式的意義,也就是:1)表達(dá)式所確定的計(jì)算過程;2)它對(duì)環(huán)境(可以把環(huán)境看作當(dāng)時(shí)可用的所有變量)的影響。如果一個(gè)表達(dá)式(或子表達(dá)式)只計(jì)算出值而不改變環(huán)境,我們就說它是引用透明的,這種表達(dá)式早算晚算對(duì)其他計(jì)算沒有影響(不改變計(jì)算的環(huán)境。當(dāng)然,它的值可能受到其他計(jì)算的影響)。如果一個(gè)表達(dá)式不僅算出一個(gè)值,還修改了環(huán)境,就說這個(gè)表達(dá)式有副作用(因?yàn)樗嘧隽祟~外的事)。a++? 就是有副作用的表達(dá)式。這些說法也適用于其他語言里的類似問題。
?
現(xiàn)在問題變成:如果 C/C++? 程序里的某個(gè)表達(dá)式(部分)有副作用,這種副作用何時(shí)才能實(shí)際體現(xiàn)到使用中?為使問題更清楚,我們假定程序里有代碼片段“...a[i]++ ... a[j] ...”,假定當(dāng)時(shí) i與 j 的值恰好相等(a[i] 和 a[j]? 正好引用同一數(shù)組元素) ;假定 a[i]++? 確實(shí)在 a[j] 之前計(jì)算;再假定其間沒有其他修改 a[i]? 的動(dòng)作。在這些假定下,a[i]++? 對(duì) a[i] 的修改能反映到 a[j] 的求值中嗎?注意:由于 i? 與 j? 相等的問題無法靜態(tài)判定,在目標(biāo)代碼里,這兩個(gè)數(shù)組元素訪問(對(duì)內(nèi)存的訪問)必然通過兩段獨(dú)立代碼完成。現(xiàn)代計(jì)算機(jī)的計(jì)算都在寄存器里做,問題現(xiàn)在變成:在取 a[j]? 值的代碼執(zhí)行之前,a[i]? 更新的值是否已經(jīng)被(從寄存器)保存到內(nèi)存?如果了解語言在這方面的規(guī)定,這個(gè)問題的答案就清楚了。
程序語言通常都規(guī)定了執(zhí)行中變量修改的最晚實(shí)現(xiàn)時(shí)刻(稱為順序點(diǎn)、序點(diǎn)或執(zhí)行點(diǎn))。程序執(zhí)行中存在一系列順序點(diǎn)(時(shí)刻),語言保證一旦執(zhí)行到達(dá)一個(gè)順序點(diǎn),在此之前發(fā)生的所有修改(副作用)都必須實(shí)現(xiàn)(必須反應(yīng)到隨后對(duì)同一存儲(chǔ)位置的訪問中) ,在此之后的所有修改都還沒有發(fā)生。在順序點(diǎn)之間則沒有任何保證。對(duì) C/C++ 語言這類允許表達(dá)式有副作用的語言,順序點(diǎn)的概念特別重要。
?
現(xiàn)在上面問題的回答已經(jīng)很清楚了:如果在 a[i]++? 和 a[j]? 之間存在一個(gè)順序點(diǎn),那么就能保證 a[j]? 將取得修改之后的值;否則就不能保證。
C/C++語言定義(語言的參考手冊(cè))明確定義了順序點(diǎn)的概念。順序點(diǎn)位于:
1.? 每個(gè)完整表達(dá)式結(jié)束時(shí)。完整表達(dá)式包括變量初始化表達(dá)式,表達(dá)式語句,return 語句的表達(dá)式,以及條件、循環(huán)和 switch 語句的控制表達(dá)式(for 頭部有三個(gè)控制表達(dá)式);?
2.? 運(yùn)算符 &&、||、?:? 和逗號(hào)運(yùn)算符的第一個(gè)運(yùn)算對(duì)象計(jì)算之后;
3.? 函數(shù)調(diào)用中對(duì)所有實(shí)際參數(shù)和函數(shù)名表達(dá)式(需要調(diào)用的函數(shù)也可能通過表達(dá)式描述)的求值完成之后(進(jìn)入函數(shù)體之前)。
?
假設(shè)時(shí)刻ti和ti+1是前后相繼的兩個(gè)順序點(diǎn),到了ti+1,任何C/C++? 系統(tǒng)(VC、BC等都是C/C++系統(tǒng))都必須實(shí)現(xiàn)ti之后發(fā)生的所有副作用。當(dāng)然它們也可以不等到時(shí)刻ti+1,完全可以選擇在時(shí)段 [t, ti+1]? 之間的任何時(shí)刻實(shí)現(xiàn)在此期間出現(xiàn)的副作用,因?yàn)镃/C++? 語言允許這些選擇。
前面討論中假定了 a[i]++? 在 a[i](這塊應(yīng)該是裘老的小筆誤吧?a[i]->a[j])之前做。在一個(gè)程序片段里 a[i]++? 究竟是否先做,還與它所在的表達(dá)式確定的計(jì)算過程有關(guān)。我們都熟悉 C/C++? 語言有關(guān)優(yōu)先級(jí)、結(jié)合性和括號(hào)的規(guī)定,而出現(xiàn)多個(gè)運(yùn)算對(duì)象時(shí)的計(jì)算順序卻常常被人們忽略。看下面例子:
? (a + b) * (c + d)? fun(a++, b, a+5)
這里“*”的兩個(gè)運(yùn)算對(duì)象中哪個(gè)先算?fun 及其三個(gè)參數(shù)按什么順序計(jì)算?對(duì)第一個(gè)表達(dá)式,采用任何計(jì)算順序都沒關(guān)系,因?yàn)槠渲械淖颖磉_(dá)式都是引用透明的。第二個(gè)例子里的實(shí)參表達(dá)式出現(xiàn)了副作用,計(jì)算順序就非常重要了。少數(shù)語言明確規(guī)定了運(yùn)算對(duì)象的計(jì)算順序(Java 規(guī)定從左到右),C/C++? 則有意不予規(guī)定,既沒有規(guī)定大多數(shù)二元運(yùn)算的兩個(gè)對(duì)象的計(jì)算順序(除了“&&”、“||” 和“,”),也沒有規(guī)定函數(shù)參數(shù)和被調(diào)函數(shù)的計(jì)算順序。在計(jì)算第二個(gè)表達(dá)式時(shí),首先按照某種順序算 fun、a++、b 和a+5,之后是順序點(diǎn),而后進(jìn)入函數(shù)執(zhí)行。
不少書籍在這些問題上有錯(cuò)(包括一些很流行的書)。例如說C/C++? 先算左邊(或右邊),或者說某個(gè) C/C++? 系統(tǒng)先計(jì)算某一邊。這些說法都是錯(cuò)誤的!一個(gè) C/C++ 系統(tǒng)可以永遠(yuǎn)先算左邊或永遠(yuǎn)先算右邊,也可以有時(shí)先算左邊有時(shí)先算右邊,或在同一表達(dá)式里有時(shí)先算左邊有時(shí)先算右邊。不同系統(tǒng)可能采用不同的順序(因?yàn)槎挤险Z言標(biāo)準(zhǔn));同一系統(tǒng)的不同版本完全可以采用不同方式;同一版本在不同優(yōu)化方式下,在不同位置都可能采用不同順序。因?yàn)檫@些做法都符合語言規(guī)范。在這里還要注意順序點(diǎn)的問題:即使某一邊的表達(dá)式先算了,其副作用也可能沒有反映到內(nèi)存,因此對(duì)另一邊的計(jì)算沒有影響。
回到前面的例子:“誰知道下面 C 語句給 n 賦什么值?”
? m = 1; n = m++ +m++;
正確回答是:不知道!語言沒有規(guī)定它應(yīng)該算出什么,結(jié)果完全依賴具體系統(tǒng)在具體上下文中的具體處理。其中牽涉到運(yùn)算對(duì)象的求值順序和變量修改的實(shí)現(xiàn)時(shí)刻問題。對(duì)于:
? cout << a++ << a;
我們知道它是
(cout.operator <<(a++)).operator << (a);
?的簡(jiǎn)寫。先看外層函數(shù)調(diào)用,這里需要算出所用函數(shù)(由加下劃線的一段得到),還需要計(jì)算 a 的值。語言沒有規(guī)定哪個(gè)先算。如果真的先算函數(shù),這一計(jì)算中出現(xiàn)了另一次函數(shù)調(diào)用,在被調(diào)函數(shù)體執(zhí)行前有一個(gè)順序點(diǎn),那時(shí) a++的副作用就會(huì)實(shí)現(xiàn)。如果是先算參數(shù),求出 a的值 4,而后計(jì)算函數(shù)時(shí)的副作用當(dāng)然不會(huì)改變它(這種情況下輸出兩個(gè) 4)。當(dāng)然,這些只是假設(shè),實(shí)際應(yīng)該說的是:這種東西根本不該寫,討論其效果沒有意義。
有人可能說,為什么人們?cè)O(shè)計(jì) C/C++時(shí)不把順序規(guī)定清楚,免去這些麻煩?C/C++ 語言的做法完全是有意而為,其目的就是允許編譯器采用任何求值順序,使編譯器在優(yōu)化中可以根據(jù)需要調(diào)整實(shí)現(xiàn)表達(dá)式求值的指令序列,以得到效率更高的代碼。像 Java 那樣嚴(yán)格規(guī)定表達(dá)式的求值順序和效果,不僅限制了語言的實(shí)現(xiàn)方式,還要求更頻繁的內(nèi)存訪問(以實(shí)現(xiàn)副作用),這些可能帶來可觀的效率損失。應(yīng)該說,在這個(gè)問題上,C/C++和 Java 的選擇都貫徹了它們各自的設(shè)計(jì)原則,各有所獲(C/C++? 潛在的效率,Java 更清晰的程序行為),當(dāng)然也都有所失。還應(yīng)該指出,大部分程序設(shè)計(jì)語言實(shí)際上都采用了類似 C/C++的規(guī)定。
討論了這么多,應(yīng)該得到什么結(jié)論呢?C/C++? 語言的規(guī)定告訴我們,任何依賴于特定計(jì)算順序、依賴于在順序點(diǎn)之間實(shí)現(xiàn)修改效果的表達(dá)式,其結(jié)果都沒有保證。程序設(shè)計(jì)中應(yīng)該貫徹的規(guī)則是:如果在任何“完整表達(dá)式”(形成一段由順序點(diǎn)結(jié)束的計(jì)算)里存在對(duì)同一“變量”的多個(gè)引用,那么表達(dá)式里就不應(yīng)該出現(xiàn)對(duì)這一“變量”的副作用。否則就不能保證得到預(yù)期結(jié)果。注意:這里的問題不是在某個(gè)系統(tǒng)里試一試的問題,因?yàn)槲覀儾豢赡茉囼?yàn)所有可能的表達(dá)式組合形式以及所有可能的上下文。這里討論的是語言,而不是某個(gè)實(shí)現(xiàn)。總而言之,絕不要寫這種表達(dá)式,否則我們或早或晚會(huì)在某種環(huán)境中遇到麻煩。?
后記:去年參加一個(gè)學(xué)術(shù)會(huì)議,看到有同行寫文章討論某個(gè) C 系統(tǒng)里表達(dá)式究竟按什么順序求值,并總結(jié)出一些“規(guī)律”。從討論中了解到某“程序員水平考試”出了這類題目。這使我感到很不安。今年給一個(gè)教師學(xué)習(xí)班講課,發(fā)現(xiàn)許多專業(yè)課教師也對(duì)這一基本問題也不甚明了,更覺得問題確實(shí)嚴(yán)重。因此整理出這篇短文供大家參考。
后后記:4 年多過去了,許多新的和老的教科書仍然在不厭其煩地討論在 C 語言里原本并無意義的問題(如本文所指出的)。希望學(xué)習(xí)和使用 C 語言的人不要陷入其中。——2009.2
?
《新程序員》:云原生和全面數(shù)字化實(shí)踐50位技術(shù)專家共同創(chuàng)作,文字、視頻、音頻交互閱讀總結(jié)
以上是生活随笔為你收集整理的C/C++ 语言中表达式的求值的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Discuz!NT发帖回复后没有积分动画
- 下一篇: Dynamic LAN-to-LAN ×