關於C語言表達式求值問題的解釋

很多新手對於C的表達式求值(如i++ + ++i)這一問題感到十分的疑惑,不能很好地理解,今日小軒獻醜,準備爲新手們解釋一下,如有不通之處,請各位同道指出。

首先,貼上裘老的解釋:http://bbs.csdn.net/topics/370153775

其次,是小軒認爲要掌握這個問題需要的一些基礎知識:


1.變量的訪問種類與副作用的產生
在程序設計中經常用到“數”這一概念,在程序設計語言中,“數”被抽象爲“量”,在C語言中,“量”分爲“變量”和“常量”兩種。其中與變量相關的內容較多,主要爲訪問類型和屬性兩大類(本文解釋表達式求值問題,此問題與變量的屬性無過多牽連,所以會在另外一篇文章中向大家介紹變量的屬性)。C語言中訪問類型有兩種:透明引用和真引用;透明引用指不產生副作用的訪問,而真引用與之恰好相反,指產生副作用的訪問。
那麼,什麼是副作用呢?副作用指編譯器系統對一個變量實行的數值改變的過程,簡單來說,就是改變一個變量的數值。

第一個基礎知識介紹完了,現在舉個例子說明一下:
printf("%d",a+1);這裏編譯器對變量a的訪問屬於透明引用,沒有修改變量a的值(即沒有產生副作用)
if(++a) a=0;7 這裏的對變量a的兩個訪問都屬於真引用,都修改了變量a的值(即都產生了副作用)


2.序列點的定義,作用及其存在位置
序列點,也稱順序點,對於編譯器系統而言,它是一個瞬間,一個在此序列點之前的全部的副作用都必須要實現的瞬間,也就是說,編譯器系統處理到存在序列點的地方時,前面的所有副作用都已產生(即被真引用的變量都已改變它的數值),序列點存在的意義是爲了保證編譯工作的有序性和有效性。
有序性:多個不同的序列點的存在使源程序中處於不同序列點之間並被多次真引用的變量的副作用的產生有序化。
有效性:序列點的存在可以使編譯器各模塊之間相互通信,序列點就是他們的通信方式,可以保證編譯器各模塊工作的有效性。

那麼,都什麼地方存在序列點呢?C FAQs中明確寫道:
1.完整表達式(表達式語句或不爲任何其他表達式的子表達式的表達式)的尾部,即“;”處
2.“||”、“&&”、“?:”或逗號操作符處
3.函數調用處(參數求值完畢,函數被實際調用之前)

關於序列點的基礎知識就這麼多,現在讓我們看幾個例子:
a=1;本例中有一個序列點,位於結束符處,有一個副作用,在唯一的序列點之前有效。
a=1,b=2,c=3;本例中有三個序列點,分別位於兩個逗號操作符處和結束符處,變量a的副作用在第一個序列點之前有效,變量b的副作用在第二個序列點之前,第一個序列點之後有效,變量c的副作用在第二個序列點之後,第三個序列點之前有效。
if(a==1 && ++b>0)本例中有兩個序列點,分別位於"&&"處和語句結束符處(由於if語句的特殊性,沒有結束符,但if內的表達式是一個完整表達式,所以也存在序列點),變量a被透明引用,沒有產生副作用,即第一個序列點之前沒有副作用產生,變量b被真引用,在第一個序列點之後,第二個序列點之前實現其副作用。
printf("%d",a);本例中有兩個序列點,分別位於函數調用處和結束符處(函數中的“,”是分隔符,用來分割參數,不是逗號操作符),兩個序列點之前及其相對應的序列點之後均沒有副作用
a=0xffffffff,a++,c=malloc(a),*c=1;                本例中有五個序列點,分別位於三個逗號操作符處、函數調用處和結束符處,變量a的第一個副作用在第一個序列點之前有效,第二個副作用在第一個序列點之後,第二個序列點之前有效,變量c的第一個副作用在第二個序列點之後,第三個序列點(第三個序列點是函數表達式內的那個)之前有效,第三個序列點與第四個序列點之間沒有副作用,變量c的第二個副作用在地四個序列點之後,第五個序列點之前有效。


