淺談C/C++ 語言中的表達式求值
經(jīng)??梢栽谝恍┯懻摻M里看到下面的提問:“誰知道下面C語句給n賦什么值?”
m = 1; n = m+++m++;
最近有位不相識的朋友發(fā)email給我,問為什么在某個C++系統(tǒng)里,下面表達式打印出兩個4,而不是4和5:
a = 4; cout << a++ << a;
C++ 不是規(guī)定 << 操作左結(jié)合嗎?是C++ 書上寫錯了,還是這個系統(tǒng)的實現(xiàn)有問題?
注:運行a = 4; cout << a++ << a;
如在Visual c++ 6.0中,得到的是4和4;在Visual Studio中,得到的是4和5.
到底哪個是對的呢?請詳看后面的分析!
要弄清這些,需要理解的一個問題是:如果程序里某處修改了一個變量(通過賦值、增量/減量操作等),什么時候從該變量能夠取到新值?有人可能說,“這算什么問題!我修改了變量,再從這個變量取值,取到的當然是修改后的值!”其實事情并不這么簡單。
C/C++ 語言是“基于表達式的語言”,所有計算(包括賦值)都在表達式里完成?!皒 = 1;”就是表達式 “x = 1”后加表示語句結(jié)束的分號。要弄清程序的意義,首先要理解表達式的意義,也就是:1)表達式所確定的計算過程;2)它對環(huán)境(可以把環(huán)境看作 當時可用的所有變量)的影響。如果一個表達式(或子表達式)只計算出值而不改變環(huán)境,我們就說它是引用透明的,這種表達式早算晚算對其他計算沒有影響(不 改變計算的環(huán)境。當然,它的值可能受到其他計算的影響)。如果一個表達式不僅算出一個值,還修改了環(huán)境,就說這個表達式有副作用(因為它多做了額外的事)。a++ 就是有副作用的表達式。這些說法也適用于其他語言里的類似問題。
現(xiàn)在問題變成:如果C/C++ 程序里的某個表達式(部分)有副作用,這種副作用何時才能實際體現(xiàn)到使用中?為使問題更清楚,我們假定程序里有代碼片段 “...a[i]++ ... a[j] ...”,假定當時i與j的值恰好相等(a[i] 和a[j] 正好引用同一數(shù)組元素);假定a[i]++ 確 實在a[j] 之前計算;再假定其間沒有其他修改a[i] 的動作。在這些假定下,a[i]++ 對 a[i] 的修改能反映到 a[j] 的求值中嗎? 注意:由于 i 與 j 相等的問題無法靜態(tài)判定,在目標代碼里,這兩個數(shù)組元素訪問(對內(nèi)存的訪問)必然通過兩段獨立代碼完成。現(xiàn)代計算機的計算都在寄 存器里做,問題現(xiàn)在變成:在取 a[j] 值的代碼執(zhí)行之前,a[i] 更新的值是否已經(jīng)被(從寄存器)保存到內(nèi)存?如果了解語言在這方面的規(guī)定,這個問 題的答案就清楚了。
程序語言通常都規(guī)定了執(zhí)行中變量修改的最晚實現(xiàn)時刻(稱為順序點、序點或執(zhí)行點)。程序執(zhí)行中存在一系列順序點
(時 刻),語言保證一旦執(zhí)行到達一個順序點,在此之前發(fā)生的所有修改(副作用)都必須實現(xiàn)(必須反應到隨后對同一存儲位置的訪問中),在此之后的所有修改都還 沒有發(fā)生。在順序點之間則沒有任何保證。對C/C++ 語言這類允許表達式有副作用的語言,順序點的概念特別重要。
現(xiàn)在上面問題的回答已經(jīng)很清楚了:如果在a[i]++ 和a[j] 之間存在一個順序點,那么就能保證a[j] 將取得修改之后的值;否則就不能保證。
C/C++語言定義(語言的參考手冊)明確定義了順序點的概念。順序點位于:
1. 每個完整表達式結(jié)束時。完整表達式包括變量初始化表達式,表達式語句,return語句的表達式,以及條件、循環(huán)和switch語句的控制表達式(for頭部有三個控制表達式);
2. 運算符 &&、||、?: 和逗號運算符的第一個運算對象計算之后;
3. 函數(shù)調(diào)用中對所有實際參數(shù)和函數(shù)名表達式(需要調(diào)用的函數(shù)也可能通過表達式描述)的求值完成之后(進入函數(shù)體之前)。
假設時刻ti和ti+1是前后相繼的兩個順序點,到了ti+1,任何C/C++ 系統(tǒng)(VC、BC等都是C/C++系統(tǒng))都必須實現(xiàn)ti之后發(fā)生的所有副 作用。當然它們也可以不等到時刻ti+1,完全可以選擇在時段 [t, ti+1] 之間的任何時刻實現(xiàn)在此期間出現(xiàn)的副作用,因為C/C++ 語言允許 這些選擇。
前面討論中假定了a[i]++ 在a[i] 之前做。在一個程序片段里a[i]++ 究竟是否先做,還與它所在的表達式確定的計算過程有關。我們都熟悉C/C++ 語言有關優(yōu)先級、結(jié)合性和括號的規(guī)定,而出現(xiàn)多個運算對象時的計算順序卻常常被人們忽略。看下面例子:
(a + b) * (c + d) fun(a++, b, a+5)
這里“*”的兩個運算對象中哪個先算?fun及其三個參數(shù)按什么順序計算?對第一個表達式,采用任何計算順序都沒關系,因為其中的子表達式都是引用透明的。 第二個例子里的實參表達式出現(xiàn)了副作用,計算順序就非常重要了。少數(shù)語言明確規(guī)定了運算對象的計算順序(Java規(guī)定從左到右),C/C++ 則有意不予 規(guī)定,既沒有規(guī)定大多數(shù)二元運算的兩個對象的計算順序(除了&&、|| 和 ,),也沒有規(guī)定函數(shù)參數(shù)和被調(diào)函數(shù)的計算順序。在計算第二 個表達式時,首先按照某種順序算fun、a++、b和a+5,之后是順序點,而后進入函數(shù)執(zhí)行。
不少書籍在這些問題上有錯(包括一些很流行的書)。例如說C/C++ 先算左邊(或右邊),或者說某個C/C++ 系統(tǒng)先計算某一邊。這些說法都是錯誤 的!一個C/C++ 系統(tǒng)可以永遠先算左邊或永遠先算右邊,也可以有時先算左邊有時先算右邊,或在同一表達式里有時先算左邊有時先算右邊。不同系統(tǒng)可能采 用不同的順序(因為都符合語言標準);同一系統(tǒng)的不同版本完全可以采用不同方式;同一版本在不同優(yōu)化方式下,在不同位置都可能采用不同順序。因為這些做法 都符合語言規(guī)范。在這里還要注意順序點的問題:即使某一邊的表達式先算了,其副作用也可能沒有反映到內(nèi)存,因此對另一邊的計算沒有影響。
回到前面的例子:“誰知道下面C語句給n賦什么值?”
m = 1; n = m++ +m++;
正確回答是:不知道!語言沒有規(guī)定它應該算出什么,結(jié)果完全依賴具體系統(tǒng)在具體上下文中的具體處理。其中牽涉到運算對象的求值順序和變量修改的實現(xiàn)時刻問題。對于:
cout << a++ << a;
我們知道它是
(cout.operator <<(a++)).operator << (a);
的簡寫。先看外層函數(shù)調(diào)用,這里需要算出所用函數(shù),還需要計算a的值。語言沒有規(guī)定哪個先算。如果真的先算函數(shù),這一計算中出現(xiàn)了另一次函數(shù)調(diào)用,在被調(diào) 函數(shù)體執(zhí)行前有一個順序點,那時a++的副作用就會實現(xiàn)。如果是先算參數(shù),求出a的值
4,而后計算函數(shù)時的副作用當然不會改變它(這種情況下輸出兩個 4)。當然,這些只是假設,實際應該說的是:這種東西根本不該寫,討論其效果沒有意義。
有人可能說,為什么人們設計 C/C++時不把順序規(guī)定清楚,免去這些麻煩?C/C++ 語言的做法完全是有意而為,其目的就是允
許編譯器采用任何求值順序,使編譯器在優(yōu)化中可以根據(jù)需要調(diào)整實現(xiàn)表達式求值的指令序列,以得到效率更高的代碼。
像 Java那樣嚴格規(guī)定表達式的求值順序和效果,不僅限制了語言的實現(xiàn)方式,還要求更頻繁的內(nèi)存訪問(以實現(xiàn)副作用),這些可能帶來可觀的效率損失。應該 說,在這個問題上,C/C++和Java的選擇都貫徹了它們各自的設計原則,各有所獲(C/C++ 潛在的效率,Java更清晰的程序行為),當然也都有 所失。還應該指出,大部分程序設計語言實際上都采用了類似C/C++的規(guī)定。
討論了這么多,應該得到什么結(jié)論呢?C/C++ 語言的規(guī)定告訴我們,任何依賴于特定計算順序、依賴于在順序點之間實現(xiàn)修改效果的表達式,其結(jié)果都沒有保證。程序設計中應該貫徹的規(guī)則是:如果在任何“完整表達式”(形成一段由順序點結(jié)束的計算)里存在對同一“變量”的多個引用,那么表達式里就不應該出現(xiàn)對這一“變量”的副作用。否則就不能保證得到預期結(jié)果。注意:這里的問題不是在某個系統(tǒng)里試一試的問題,因為我們不可能試驗所有可能的表達式組合形式以及所有可能的上下文。這里討論的是語言,而不是某個實現(xiàn)。總而言之,絕不要寫這種表達式,否則我們或早或晚會某種環(huán)境中遇到麻煩。
后記:去年參加一個學術會議,看到有同行寫文章討論某個C系統(tǒng)里表達式究竟按什么順序求值,并總結(jié)出一些“規(guī)律”。從討論中了解到某“程序員水平考試”出 了這類題目。這使我感到很不安。今年給一個教師學習班講課,發(fā)現(xiàn)許多專業(yè)課教師也對這一基本問題也不甚明了,更覺得問題確實嚴重。因此整理出這篇短文供大 家參考。
以上這篇淺談C/C++ 語言中的表達式求值就是小編分享給大家的全部內(nèi)容了,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
C++實現(xiàn)LeetCode(6.字型轉(zhuǎn)換字符串)
這篇文章主要介紹了C++實現(xiàn)LeetCode(6.字型轉(zhuǎn)換字符串),本篇文章通過簡要的案例,講解了該項技術的了解與使用,以下就是詳細內(nèi)容,需要的朋友可以參考下2021-07-07簡單比較C語言中的execl()函數(shù)與execlp()函數(shù)
這篇文章主要介紹了C語言中的execl()函數(shù)與execlp()函數(shù)的簡單比較,是C語言入門學習中的基礎知識,需要的朋友可以參考下2015-08-08C++非遞歸隊列實現(xiàn)二叉樹的廣度優(yōu)先遍歷
這篇文章主要介紹了C++非遞歸隊列實現(xiàn)二叉樹的廣度優(yōu)先遍歷,實例分析了遍歷二叉樹相關算法技巧,并附帶了兩個相關算法實例,需要的朋友可以參考下2015-07-07C/C++?Qt?數(shù)據(jù)庫與ComBox實現(xiàn)多級聯(lián)動示例代碼
Qt中的SQL數(shù)據(jù)庫組件可以與ComBox組件形成多級聯(lián)動效果,在日常開發(fā)中多級聯(lián)動效果應用非常廣泛,今天給大家分享二級ComBox菜單如何與數(shù)據(jù)庫形成聯(lián)動,本文通過實例代碼給大家介紹的非常詳細,需要的朋友參考下吧2021-12-12