論壇和博客上經常看到關於C語言中運算符的迷惑,甚至是錯誤的解讀。這樣的迷惑或解讀大都發生在表達式中存在着較爲複雜的反作用時。但從本質上看,仍然是概念理解上的誤差。本文試圖經過對三個典型表達式的分析,集中說說運算符的優先級、結合性方面的問題,同時說明它們跟求值過程之間存在的區別與聯繫。c++
優先級決定表達式中各類不一樣的運算符起做用的優先次序,而結合性則在相鄰的運算符的具備同等優先級時,決定表達式的結合方向。ide
(一)a = b = c;
關於優先級與結合性的經典示例之一就是上面這個「連續賦值」表達式。
b的兩邊都是賦值運算,優先級天然相同。而賦值表達式具備「向右結合」的特性,這就決定了這個表達式的語義結構是「a = (b = c)」,而非「(a = b) = c」。即首先完成c向b的賦值(類型不一樣時可能發生提高、截斷或強制轉換之類的事情),而後將表達式「b = c」的值再賦向a。咱們知道,賦值表達式的值就是賦值完成以後左側操做數擁有的值,在最簡單的狀況下,即a、b、c的類型徹底相同時,它跟「b = c; a = b;」這樣分開來寫效果徹底相同。
通常來說,對於二元運算符▽來講,若是它是「向左結合」的,那麼「x ▽ y ▽ z」將被解讀爲「(x ▽ y) ▽ z」,反之則被解讀爲「x ▽ (y ▽ z)」。注意,相鄰的兩個運算符能夠不一樣,但只要有同等優先級,上面的結論就適用。再好比「a * b / c」將被解讀爲「(a * b) / c」,而不是「a * (b / c)」——要知道這可能致使徹底不一樣的結果。
而一元運算符的結合性問題通常會簡單一些,好比「*++p」只可能被解讀爲「*(++p)」。三元運算符後面會提到。函數
(二)*p++;
像下面這樣實現strcpy函數的示例代碼隨處都能見到:post
[cpp] view plaincopy優化
char* strcpy( char* dest, const char* src ){ lua
char*p = dest; spa
while(*p++ = *src++); .net
return dest; 指針
} 代碼規範
理解這一實現的關鍵在於理解「*p++」的含義。
首先,解引用運算符「*」的優先級低於後自增運算符「++」,因此,這個表達式在語義上等價於「*(p++)」,而不是「(*p)++」。
論壇上常常有朋友不明白,爲何「p++」加不加括號效果都同樣,這就是答案:由於後自增的優先級原本就比解引用高,加上括號也是多餘。(這裏僅指語義上多餘,有人以爲從程序可讀性上考慮並很少餘,那是另外一回事。)
但這裏還有一個問題容易讓人糊塗,那就是後自增運算符的語義。許多書上都講「後自增是先取值,後加1。」這麼講固然沒錯,但在上面這樣的while語句中,人們仍是容易糊塗。當一個表達式中同時包含自增、解引用和賦值,並最終作爲控制循環的條件,所謂的「先取值」又是「先」到什麼地步呢?咱們仍是看看C語言標準上的說法吧。如下摘自C99標準:ISO/IEC 9899:1999:
6.5.2.4-2:The result of the postfix ++ operator is the value of the operand. After the result is obtained, the value of the operand is incremented. …… The side effect of updating the stored value of the operand shall occur between the previous and the next sequence point.
也就是說,後自增表達式的結果值就是被自增以前的那個值,而後這個結果值被肯定以後,操做數的值會被自增。而這種「自增」的反作用會在上一個「序列點」跟下一個「序列點」之間完成。
本文不打算詳細討論序列點。有興趣的讀者能夠閱讀一下標準。須要指出的是:賦值運算在C語言中並非一個序列點,因此,上面的while語句中,src的自增效果無需是在賦值以前完成。但while的整個控制表達式的結束倒是一個序列點。
咱們能夠這樣解析「while(*p++ = *src++) ;」:首先,while當中的條件變量是個賦值表達式,左側操做數是「*p++」,右側操做數是「*src++」,整個表達式的值將是賦值完成以後左側項的值。而左右兩側是對兩個後自增表達式解引用。既然解引用做用於整個後自增表達式而不是僅做用於p或src,那麼根據上面引用的標準,它們「取用」的分別是指針p和src的當前值。而自增的反作用只需在下一個序列點以前完成便可。
綜上所述:編譯器要分別取得指針p和src的當前值,基於這個值完成「*src」向「*p」的賦值;同時這個賦值結果成爲整個賦值表達式的值,用以決定是否退出while循環。而後,在整個表達式結束時的某一時刻(在不影響以前敘述的前提下),p和src分別被加1。
簡言之,整個表達式徹底結束之時,咱們既完成了基於p和src的舊值所進行的賦值和循環條件判斷,也完成了p和src的自增。
顯然,這樣的描述仍是讓人頭暈。我曾見過關於後自增(後自減)運算的另外兩種「說法」,雖然跟C語言標準上的說法並不徹底一致,但在最終的語義效果上卻一模一樣。這兩種說法是:
(1)後自增「x++」至關於一個逗號表達式:「tmp = x, ++x, tmp」;
(2)後自增就是把操做數加1,而後返回加1以前的值做爲整個表達式的值。
相對來說,仍是標準中的說法爲編譯器的實現(特別是優化)留下了更多空間,但上面的這兩種「說法」卻更便於人的理解,並且跟正確的用法在最終效果上是一致的。在C++語言中,當須要重載後自增運算符時,慣常採用的機制就是基於上面兩種說法。
有了這些理解,再來理解相似下面的strlen實現也就沒什麼問題了:
[cpp] view plaincopy
size_t strlen(const char* str){
const char* p = str;
while(*p++);
return p - str - 1;
}
注意上面函數中最後的減1。雖然是否退出while循環是由p的當前值解引用決定的,但即便while要退出,在「正式」退出以前,後自增(「++」)加1的反作用仍是要體現。也能夠這麼理解:所謂「退出循環」,是指「再也不執行循環體」,但控制表達式並不是循環體的一部分,它的全部反作用在整個表達式結束以前都會生效。因此,咱們最後要減掉循環退出時多走的這一步。
還想重複一遍:*p++就是*(p++),它們除了可讀性以外沒有任何區別,因此那種認爲加上括號就能夠實現先加1再解引用的想法是錯誤的。要達到那樣的效果,能夠用「*++p」。
(三)x > y ? 100 : ++y > 2 ? 20 : 30
這個表達式看起來有點嚇人。讓咱們先給出更多的上下文吧:
[cpp] view plaincopy
int x = 3;
int y = 2;
int z = x > y ? 100 : ++y > 2 ? 20 : 30;
此時,z的值該是多少呢?
這裏面是兩個條件運算符(?:,也叫「三目運算符」)嵌套,許多人會去查條件運算符的特性,得知它是「向右結合」的,因而認爲右側的內層條件運算「++y > 2 ? 20 : 30」先求值,這樣y首先被加1,大於2的條件成立,從而使第二個條件運算取得結果「20」;而後再來求值整個條件表達式。這時,因爲y已經變成3,「x > y」再也不成立。整個結果天然就是剛剛求得的20了。
這種思路是錯誤的。
錯誤的緣由在於:它把優先級、結合性跟求值次序徹底混爲一談了。
首先,在多數狀況下,C語言對錶達式中各子表達式的求值次序並無嚴格規定;其次,即便是求值次序肯定的場合,也是要先肯定了表達式的語義結構,在得到肯定的語義以後才談得上「求值次序」。
對於上面的例子,條件運算符「向右結合」這一特性,並無決定內層的條件表達式先被求值,而是決定了上面表達式的語義結構等價於「x > y ? 100 : (++y > 2 ? 20 : 30)」,而不是等價於「(x > y ? 100 : ++y) > 2 ? 20 : 30」。——這纔是「向右結合」的真正含義。
編譯器肯定了表達式的結構以後,就能夠準確地爲它產生運行時的行爲了。條件運算符是C語言中爲數很少的對求值次序有明確規定的運算符之一(另位還有三位,分別是邏輯與「&&」、邏輯或「||」和逗號運算符「,」)。
C語言規定:條件表達式首先對條件部分求值,若條件部分爲真,則對問號以後冒號以前的部分求值,並將求得的結果做爲整個表達式的結果值,不然對冒號以後的部分求值並做爲結果值。
所以,對於表達式「x > y ? 100 : (++y > 2 ? 20 : 30)」,首先看x大於y是否成立,在本例中它是成立的,所以整個表達式的值即爲100。也所以冒號以後的部分得不到求值機會,它的全部反作用也就沒機會生效。
總結一下,本文主要闡述瞭如下幾點:
(1)優先級決定表達式中各類不一樣的運算符起做用的優先次序,而結合性則在相鄰的兩個運算符的具備同等優先級時,決定表達式的結合方向;
(2)後自增(後自減)從語義效果上能夠理解爲在作完自增(自減)以後,返回自增(自減)以前的值做爲整個表達式的結果值;
(3)準確來說,優先級和結合性肯定了表達式的語義結構,不能跟求值次序混爲一談。
[PS-1] 維基百科上有C/C++語言運算符表:http://en.wikipedia.org/wiki/Operators_in_C_and_C%2B%2B
[PS-2] 曾在新浪微博上見benbearchen提到有的公司在代碼規範中要求:若是while的循環體爲空語句,那麼必需以continue語句代替,不許只寫一個分號。我本人很同意這個。上面strcpy和strlen的兩個例子之因此沒那麼用,只是爲了「隨大流」,由於這兩個函數的示例實現,許多人、許多書上都這麼寫。
一.運算符的優先級
在C++ Primer一書中,對於運算符的優先級是這樣描述的:
Precedence specifies how the operands are grouped. It says nothing about the order in which the operands are evaluated.
意識是說優先級規定操做數的結合方式,但並未說明操做數的計算順序。舉個例子:
6+3*4+2
若是直接按照從左到右的計算次序獲得的結果是:38,可是在C/C++中它的值爲20。
由於乘法運算符的優先級高於加法的優先級,所以3是和4分組到一塊兒的,並非6與3進行分組。這就是運算符優先級的含義。
二.運算符的結合性
Associativity specifies how to group operators at the same precedence level.
結合性規定了具備相同優先級的運算符如何進行分組。
舉個例子:
a=b=c=d;
因爲該表達式中有多個賦值運算符,究竟是如何進行分組的,此時就要看賦值運算符的結合性了。由於賦值運算符是右結合性,所以該表達式等同於(a=(b=(c=d))),而不是(a=(b=c)=d)這樣進行分組的。
同理如m=a+b+c;
等同於m=(a+b)+c;而不是m=a+(b+c);
三.操做數的求值順序
在C/C++中規定了全部運算符的優先級以及結合性,可是並非全部的運算符都被規定了操做數的計算次序。在C/C++中只有4個運算符被規定了操做數的計算次序,它們是&&,||,逗號運算符(,),條件運算符(?:)。
如m=f1()+f2();
在這裏是先調用f1(),仍是先調用f2()?不清楚,不一樣的編譯器有不一樣的調用順序,甚至相同的編譯器不一樣的版本會有不一樣的調用順序。只要最終結果與調用次序無關,這個語句就是正確的。這裏要分清楚操做數的求值順序和運算符的結合性這兩個概念,可能有時候會這樣去理解,由於加法操做符的結合性是左結合性,所以先調用f1(),再調用f2(),這種理解是不正確的。結合性是肯定操做符的對象,並非操做數的求值順序。
同理2+3*4+5;
它的結合性是(2+(3*4))+5,可是不表明3*4是最早計算的,它的計算次序是未知的,未定義的。
好比3*4->2+3*4->2+3*4+5
以及2->3*4->2+3*4->2+3*4+5和5->3*4->2+3*4->2+3*4+5這些次序都是有可能的。雖然它們的計算次序不一樣,可是對最終結果是沒有影響的。