# 第?16?章?運算符詳解
**目錄**
+ [1\. 位運算](ch16s01.html)
+ [1.1\. 按位與、或、異或、取反運算](ch16s01.html#id2761062)
+ [1.2\. 移位運算](ch16s01.html#id2761805)
+ [1.3\. 掩碼](ch16s01.html#id2761995)
+ [1.4\. 異或運算的一些特性](ch16s01.html#id2762114)
+ [2\. 其它運算符](ch16s02.html)
+ [2.1\. 復合賦值運算符](ch16s02.html#id2762352)
+ [2.2\. 條件運算符](ch16s02.html#id2762537)
+ [2.3\. 逗號運算符](ch16s02.html#id2762598)
+ [2.4\. sizeof運算符與typedef類型聲明](ch16s02.html#id2762676)
+ [3\. Side Effect與Sequence Point](ch16s03.html)
+ [4\. 運算符總結](ch16s04.html)
本章介紹很多前面沒有講過的運算符,重點是位運算,然后引出一個重要的概念Sequence Point,在最后一節總結C語言各種運算符的優先級和結合性。
## 1.?位運算
整數在計算機中用二進制的位來表示,C語言提供一些運算符可以直接操作整數中的位,稱為位運算,這些運算符的操作數都必須是整型的。在以后的學習中你會發現,有些信息利用整數中的某幾個位來存儲,要訪問這些位,僅僅有對整數的操作是不夠的,必須借助位運算,例如[第?2?節 “Unicode和UTF-8”](apas02.html#app-encoding.utf8)介紹的UTF-8編碼就是如此,學完本節之后你應該能自己寫出UTF-8的編碼和解碼程序。本節首先介紹各種位運算符,然后介紹與位運算有關的編程技巧。
### 1.1.?按位與、或、異或、取反運算
在[第?3?節 “布爾代數”](ch04s03.html#cond.bool)講過邏輯與、或、非運算,并列出了真值表,對于整數中的位也可以做與、或、非運算,C語言提供了按位與(Bitwise AND)運算符&、按位或(Bitwise OR)運算符|和按位取反(Bitwise NOT)運算符~,此外還有按位異或(Bitwise XOR)運算符^,我們在[第?1?節 “為什么計算機用二進制計數”](ch14s01.html#number.binary)講過異或運算。下面用二進制的形式舉幾個例子。
**圖?16.1.?位運算**

注意,&、|、^運算符都是要做Usual Arithmetic Conversion的(其中有一步是Integer Promotion),~運算符也要做Integer Promotion,所以在C語言中其實并不存在8位整數的位運算,操作數在做位運算之前都至少被提升為`int`型了,上面用8位整數舉例只是為了書寫方便。比如:
```
unsigned char c = 0xfc;
unsigned int i = ~c;
```
計算過程是這樣的:常量0xfc是`int`型的,賦給`c`要轉成`unsigned char`,值不變;`c`的十六進制表示是fc,計算`~c`時先提升為整型(000000fc)然后取反,最后結果是ffffff03。注意,如果把`~c`看成是8位整數的取反,最后結果就得3了,這就錯了。為了避免出錯,一是盡量避免不同類型之間的賦值,二是每一步計算都要按上一章講的類型轉換規則仔細檢查。
### 1.2.?移位運算
移位運算符(Bitwise Shift)包括左移<<和右移>>。左移將一個整數的各二進制位全部左移若干位,例如0xcfffffff3<<2得到0x3fffffcc:
**圖?16.2.?左移運算**

最高兩位的11被移出去了,最低兩位又補了兩個0,其它位依次左移兩位。但要注意,移動的位數必須小于左操作數的總位數,比如上面的例子,左邊是`unsigned int`型,如果左移的位數大于等于32位,則結果是Undefined。移位運算符不同于+ - * / ==等運算符,兩邊操作數的類型不要求一致,但兩邊操作數都要做Integer Promotion,整個表達式的類型和左操作數提升后的類型相同。
復習一下[第?2?節 “不同進制之間的換算”](ch14s02.html#number.convert)講過的知識可以得出結論,_在一定的取值范圍內,將一個整數左移1位相當于乘以2_。比如二進制11(十進制3)左移一位變成110,就是6,再左移一位變成1100,就是12。讀者可以自己驗證這條規律對有符號數和無符號數都成立,對負數也成立。當然,如果左移改變了最高位(符號位),那么結果肯定不是乘以2了,所以我加了個前提“在一定的取值范圍內”。由于計算機做移位比做乘法快得多,編譯器可以利用這一點做優化,比如看到源代碼中有`i * 8`,可以編譯成移位指令而不是乘法指令。
當操作數是無符號數時,右移運算的規則和左移類似,例如0xcfffffff3>>2得到0x33fffffc:
**圖?16.3.?右移運算**

最低兩位的11被移出去了,最高兩位又補了兩個0,其它位依次右移兩位。和左移類似,移動的位數也必須小于左操作數的總位數,否則結果是Undefined。在一定的取值范圍內,將一個整數右移1位相當于除以2,小數部分截掉。
當操作數是有符號數時,右移運算的規則比較復雜:
* 如果是正數,那么高位移入0
* 如果是負數,那么高位移入1還是0不一定,這是Implementation-defined的。對于x86平臺的`gcc`編譯器,最高位移入1,也就是仍保持負數的符號位,這種處理方式對負數仍然保持了“右移1位相當于除以2”的性質。
綜上所述,由于類型轉換和移位等問題,用有符號數做位運算是很不方便的,所以,_建議只對無符號數做位運算,以減少出錯的可能_。
#### 習題
1、下面兩行`printf`打印的結果有何不同?請讀者比較分析一下。`%x`轉換說明的含義詳見[第?2.9?節 “格式化I/O函數”](ch25s02.html#stdlib.formatio)。
```
int i = 0xcffffff3;
printf("%x\n", 0xcffffff3>>2);
printf("%x\n", i>>2);
```
### 1.3.?掩碼
如果要對一個整數中的某些位進行操作,怎樣表示這些位在整數中的位置呢?可以用掩碼(Mask)來表示。比如掩碼0x0000ff00表示對一個32位整數的8~15位進行操作,舉例如下。
1、取出8~15位。
```
unsigned int a, b, mask = 0x0000ff00;
a = 0x12345678;
b = (a & mask) >> 8; /* 0x00000056 */
```
這樣也可以達到同樣的效果:
```
b = (a >> 8) & ~(~0U << 8);
```
2、將8~15位清0。
```
unsigned int a, b, mask = 0x0000ff00;
a = 0x12345678;
b = a & ~mask; /* 0x12340078 */
```
3、將8~15位置1。
```
unsigned int a, b, mask = 0x0000ff00;
a = 0x12345678;
b = a | mask; /* 0x1234ff78 */
```
#### 習題
1、統計一個無符號整數的二進制表示中1的個數,函數原型是`int countbit(unsigned int x);`。
2、用位操作實現無符號整數的乘法運算,函數原型是`unsigned int multiply(unsigned int x, unsigned int y);`。例如:(11011)<sub>2</sub>×(10010)<sub>2</sub>=((11011)<sub>2</sub><<1)+((11011)<sub>2</sub><<4)。
3、對一個32位無符號整數做循環右移,函數原型是`unsigned int rotate_right(unsigned int x);`。所謂循環右移就是把低位移出去的部分再補到高位上去,例如`rotate_right(0xdeadbeef, 16)`的值應該是0xefdeadbe。
### 1.4.?異或運算的一些特性
1、一個數和自己做異或的結果是0。如果需要一個常數0,x86平臺的編譯器可能會生成這樣的指令:`xorl %eax, %eax`。不管`eax`寄存器里的值原來是多少,做異或運算都能得到0,這條指令比同樣效果的`movl $0, %eax`指令快,因為前者只需要在CPU內部計算,而后者需要訪問內存,在下一章[第?5?節 “Memory Hierarchy”](ch17s05.html#arch.memh)詳細介紹。
2、從異或的真值表可以看出,不管是0還是1,和0做異或保持原值不變,和1做異或得到原值的相反值。可以利用這個特性配合掩碼實現某些位的翻轉,例如:
```
unsigned int a, b, mask = 1U << 6;
a = 0x12345678;
b = a ^ mask; /* flip the 6th bit */
```
3、如果a<sub>1</sub> ^ a<sub>2</sub> ^ a<sub>3</sub> ^ ... ^ a<sub>n</sub>的結果是1,則表示a<sub>1</sub>、a<sub>2</sub>、a<sub>3</sub>...a<sub>n</sub>之中1的個數為奇數個,否則為偶數個。這條性質可用于奇偶校驗(Parity Check),比如在串口通信過程中,每個字節的數據都計算一個校驗位,數據和校驗位一起發送出去,這樣接收方可以根據校驗位粗略地判斷接收到的數據是否有誤。
4、x ^ x ^ y == y,因為x ^ x == 0,0 ^ y == y。這個性質有什么用呢?我們來看這樣一個問題:交換兩個變量的值,不得借助額外的存儲空間,所以就不能采用`temp = a; a = b; b = temp;`的辦法了。利用位運算可以這樣做交換:
```
a = a ^ b;
b = b ^ a;
a = a ^ b;
```
分析一下這個過程。為了避免混淆,把a和b的初值分別記為a<sub>0</sub>和b<sub>0</sub>。第一行,`a = a<sub>0</sub> ^ b<sub>0</sub>`;第二行,把a的新值代入,得到`b = b<sub>0</sub> ^ a<sub>0</sub> ^ b<sub>0</sub>`,等號右邊的b<sub>0</sub>相當于上面公式中的x,a<sub>0</sub>相當于y,所以結果為a<sub>0</sub>;第三行,把a和b的新值代入,得到`a = a<sub>0</sub> ^ b<sub>0</sub> ^ a<sub>0</sub>`,結果為b<sub>0</sub>。注意這個過程不能把同一個變量自己跟自己交換,而利用中間變量`temp`則可以交換。
#### 習題
1、請在網上查找有關RAID(Redundant Array of Independent Disks,獨立磁盤冗余陣列)的資料,理解其實現原理,其實就是利用了本節的性質3和4。
2、交換兩個變量的值,不得借助額外的存儲空間,除了本節講的方法之外你還能想出什么方法?本節講的方法不能把同一個變量自己跟自己交換,你的方法有沒有什么局限性?
## 2.?其它運算符
### 2.1.?復合賦值運算符
復合賦值運算符(Compound Assignment Operator)包括`*=` `/=` `%=` `+=` `-=` `<<=` `>>=` `&=` `^=` `|=`,一邊做運算一邊賦值。例如`a += 1`相當于`a = a + 1`。但有一點細微的差別,前者對表達式`a`只求值一次,而后者求值兩次,如果`a`是一個復雜的表達式,求值一次和求值兩次的效率是不同的,例如`a[i+j] += 1`和`a[i+j] = a[i+j] + 1`。那么僅僅是效率上的差別嗎?對于沒有Side Effect的表達式,求值一次和求值兩次的結果是一樣的,但對于有Side Effect的表達式則不一定,例如`a[foo()] += 1`和`a[foo()] = a[foo()] + 1`,如果`foo()`函數調用有Side Effect,比如會打印一條消息,那么前者只打印一次,而后者打印兩次。
在[第?3?節 “for語句”](ch06s03.html#iter.for)講自增、自減運算符時說`++i`相當于`i = i + 1`,其實更準確地說應該是等價于`i += 1`,表達式`i`只求值一次,而`--i`等價于`i -= 1`。
### 2.2.?條件運算符
條件運算符(Conditional Operator)是C語言中唯一一個三目運算符(Ternary Operator),帶三個操作數,它的形式是`表達式1 ? 表達式2 : 表達式3`,這個運算符所組成的整個表達式的值等于表達式2或表達式3的值,取決于表達式1的值是否為真,可以把它想像成這樣的函數:
```
if (表達式1)
return 表達式2;
else
return 表達式3;
```
表達式1相當于`if`語句的控制表達式,因此它的值必須是標量類型,而表達式2和3相當于同一個函數在不同情況下的返回值,因此它們的類型要求一致,也要做Usual Arithmetic Conversion。
下面舉個例子,定義一個函數求兩個參數中較大的一個。
```
int max(int a, int b)
{
return (a > b) ? a : b;
}
```
### 2.3.?逗號運算符
逗號運算符(Comma Operator)也是一種雙目運算符,它的形式是`表達式1, 表達式2`,兩個表達式不要求類型一致,左邊的表達式1先求值,求完了直接把值丟掉,再求右邊表達式2的值作為整個表達式的值。逗號運算符是左結合的,類似于+ - * /運算符,根據組合規則可以寫出`表達式1, 表達式2, 表達式3, ..., 表達式n`這種形式,`表達式1, 表達式2`可以看作一個子表達式,先求表達式1的值,然后求表達式2的值作為這個子表達式的值,然后這個值再和表達式3組成一個更大的表達式,求表達式3的值作為這個更大的表達式的值,依此類推,整個計算過程就是從左到右依次求值,最后一個表達式的值成為整個表達式的值。
注意,函數調用時各實參之間也是用逗號隔開,這種逗號是分隔符而不是逗號運算符。但可以這樣使用逗號運算符:
```
f(a, (t=3, t+2), c)
```
傳給函數`f`的參數有三個,其中第二個參數的值是表達式`t+2`的值。
### 2.4.?sizeof運算符與typedef類型聲明
`sizeof`是一個很特殊的運算符,它有兩種形式:“sizeof 表達式”和“sizeof(類型名)”。這個運算符很特殊,“sizeof 表達式”中的子表達式并不求值,而只是根據類型轉換規則求得子表達式的類型,然后把這種類型所占的字節數作為整個表達式的值。有些人喜歡寫成“sizeof(表達式)”的形式也可以,這里的括號和`return(1);`的括號一樣,不起任何作用。但另外一種形式“sizeof(類型名)”的括號則是必須寫的,整個表達式的值也是這種類型所占的字節數。
比如用`sizeof`運算符求一個數組的長度:
```
int a[12];
printf("%d\n", sizeof a/sizeof a[0]);
```
在上面這個例子中,由于`sizeof 表達式`中的子表達式不需要求值,所以不需要到運行時才計算,事實上在編譯時就知道`sizeof a`的值是48,`sizeof a[0]`的值是4,所以在編譯時就已經把`sizeof a/sizeof a[0]`替換成常量12了,這是一個常量表達式。
`sizeof`運算符的結果是`size_t`類型的,這個類型定義在`stddef.h`頭文件中,不過你的代碼中只要不出現`size_t`這個類型名就不用包含這個頭文件,比如像上面的例子就不用包含這個頭文件。C標準規定`size_t`是一種無符號整型,編譯器可以用`typedef`做一個類型聲明:
```
typedef unsigned long size_t;
```
那么`size_t`就代表`unsigned long`型。不同平臺的編譯器可能會根據自己平臺的具體情況定義`size_t`所代表的類型,比如有的平臺定義為`unsigned long`型,有的平臺定義為`unsigned long long`型,C標準規定`size_t`這個名字就是為了隱藏這些細節,使代碼具有可移植性。所以注意不要把`size_t`類型和它所代表的真實類型混用,例如:
```
unsigned long x;
size_t y;
x = y;
```
如果在一種ILP32平臺上定義`size_t`代表`unsigned long long`型,這段代碼把`y`賦給`x`時就把高位截掉了,結果可能是錯的。
`typedef`這個關鍵字用于給某種類型起個新名字,比如上面的`typedef`聲明可以這么看:去掉`typedef`就成了一個變量聲明`unsigned long size_t;`,`size_t`是一個變量名,類型是`unsigned long`,那么加上`typedef`之后,`size_t`就是一個類型名,就代表`unsigned long`類型。再舉個例子:
```
typedef char array_t[10];
array_t a;
```
這相當于聲明`char a[10];`。類型名也遵循標識符的命名規則,并且通常加個`_t`后綴表示Type。
## 3.?Side Effect與Sequence Point
如果你只想規規矩矩地寫代碼,那么基本用不著看這一節。本節的內容基本上是鉆牛角尖兒的,除了Short-circuit比較實用,其它寫法都應該避免使用。但沒辦法,有時候不是你想鉆牛角尖兒,而是有人逼你去鉆牛角尖兒。這是我們的學員在找工作筆試時碰到的問題:
```
int a=0;
a = (++a)+(++a)+(++a)+(++a);
```
據我了解,似乎很多公司都有出這種筆試題的惡趣味。答案應該是Undefined,我甚至有些懷疑出題人是否真的知道答案。下面我來解釋為什么是Undefined。
我們知道,調用一個函數可能產生Side Effect,使用某些運算符(++ -- = 復合賦值)也會產生Side Effect,如果一個表達式中隱含著多個Side Effect,究竟哪個先發生哪個后發生呢?C標準規定代碼中的某些點是Sequence Point,當執行到一個Sequence Point時,在此之前的Side Effect必須全部作用完畢,在此之后的Side Effect必須一個都沒發生。至于兩個Sequence Point之間的多個Side Effect哪個先發生哪個后發生則沒有規定,編譯器可以任意選擇各Side Effect的作用順序。下面詳細解釋各種Sequence Point。
1、調用一個函數時,在所有準備工作做完之后、函數調用開始之前是Sequence Point。比如調用`foo(f(), g())`時,`foo`、`f()`、`g()`這三個表達式哪個先求值哪個后求值是Unspecified,但是必須都求值完了才能做最后的函數調用,所以`f()`和`g()`的Side Effect按什么順序發生不一定,但必定在這些Side Effect全部作用完之后才開始調用`foo`函數。
2、條件運算符?:、逗號運算符、邏輯與&&、邏輯或||的第一個操作數求值之后是Sequence Point。我們剛講過條件運算符和逗號運算符,條件運算符要根據表達式1的值是否為真決定下一步求表達式2還是表達式3的值,如果決定求表達式2的值,表達式3就不會被求值了,反之也一樣,逗號運算符也是這樣,表達式1求值結束才繼續求表達式2的值。
邏輯與和邏輯或早在[第?3?節 “布爾代數”](ch04s03.html#cond.bool)就講了,但在初學階段我一直回避它們的操作數求值順序問題。這兩個運算符和條件運算符類似,先求左操作數的值,然后根據這個值是否為真,右操作數可能被求值,也可能不被求值。比如[例?8.5 “剪刀石頭布”](ch08s05.html#array.scissor)這個程序中的這幾句:
```
ret = scanf("%d", &man);
if (ret != 1 || man < 0 || man > 2) {
printf("Invalid input! Please input 0, 1 or 2.\n");
continue;
}
```
其實可以寫得更簡單(類似于[[K&R]](bi01.html#bibli.kr "The C Programming Language")的簡潔風格):
```
if (scanf("%d", &man) != 1 || man < 0 || man > 2) {
printf("Invalid input! Please input 0, 1 or 2.\n");
continue;
}
```
這個控制表達式的求值順序是:先求`scanf("%d", &man) = 1`的值,如果`scanf`調用失敗,則返回值不等于1成立,||運算有一個操作數為真則整個表達式為真,這時直接執行下一句`printf`,根本不會再去求`man < 0`或`man > 2`的值;如果`scanf`調用成功,則讀入的數保存在變量`man`中,并且返回值等于1,那么說它不等于1就不成立了,第一個||運算的左操作數為假,就會去求右操作數`man < 0`的值作為整個表達式的值,這時變量`man`的值正是`scanf`讀上來的值,我們判斷它是否在[0, 2]之間,如果`man < 0`不成立,則整個表達式`scanf("%d", &man) != 1 || man < 0` 的值為假,也就是第二個||運算的左操作數為假,所以最后求右操作數`man > 2`的值作為整個表達式的值。
&&運算與此類似,`a && b`的計算過程是:首先求表達式`a`的值,如果`a`的值是假則整個表達式的值是假,不會再去求`b`的值;如果`a`的值是真,則下一步求`b`的值作為整個表達式的值。所以,`a && b`相當于“if a then b”,而`a || b`相當于“if not a then b”。這種特性稱為Short-circuit,很多人喜歡利用Short-circuit特性簡化代碼。
3、在一個完整的聲明末尾是Sequence Point,所謂完整的聲明是指這個聲明不是另外一個聲明的一部分。比如聲明`int a[10], b[20];`,在`a[10]`末尾是Sequence Point,在`b[20]`末尾也是。
4、在一個完整的表達式末尾是Sequence Point,所謂完整的表達式是指這個表達式不是另外一個表達式的一部分。所以如果有`f(); g();`這樣兩條語句,`f()`和`g()`是兩個完整的表達式,`f()`的Side Effect必定在`g()`之前發生。
5、在庫函數即將返回時是Sequence Point。這條規則似乎可以包含在上一條規則里面,因為函數返回時必然會結束掉一個完整的表達式。而事實上很多庫函數是以宏定義的形式實現的([第?2.1?節 “函數式宏定義”](ch21s02.html#prep.funcmacro)),并不是真正的函數,所以才需要有這條規則。
還有兩種Sequence Point和某些C標準庫函數的執行過程相關,此處從略,有興趣的讀者可參考[[C99]](bi01.html#bibli.c99 "ISO/IEC 9899: Programming Languages - C")的Annex C。
現在可以分析一下本節開頭的例子了。`a = (++a)+(++a)+(++a)+(++a);`的結果之所以是Undefined,因為在這個表達式中有五個Side Effect都在改變`a`的值,這些Side Effect按什么順序發生不一定,只知道在整個表達式求值結束時一定都發生了。比如現在求第二個`++a`的值,這時第一個、第三個、第四個`++a`的Side Effect發生了沒有,`a`的值被加過幾次了,這些都不確定,所以第二個`++a`的值也不確定。這行代碼用不同平臺的不同編譯器來編譯結果是不同的,甚至在同一平臺上用同一編譯器的不同版本來編譯也可能不同。
寫表達式應遵循的原則一:_在兩個Sequence Point之間,同一個變量的值只允許被改變一次_。僅有這一條原則還不夠,例如`a[i++] = i;`的變量`i`只改變了一次,但結果仍是Undefined,因為等號左邊改`i`的值,等號右邊讀`i`的值,到底是先改還是先讀?這個讀寫順序是不確定的。但為什么`i = i + 1;`就沒有歧義呢?雖然也是等號左邊改`i`的值,等號右邊讀`i`的值,但你不讀出`i`的值就沒法計算`i + 1`,那拿什么去改`i`的值呢?所以這個讀寫順序是確定的。寫表達式應遵循的原則二:_如果在兩個Sequence Point之間既要讀一個變量的值又要改它的值,只有在讀寫順序確定的情況下才可以這么寫_。
## 4.?運算符總結
到此為止,除了和指針相關的運算符還沒講之外,其它運算符都講過了,是時候做一個總結了。
運算符+ - * / % > < >= <= == != & | ^ 以及各種復合賦值運算符要求兩邊的操作數類型一致,條件運算符?:要求后兩個操作數類型一致,這些運算符在計算之前都需要做Usual Arithmetic Conversion。
下面按優先級從高到低的順序總結一下C語言的運算符,每一條所列的各運算符具有相同的優先級,對于同一優先級的多個運算符按什么順序計算也有說明,雙目運算符就簡單地用“左結合”或“右結合”來說明了。和指針有關的運算符* & ->也在這里列出來了,到[第?23?章 _指針_](ch23.html#pointer)再詳細解釋。
1、標識符、常量、字符串和用()括號套起來的表達式是組成表達式的最基本單元,在運算中做操作數,優先級最高。
2、后綴運算符,包括數組取下標[]、函數調用()、結構體取成員“.”、指向結構體的指針取成員->、后綴自增++、后綴自減--。如果一個操作數后面有多個后綴,按照離操作數從近到遠的順序(也就是從左到右)依次計算,比如`a.name++`,先算`a.name`,再++,這里的`.name`應該看成`a`的一個后綴,而不是把`.`看成雙目運算符。
3、單目運算符,包括前綴自增++、前綴自減--、`sizeof`、類型轉換()、取地址運算&、指針間接尋址*、正號+、負號-、按位取反~、邏輯非!。如果一個操作數前面有多個前綴,按照離操作數從近到遠的順序(也就是從右到左)依次計算,比如`!~a`,先算`~a`,再求!。
4、乘*、除/、模%運算符。這三個運算符是左結合的。
5、加+、減-運算符。左結合。
6、移位運算符<<和>>。左結合。
7、關系運算符< > <= >=。左結合。
8、相等性運算符==和!=。左結合。
9、按位與&。左結合。
10、按位異或^。左結合。
11、按位或|。左結合。
12、邏輯與&&。左結合。
13、邏輯或||。左結合。
14、條件運算符:?。在[第?2?節 “if/else語句”](ch04s02.html#cond.ifelse)講過Dangling-else問題,條件運算符也有類似的問題。例如`a ? b : c ? d : e`是看成`(a ? b : c) ? d : e`還是`a ? b : (c ? d : e)`呢?C語言規定是后者。
15、賦值=和各種復合賦值(`*=` `/=` `%=` `+=` `-=` `<<=` `>>=` `&=` `^=` `|=`)。在雙目運算符中只有賦值和復合賦值是右結合的。
16、逗號運算符。左結合。
[[K&R]](bi01.html#bibli.kr "The C Programming Language")第2章也有這樣一個列表,但是對于結合性解釋得不夠清楚。左結合和右結合這兩個概念只對雙目運算符有意義,對于前綴、后綴和三目運算符我單獨做了說明。C語言表達式的詳細語法規則可以參考[[C99]](bi01.html#bibli.c99 "ISO/IEC 9899: Programming Languages - C")的Annex A.2,其實語法規則并不是用優先級和結合性這兩個概念來表述的,有一些細節用優先級和結合性是表達不了的,只有看C99才能了解完整的語法規則。
### 習題
1、以下代碼得到的`sum`是0xffff,對嗎?
```
int i = 0;
unsigned int sum = 0;
for (; i < 16; i++)
sum = sum + 1U<<i;
```
- Linux C編程一站式學習
- 歷史
- 前言
- 部分?I.?C語言入門
- 第?1?章?程序的基本概念
- 第?2?章?常量、變量和表達式
- 第?3?章?簡單函數
- 第?4?章?分支語句
- 第?5?章?深入理解函數
- 第?6?章?循環語句
- 第?7?章?結構體
- 第?8?章?數組
- 第?9?章?編碼風格
- 第?10?章?gdb
- 第?11?章?排序與查找
- 第?12?章?棧與隊列
- 第?13?章?本階段總結
- 部分?II.?C語言本質
- 第?14?章?計算機中數的表示
- 第?15?章?數據類型詳解
- 第?16?章?運算符詳解
- 第?17?章?計算機體系結構基礎
- 第?18?章?x86匯編程序基礎
- 第?19?章?匯編與C之間的關系
- 第?20?章?鏈接詳解
- 第?21?章?預處理
- 第?22?章?Makefile基礎
- 第?23?章?指針
- 第?24?章?函數接口
- 第?25?章?C標準庫
- 第?26?章?鏈表、二叉樹和哈希表
- 第?27?章?本階段總結
- 部分?III.?Linux系統編程
- 第?28?章?文件與I/O
- 第?29?章?文件系統
- 第?30?章?進程
- 第?31?章?Shell腳本
- 第?32?章?正則表達式
- 第?33?章?信號
- 第?34?章?終端、作業控制與守護進程
- 第?35?章?線程
- 第?36?章?TCP/IP協議基礎
- 第?37?章?socket編程
- 附錄?A.?字符編碼
- 附錄?B.?GNU Free Documentation License Version 1.3, 3 November 2008
- 參考書目
- 索引