C操作符(下)

一、左值與右值

爲了理解有些操作符存在的限制,必須理解左值(L-value)和右值(R-value)之間的區別。這兩個術語是多年前由編譯器設計者所創造並沿用至今,儘管它們的定義並不與C語言嚴格吻合。

左值就是那些能出現在賦值符號左邊的東西。右值就是那些可以出現在賦值符號右邊的東西。舉慄:

a = b + 25;

a是個左值,因爲它標識了一個可以存儲結果值的地點,b+25是個右值,因爲它指定了一個值。

它們可以互換嗎?

b + 25 = a;

原先作左值的a此時也可以當做右值,因爲a的位置也包含一個值。然而,b+25不能作爲左值。因爲它並未標識一個特定的位置。因此這條賦值語句是非法的。

當計算b+25時,它的結果必然保存於機器的某個地方。但是,程序員沒辦法預測該結果會存儲於什麼地方,也無法保證這個表達式的值下次還會存儲於那個地方。其結果是,這個表達式不是一個左值。基於同樣的理由,字面值常量也都不是左值。

聽上去似乎變量可以作爲左值而表達式不能作爲左值,但是這個推斷並不準確。在下面的賦值語句中,左值便是一個表達式。

int a[30];

...

a[b + 10] = 0;

下標引用實際上是一個操作符,所以表達式的左邊實際上是個表達式,但它卻是一個合法的左值,因爲它標識了一個特定的位置,我們以後還能再程序中引用它。這裏再舉一個栗子:

int a, *p;

...

p = &a;

*p = 20;

第二條賦值語句,它左邊的那個值顯然是一個表達式,但它也是個合法的左值。因爲指針p的值是內存中某個特定的位置的地址,*操作符使機器指向那個位置,當它作爲左值使用時,這個表達式指定需要進行修改的位置。當它作爲右值使用時,它就提取了當前存儲於這個位置的值。

有些操作符,如間接訪問(*)和下標引用([]),它們的結果是個左值。其餘操作符的結果則是個右值。

 

二、表達式求值

表達式的求值順序一部分是由它所包含的操作符的優先級和結合性決定。同樣有些表達式的操作數在求值過程中可能轉換爲其他類型。

1.隱式類型轉換

C的整型算術運算總是至少以缺省整型類型(default type)的精度來進行的。爲了獲得這個精度,表達式中的字符型和短整型操作數在使用之前被轉換爲普通整型,這種轉換稱爲整型提升(integral promotion)。例如,

char a, b, c;

...

a = b + c;

b和c的值都被提升爲普通整型,然後執行加法運算。加法運算的結果將被截短,然後再存儲於a中。這個例子的結果和使用8位算術的結果是一樣的。但在下面例子中,它的結果就不再相同。這個例子用於計算一系列字符的簡單檢驗和。

a = (~a ^ b << 1) >> 1;

由於存在求補和左移操作,所以8位精度是不夠的。標準要求進行完整的整型求值,所以對於這類表達式的結果,不會存在歧義性。(標準說明結果應該是通過完整的整型求值得到,編譯器如果知道採用8位精度的求值不會影響最終結果,它也允許編譯器這樣做)。

2.算術轉換

如果某個操作符的各個操作數屬於不同的類型,那麼除非其中一個操作數轉換爲另外一個操作數的類型,否則操作就無法進行。下面的層次體系稱爲尋常算術轉換(usual arithmetic conversion)。

long double

double

float

unsigned long int

long int

unsigned int

int

如果某個操作數的類型在上面排名較低,那麼它首先將轉換爲另外一個操作數的類型然後執行操作。

3.操作符的屬性

複雜表達式的求值順序是由3個因素決定的:操作符的優先級、操作符的結合性及操作符是否控制執行的順序。相鄰兩個操作符哪個縣執行取決於它們的優先級,如果兩者的優先級相同,那麼它們的執行順序由它們的結合性決定。簡單地說,結合性就是一串操作符是從左向右依次執行還是反過來執行。最後,有4個操作符,它們可以對整個表達式的求值順序施加控制,它們能保證某個表達式能夠在另一個子表達式的所有求值過程完成之前進行求值,也可能使某個表達式被跳過不再求值。

每個操作符的所有屬性都列在下優先級表中。表中各個列分別代表操作符、它的功能簡述、用法示例、結果類型、結合性以及當它出現時是否會對錶達式的求值順序施加控制。用法示例提示它是否需要操作數爲左值。術語lexp(left-expression)表示左值表達式,rexp表示右值表達式。記住,左值意味着一個位置,而右值意味着一個值。所以,在使用右值的地方也可以使用左值。但是需要左值的地方不能使用右值。

4.優先級和求值的順序

如果一個表達式中的操作符超過一個,是什麼決定這些操作符的執行順序呢?答案是優先級,C的每個操作符都有其優先級,用於確定它和表達式中其餘操作符之間的關係。但僅憑優先級還不能確定求值的順序。下面是它的基本規則:

兩個相鄰的操作符的執行順序由它們的優先級決定。如果它們的優先級相同,它們的執行順序由它們的結合性決定。除此之外,編譯器可以自由決定使用任何順序對錶達式進行求值,只要它不違背逗號、&&、||和?:操作符所施加的限制。

換句話說,表達式中操作符的優先級只決定表達式的各個組成部分在求值過程中如何進行聚組。舉個例子:

a + b * c