3.對於前綴運算和後綴運算的解釋
前綴運算,主要包括前綴自增運算(++i)和前綴自減運算(--i),相對應的,後綴運算主要包括後綴自增運算(i++)和後綴自減運算(i--)。
前綴運算與後綴運算的差別主要在於其副作用實現依據的序列點的不同。
前綴運算的副作用在本句結束符處的序列點之前有效,而後綴運算的副作用在本句結束符處的序列點之後有效。

舉例:
a++;本例中有一個副作用和一個序列點,其中副作用由後綴運算產生,所以在本句結束之前,變量a的值都不改變,在本句結束之後,變量a的值改變。
--a;本例中有一個副作用和一個序列點,其中副作用由前綴運算產生,所以在本句結束之前,變量a的值就已經改變。


4.C語言一個重要的規則
ANSI/ISO C標準中有這樣的描述:在上一個和下一個序列點之間,一個對象所保存的值至多隻能被表達式的求值修改一次,而且只有在確定將要保存的值的時候才能訪問前一個值。

我做具體解釋如下:
在上一個和下一個序列點之間指的就是相鄰的兩個序列點,一個對象所保存的值至多隻能被表達式求值修改一次指的是一個變量的值只能修改一次,只有在確定將要保存的值的時候才能訪問前一個值指的是將要用來計算表達式計算的每個變量的值都應該是確定的;整合化簡後,可以改述爲:在兩個相鄰的序列點之間,對於同一個變量只能進行一次真引用。

現舉例如下:
a=1,b=2,a=b;本例中有三個序列點和副作用,且相鄰序列點之間對同一變量進行真引用的次數沒有超過一次,所以本表達式合法。
a=a+1;本例中有一個序列點和一個副作用,變量a在尋列點之間出現兩次,但對變量a的真引用的次數只有一次,沒有違反我們剛纔的規則,所以本表達式同樣合法。
a[b=2]=++b;本例中有一個序列點和兩個副作用,變量b的第一個副作用對變量b就行了一次真引用,就變量b的值修改爲2,變量b的第二個副作用源自於前綴自增表達式,兩個副作用都在同兩個序列點之間,所以編譯器不知道是先進行第一個副作用還是先進行第二個副作用了,所以就有了四種結果(a[2]=2、a[2]=3、a[3]=2、a[3]=3)了,這是我們不希望看到的,所以本表達式不合法。
a[b=2]=b--;本例中有兩個序列點和兩個副作用,但是變量b的第二個副租用源自於後綴運算,這個副作用在本句結束符處的序列點之後有效,所以本句還可以解釋爲a[2]=2,b=1;這樣的表達式符合我們的規則,所以本表達式合法。

注:我這裏指的表達式不合法不是說表達式不能通過編譯器的編譯,指的是可以通過編譯但結果未知(就像a[b=2]=++b這個例子一樣有四種解釋方案,結果自然未知),我們習慣上統稱此類問題爲UB問題(未定義問題)。未定義問題主要是因爲C語言標準沒有明確規定遇到這種問題時編譯器應如何處理,所以,對於未定義問題不同編譯器給出的結果可能不同,可能相同,例如char c="asdf";在VC6.0和LCC-WIN32的結果就是不同的。


下面回到主題,我們終於可以看看i++ + ++i了!在本例中,只有一個序列點(未寫出全句),但在這個序列點之前訪問了兩次變量i,其中有一次訪問爲真引用,這樣就使表達式產生了二義性(後面的i自加後,前面的i是自加前的i還是自加後的i?),所以這個表達式就成爲了未定義問題。




ok,寫到這裏,這篇文章就差不多該結稿了,我希望新手們將本文多讀幾遍,就算不能全部理解,也要能記下多少就記多少,以後多練練,就回了;至於老手們,看後如果找到了什麼不恰當的地方,還請指出,我一定會努力修改,使本文日臻完美的。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章