淺談C/C++ 語言中的表達(dá)式求值
經(jīng)??梢栽谝恍┯懻摻M里看到下面的提問:“誰知道下面C語句給n賦什么值?”
m = 1; n = m+++m++;
最近有位不相識的朋友發(fā)email給我,問為什么在某個C++系統(tǒng)里,下面表達(dá)式打印出兩個4,而不是4和5:
a = 4; cout << a++ << a;
C++ 不是規(guī)定 << 操作左結(jié)合嗎?是C++ 書上寫錯了,還是這個系統(tǒng)的實(shí)現(xiàn)有問題?
注:運(yùn)行a = 4; cout << a++ << a;
如在Visual c++ 6.0中,得到的是4和4;在Visual Studio中,得到的是4和5.
到底哪個是對的呢?請?jiān)斂春竺娴姆治觯?/span>
要弄清這些,需要理解的一個問題是:如果程序里某處修改了一個變量(通過賦值、增量/減量操作等),什么時候從該變量能夠取到新值?有人可能說,“這算什么問題!我修改了變量,再從這個變量取值,取到的當(dāng)然是修改后的值!”其實(shí)事情并不這么簡單。
C/C++ 語言是“基于表達(dá)式的語言”,所有計(jì)算(包括賦值)都在表達(dá)式里完成?!皒 = 1;”就是表達(dá)式 “x = 1”后加表示語句結(jié)束的分號。要弄清程序的意義,首先要理解表達(dá)式的意義,也就是:1)表達(dá)式所確定的計(jì)算過程;2)它對環(huán)境(可以把環(huán)境看作 當(dāng)時可用的所有變量)的影響。如果一個表達(dá)式(或子表達(dá)式)只計(jì)算出值而不改變環(huán)境,我們就說它是引用透明的,這種表達(dá)式早算晚算對其他計(jì)算沒有影響(不 改變計(jì)算的環(huán)境。當(dāng)然,它的值可能受到其他計(jì)算的影響)。如果一個表達(dá)式不僅算出一個值,還修改了環(huán)境,就說這個表達(dá)式有副作用(因?yàn)樗嘧隽祟~外的事)。a++ 就是有副作用的表達(dá)式。這些說法也適用于其他語言里的類似問題。
現(xiàn)在問題變成:如果C/C++ 程序里的某個表達(dá)式(部分)有副作用,這種副作用何時才能實(shí)際體現(xiàn)到使用中?為使問題更清楚,我們假定程序里有代碼片段 “...a[i]++ ... a[j] ...”,假定當(dāng)時i與j的值恰好相等(a[i] 和a[j] 正好引用同一數(shù)組元素);假定a[i]++ 確 實(shí)在a[j] 之前計(jì)算;再假定其間沒有其他修改a[i] 的動作。在這些假定下,a[i]++ 對 a[i] 的修改能反映到 a[j] 的求值中嗎? 注意:由于 i 與 j 相等的問題無法靜態(tài)判定,在目標(biāo)代碼里,這兩個數(shù)組元素訪問(對內(nèi)存的訪問)必然通過兩段獨(dú)立代碼完成?,F(xiàn)代計(jì)算機(jī)的計(jì)算都在寄 存器里做,問題現(xiàn)在變成:在取 a[j] 值的代碼執(zhí)行之前,a[i] 更新的值是否已經(jīng)被(從寄存器)保存到內(nèi)存?如果了解語言在這方面的規(guī)定,這個問 題的答案就清楚了。
程序語言通常都規(guī)定了執(zhí)行中變量修改的最晚實(shí)現(xiàn)時刻(稱為順序點(diǎn)、序點(diǎn)或執(zhí)行點(diǎn))。程序執(zhí)行中存在一系列順序點(diǎn)
(時 刻),語言保證一旦執(zhí)行到達(dá)一個順序點(diǎn),在此之前發(fā)生的所有修改(副作用)都必須實(shí)現(xiàn)(必須反應(yīng)到隨后對同一存儲位置的訪問中),在此之后的所有修改都還 沒有發(fā)生。在順序點(diǎn)之間則沒有任何保證。對C/C++ 語言這類允許表達(dá)式有副作用的語言,順序點(diǎn)的概念特別重要。
現(xiàn)在上面問題的回答已經(jīng)很清楚了:如果在a[i]++ 和a[j] 之間存在一個順序點(diǎn),那么就能保證a[j] 將取得修改之后的值;否則就不能保證。
C/C++語言定義(語言的參考手冊)明確定義了順序點(diǎn)的概念。順序點(diǎn)位于:
1. 每個完整表達(dá)式結(jié)束時。完整表達(dá)式包括變量初始化表達(dá)式,表達(dá)式語句,return語句的表達(dá)式,以及條件、循環(huán)和switch語句的控制表達(dá)式(for頭部有三個控制表達(dá)式);
2. 運(yùn)算符 &&、||、?: 和逗號運(yùn)算符的第一個運(yùn)算對象計(jì)算之后;
3. 函數(shù)調(diào)用中對所有實(shí)際參數(shù)和函數(shù)名表達(dá)式(需要調(diào)用的函數(shù)也可能通過表達(dá)式描述)的求值完成之后(進(jìn)入函數(shù)體之前)。
假設(shè)時刻ti和ti+1是前后相繼的兩個順序點(diǎn),到了ti+1,任何C/C++ 系統(tǒng)(VC、BC等都是C/C++系統(tǒng))都必須實(shí)現(xiàn)ti之后發(fā)生的所有副 作用。當(dāng)然它們也可以不等到時刻ti+1,完全可以選擇在時段 [t, ti+1] 之間的任何時刻實(shí)現(xiàn)在此期間出現(xiàn)的副作用,因?yàn)镃/C++ 語言允許 這些選擇。
前面討論中假定了a[i]++ 在a[i] 之前做。在一個程序片段里a[i]++ 究竟是否先做,還與它所在的表達(dá)式確定的計(jì)算過程有關(guān)。我們都熟悉C/C++ 語言有關(guān)優(yōu)先級、結(jié)合性和括號的規(guī)定,而出現(xiàn)多個運(yùn)算對象時的計(jì)算順序卻常常被人們忽略。看下面例子:
(a + b) * (c + d) fun(a++, b, a+5)
這里“*”的兩個運(yùn)算對象中哪個先算?fun及其三個參數(shù)按什么順序計(jì)算?對第一個表達(dá)式,采用任何計(jì)算順序都沒關(guān)系,因?yàn)槠渲械淖颖磉_(dá)式都是引用透明的。 第二個例子里的實(shí)參表達(dá)式出現(xiàn)了副作用,計(jì)算順序就非常重要了。少數(shù)語言明確規(guī)定了運(yùn)算對象的計(jì)算順序(Java規(guī)定從左到右),C/C++ 則有意不予 規(guī)定,既沒有規(guī)定大多數(shù)二元運(yùn)算的兩個對象的計(jì)算順序(除了&&、|| 和 ,),也沒有規(guī)定函數(shù)參數(shù)和被調(diào)函數(shù)的計(jì)算順序。在計(jì)算第二 個表達(dá)式時,首先按照某種順序算fun、a++、b和a+5,之后是順序點(diǎn),而后進(jìn)入函數(shù)執(zhí)行。
不少書籍在這些問題上有錯(包括一些很流行的書)。例如說C/C++ 先算左邊(或右邊),或者說某個C/C++ 系統(tǒng)先計(jì)算某一邊。這些說法都是錯誤 的!一個C/C++ 系統(tǒng)可以永遠(yuǎn)先算左邊或永遠(yuǎn)先算右邊,也可以有時先算左邊有時先算右邊,或在同一表達(dá)式里有時先算左邊有時先算右邊。不同系統(tǒng)可能采 用不同的順序(因?yàn)槎挤险Z言標(biāo)準(zhǔn));同一系統(tǒng)的不同版本完全可以采用不同方式;同一版本在不同優(yōu)化方式下,在不同位置都可能采用不同順序。因?yàn)檫@些做法 都符合語言規(guī)范。在這里還要注意順序點(diǎn)的問題:即使某一邊的表達(dá)式先算了,其副作用也可能沒有反映到內(nèi)存,因此對另一邊的計(jì)算沒有影響。
回到前面的例子:“誰知道下面C語句給n賦什么值?”
m = 1; n = m++ +m++;
正確回答是:不知道!語言沒有規(guī)定它應(yīng)該算出什么,結(jié)果完全依賴具體系統(tǒng)在具體上下文中的具體處理。其中牽涉到運(yùn)算對象的求值順序和變量修改的實(shí)現(xiàn)時刻問題。對于:
cout << a++ << a;
我們知道它是
(cout.operator <<(a++)).operator << (a);
的簡寫。先看外層函數(shù)調(diào)用,這里需要算出所用函數(shù),還需要計(jì)算a的值。語言沒有規(guī)定哪個先算。如果真的先算函數(shù),這一計(jì)算中出現(xiàn)了另一次函數(shù)調(diào)用,在被調(diào) 函數(shù)體執(zhí)行前有一個順序點(diǎn),那時a++的副作用就會實(shí)現(xiàn)。如果是先算參數(shù),求出a的值
4,而后計(jì)算函數(shù)時的副作用當(dāng)然不會改變它(這種情況下輸出兩個 4)。當(dāng)然,這些只是假設(shè),實(shí)際應(yīng)該說的是:這種東西根本不該寫,討論其效果沒有意義。
有人可能說,為什么人們設(shè)計(jì) C/C++時不把順序規(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)該 說,在這個問題上,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ì)算)里存在對同一“變量”的多個引用,那么表達(dá)式里就不應(yīng)該出現(xiàn)對這一“變量”的副作用。否則就不能保證得到預(yù)期結(jié)果。注意:這里的問題不是在某個系統(tǒng)里試一試的問題,因?yàn)槲覀儾豢赡茉囼?yàn)所有可能的表達(dá)式組合形式以及所有可能的上下文。這里討論的是語言,而不是某個實(shí)現(xiàn)。總而言之,絕不要寫這種表達(dá)式,否則我們或早或晚會某種環(huán)境中遇到麻煩。
后記:去年參加一個學(xué)術(shù)會議,看到有同行寫文章討論某個C系統(tǒng)里表達(dá)式究竟按什么順序求值,并總結(jié)出一些“規(guī)律”。從討論中了解到某“程序員水平考試”出 了這類題目。這使我感到很不安。今年給一個教師學(xué)習(xí)班講課,發(fā)現(xiàn)許多專業(yè)課教師也對這一基本問題也不甚明了,更覺得問題確實(shí)嚴(yán)重。因此整理出這篇短文供大 家參考。
以上這篇淺談C/C++ 語言中的表達(dá)式求值就是小編分享給大家的全部內(nèi)容了,希望能給大家一個參考,也希望大家多多支持我們。
上一篇:C語言 基本語法示例講解
欄 目:C語言
下一篇:C++11新特性之智能指針(shared_ptr/unique_ptr/weak_ptr)
本文標(biāo)題:淺談C/C++ 語言中的表達(dá)式求值
本文地址:http://mengdiqiu.com.cn/a1/Cyuyan/2129.html
您可能感興趣的文章
- 04-02c語言函數(shù)調(diào)用后清空內(nèi)存 c語言調(diào)用函數(shù)刪除字符
- 04-02c語言的正則匹配函數(shù) c語言正則表達(dá)式函數(shù)庫
- 04-02func函數(shù)+在C語言 func函數(shù)在c語言中
- 04-02c語言中對數(shù)函數(shù)的表達(dá)式 c語言中對數(shù)怎么表達(dá)
- 04-02c語言用函數(shù)寫分段 用c語言表示分段函數(shù)
- 04-02c語言編寫函數(shù)冒泡排序 c語言冒泡排序法函數(shù)
- 04-02c語言沒有round函數(shù) round c語言
- 04-02c語言分段函數(shù)怎么求 用c語言求分段函數(shù)
- 04-02C語言中怎么打出三角函數(shù) c語言中怎么打出三角函數(shù)的值
- 04-02c語言調(diào)用函數(shù)求fibo C語言調(diào)用函數(shù)求階乘