在這個表達式中,乘法和加法操作符是兩個相鄰的操作符。由於*操作符的優先級比+操作符高,所以乘法運算先於加法運算執行。編譯器在這裏別無選擇,它必須先執行乘法運算。

下面是一個更爲有趣的表達式:

a * b + c * d + e * f

如果僅由優先級決定這個表達式的求值順序,那麼所有三個乘法運算將在所有加法運算之前進行。事實上,這個順序並不是必需的。實際上只要保證每個乘法運算在它相鄰的加法運算之前進行即可。例如,這個表達式可能會以下面的順序進行,其中粗體的操作符表示在每個步驟中進行操作的操作符。

a * b

c * d

(a*b) + (c*d)

e * f

((a*b)+(c*d)) + (e*f)

注意第一個加法運算在最後一個乘法運算之前進行。如果這個表達式按以下順序執行,其結果是一樣的。

c * d

e * f

a * b

(a*b) + (c*d)

((a*b)+(c*d)) + (e*f)

加法運算的結合性要求兩個加法運算按照先左後右的順序執行,但它對錶達式剩餘部分的執行順序並未加以限制。尤其是,這裏並沒有任何規則要求所有的乘法運算首先執行,也沒有規則規定這幾個乘法運算之間誰先執行。優先級規則在這裏起不到作用,優先級只對相鄰操作符的執行順序起作用。

警告:

由於表達式的求值順序並非完全由操作符的優先級決定,所以像下面這樣的語句是很危險的。

c + --c

操作符的優先級規則要求自減運算在加法運算之前進行,但我們並沒有辦法得知加法操作符的左操作數是在右操作數之前還是之後進行求值。它在這個表達式中將存在區別(二義性),因爲自減操作符具有副作用。--c和c之前或之後執行,表達式的結果在兩種情況下將會不同。

標準說明類似這種表達式的值是未定義的。儘管每種編譯器都會爲這個表達式產生某個值,但到底哪個是正確的並無標準答案。因此,這樣的表達式應該避免。

同樣的,下面這個表達式說明了一個相關的問題。

f() + g() + h()

儘管左邊那個加法運算必須在右邊那個加法運算之前執行,但對於各個函數調用的順序,並沒有規則加以限制。如果它們的執行具有副作用,比如執行一些I/0任務或修改全局變量,那麼函數的調用順序的不同可能會產生不同的結果。因此,如果順序導致結果產生區別,你做好使用臨時變量,讓每個函數調用都在單獨的語句中進行。

temp = f();

temp += g();

temp += h();

 

總結

C具有豐富的操作符。算術操作符包括+(加)、-(減)、*(乘)、/(除)、%(取模)。除了%之外,其餘的操作符不僅可以作用於整型還可以作用於浮點型。

<<和>>操作符分別執行左移位和右移位操作。&、|和^分別執行與、或和異或操作。這幾個操作符都要求其操作數爲整型。

=操作符執行賦值操作。而且,C還存在複合賦值符,它將賦值符和前面的操作符結合在一起:

+=        -=     *=     /=     %=

<<=    >>=   &=   ^=     |=

複合賦值符在做右操作數之間執行指定的運算,然後把結果賦值給左操作數。

單目運算符包括!(邏輯非)、~(按位取反)、-(負)、+(正)、++和--操作符分別用於增加或減少操作數的值。這兩個操作數有前綴和後綴形式。前綴形式在操作數的值被修改之後才返回這個值,而後綴形式在操作數的值被修改之前就返回這個值。&操作符返回一個指向它的操作數指針(取地址),而*操作符對它的操作數(必須爲指針)進行間接訪問操作。sizeof返回操作數的類型的長度,以字節爲單位。最後,強制類型轉換(cast)用於修改操作數的數據類型。

關係操作符有:

>     >=     <     <=     !=    ==

每個操作符根據它的操作數之間是否存在指定的關係,或返回真,或返回假。邏輯操作符用於計算複雜的布爾表達式。對於&&操作符,只有當它的兩個操作數都爲真時,它的值纔是真;||操作符,只有它的兩個操作數都爲假時,它的值纔是假。這兩個操作符會對包含它們的表達式的求值過程施加控制。如果整個表達式的值通過左操作數便可確定,那麼右操作數便不再求值。

條件操作符?:接受三個參數,它也會對錶達式的求值過程施加控制。如果第一個操作數的值爲真,那麼整個表達式的結果就是第二個操作數的值,第三個操作數不會執行。否則結果是第三個操作數的值,第二個操作數不會執行。逗號操作符把兩個或更多的表達式連接在一起,從左向右一次進行求值,整個表達式的值就是最右邊的子表達式的值。

左值是個表達式,它可以出現在賦值符的左邊,它表示計算機內存中的一個位置。右值表示一個值,所以它只能出現在賦值符的右邊。每個左值表達式同時也是個右值,但反過來就不是這樣。

各個不同類型之間不能直接進行運算,除非其中之一的操作數轉換爲另一操作數的類型。尋常算術轉換決定哪個操作數將被轉換。操作符的優先級決定了相鄰的操作符哪個先被執行。如果它們的優先級相等,那麼它們的結合性將決定它們執行的順序。但是,這些並不能完全決定表達式的求值順序。編譯器只要不違背優先級和結合性規則,就可以自由決定複雜表達式的求值順序。表達式的結果如果依賴於求值的順序,那麼它在本質上是不可移植的,應避免使用。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章