閱讀排行
本欄相關(guān)
- 04-02c語言函數(shù)調(diào)用后清空內(nèi)存 c語言調(diào)用
- 04-02func函數(shù)+在C語言 func函數(shù)在c語言中
- 04-02c語言的正則匹配函數(shù) c語言正則表達(dá)
- 04-02c語言用函數(shù)寫分段 用c語言表示分段
- 04-02c語言中對數(shù)函數(shù)的表達(dá)式 c語言中對
- 04-02c語言編寫函數(shù)冒泡排序 c語言冒泡排
- 04-02c語言沒有round函數(shù) round c語言
- 04-02c語言分段函數(shù)怎么求 用c語言求分段
- 04-02C語言中怎么打出三角函數(shù) c語言中怎
- 04-02c語言調(diào)用函數(shù)求fibo C語言調(diào)用函數(shù)求
隨機(jī)閱讀
- 01-10C#中split用法實(shí)例總結(jié)
- 04-02jquery與jsp,用jquery
- 08-05DEDE織夢data目錄下的sessions文件夾有什
- 01-10delphi制作wav文件的方法
- 01-11ajax實(shí)現(xiàn)頁面的局部加載
- 08-05織夢dedecms什么時候用欄目交叉功能?
- 08-05dedecms(織夢)副欄目數(shù)量限制代碼修改
- 01-10使用C語言求解撲克牌的順子及n個骰子
- 01-11Mac OSX 打開原生自帶讀寫NTFS功能(圖文
- 01-10SublimeText編譯C開發(fā)環(huán)境設(shè)置