文章目錄
- 6.1 算術運算符
-
- 6.1.1 二進制算術運算符詳解
- 6.1.2 運算符的優先級和結合性
- 6.2 指派運算符
-
- 6.2.1 簡單指派
- 6.2.2 複合指派
- 6.3 自增運算符和自減運算符
- 6.4 關系運算符和邏輯運算符
- 6.5 逗号運算符
- 6.6 表達式求值
6.1 算術運算符
算術運算符可以執行加、減、乘、除、取餘運算。比如常見的
2 + 10 * 5,+和*運算符都是二進制運算符,因為運算符需要兩個操作數。
二進制算術運算符:
操作數個數 | 加法類 | 乘法類 |
---|---|---|
二進制算術運算符 | + 加法運算符 - 減法運算符 | * 乘法運算符 / 除法運算符 % 求餘運算符 |
一進制算術運算符 | +一進制正号運算符 -一進制負号運算符 |
除了二進制算術運算符,像-5這樣的式子中的減号-也屬于一種運算符,其作用是取操作數的相反數,由于操作數隻有一個,是以是一進制算術運算符。
比如,
i = +1;
j = -1;
一進制運算符+什麼都不做。它主要強調某數值是正的。
6.1.1 二進制算術運算符詳解
- 與數學中/運算符不同,C語言中整數除以整數的結果是整數,也就是說整數除以整數會丢棄小數部分,隻保留整數部分。比如,1 / 2 結果是0而不是0.5,-10 / 4結果是-2而不是-2.5。
- 運算符%要求兩個操作數都是整數,如果有一個不是整數,程式将無法編譯通過。運算結果是兩個操作數相除的餘數。比如,10 % 3的餘數是1,而12 % 4的餘數是0。
- 除%運算符以外,二進制算術運算符允許操作數都是整數、都是浮點數或者是整數和浮點數的混合。當把int型操作數和float型操作數混合在一起時,運算結果是float型的。是以,9 + 2.5f的值為11.5,而6.7f / 2的值為3.35。
- /和%運算符如果右操作數是零會出現未定義的行為。
- 當運算符/和%用在負數上時,其結果根據環境有不同。在C89标準下,如果兩個操作數中有一個是負數,除法的結果既可以向上取整也可以向下取整。比如-9/7的結果既可能為-1也可能為-2。是以-9%7的結果也是不定的,既可能是-2也可能是5。這樣的現象叫由實作定義的行為。在C99标準下,除法的結果總是向零截取,是以i%j的值的符号和i相同。比如-9 / 7結果為-1,-9 % 7結果為-2。注意,任何标準下,/和%運算結果都有如下關系:如果a/b和a%b有意義,那麼 (a/b)*b + a%b應該等于a。
6.1.2 運算符的優先級和結合性
類似于數學中先算乘除後算加減,C語言規定了運算符的優先級。
算術運算符的優先級如下:
最高優先級: + - (一進制運算符)
較高優先級:* % / (二進制運算符)
最低優先級:+ - (二進制運算符)
如果要改變預設運算符的運算順序,可以添加()。比如i + j * k按照運算符優先級先算j * k,然後i再和結果相加。圓括号的加入可以修改運算順序為先算加法,後算乘法:(i + j) * k
一些表明運算符優先級的例子:
i + j * k 等價于 i + (j * k )
-i * -j 等價于 (-i) * (-j)
+i + j / k 等價于 (+i) + (j / k)
如果表達式中包含多個優先級一樣的運算符,那麼還要确定是從左往右算還是從右往左算。這稱為運算符的結合性。如果運算符是從左往右算的,那麼稱這種運算符是左結合性,否則是右結合的。二進制算術運算符(+、 -、 *、 / 和 %)都是左結合的。是以
i - j - k 等價于 (i - j) - k
i * j / k 等價于 (i * j) / k
一進制算術運算符+和-是右結合的,比如
- + i 等價于 - (+i)
下節介紹的指派運算符=也是右結合的,比如
x = y = z = 10 等價于 x = (y = (z = 10))
運算符太多了,是以記住所有運算符的優先級和結合性可能不現實。可以在使用的時候參考手冊,比如C 中的運算符優先級,也可以用圓括号強制規定運算順序。
6.2 指派運算符
求出表達式的值之後常常需要将其存儲到變量中,以便将來使用。指派運算符
=
可以用作此目的。為了基于變量中原先的值更新變量中的值,C語言還提供了若幹複合指派運算符。
6.2.1 簡單指派
表達式
v = e
的指派效果是求出表達式
e
的值,并把此值賦給
v
。如下面的例子所示,
e
可以是常量、變量或者更為複雜的表達式。
i = 5; // i為5
j = i; // j為5
k = 10 * i + j; // k為55
如果
v
和
e
的類型不同,那麼指派運算發生時還是先求出表達式
e
的值,然後将該值轉化為
v
的類型再賦給
v
。說起來比較拗口,用例子比較直覺:
int i;
float f;
i = 72.6f; // i是72
f = 30; // f是30.0
這裡要強調的是C語言是強類型的語言,整數72和浮點數72.0在記憶體中的存儲方式是不同的,是以不同類型的指派會包含值的轉換工作。通過例子可以看到,C語言的轉換規則(效果)是盡量符合數學規則。指派過程中 右邊表達式的值可能會超出左邊變量值的範圍,比如
int i;
printf("%f\n", 1e10);
i = 1e10;
printf("%d\n", i);
大多數C語言運算符允許它們的操作數是變量、常量或者包含其它運算符的表達式。然而指派運算符要求它的左操作數必須是左值。左值表示存儲在計算機記憶體中值可以覆寫的對象。變量是左值,而如10或者2 * i這樣的表達式則不是左值。目前為止,變量是已知的唯一左值,後續章節中還 會出現其它樣式的左值。
如下指派運算符的使用是不合法的:
12 = i; // 錯誤,左操作數不是左值
i + j = 0; // 錯誤,左操作數不是左值
-i = j; // 錯誤,左操作數不是左值
在C語言中,指派就像+那樣是運算符。換句話說,指派操作産生結果,就像兩個數相加産生結果一樣。也就是說,指派操作符=構成了指派表達式,表達式
v=e
的值就是指派運算後
v
的值。比如,表達式
i = 10
的值是10, 語句
j = (i = 10) + 2;
使j的值為12。
注意,如果
i
為整型變量,表達式
i = 72.6f
的值是72。
由于指派是運算符,那麼多個指派可以串聯在一起:
i = j = k = 0;
指派運算符=是右結合的,故上述指派表達式等價于:
i = (j = (k = 0));
即先把0指派給k,
k=0
的結果0再指派給j,
j=k=0
的結果0再指派給i。
指派運算符的優先級基本是最低的了(隻高于逗号
,
運算符),是以
i = 2 * j;
會被解釋成
i = (2 * j);
而不是
(i = 2) * j;
6.2.2 複合指派
在變量原值基礎上進行運算再把計算結果指派給變量的操作是不少的,比如
i = i + 2;
。是以C語言中出現了複合指派運算符來縮短這樣的語句。使用複合指派運算符+=可以将寫為
i += 2;
+= 運算符将右操作數的值加到左邊的變量中。
注意,+=中
+
和
=
号要寫在一起,中間不能有空格。
複合指派運算符還包括
-= *= /= %=
所有複合指派運算符的工作原理大體相同。
運算符 | 例子 | 相當于 |
---|---|---|
+= | i += 8; | i = i + 8; |
-= | i -= 8; | i = i - 8; |
*= | i *= 8; | i = i * 8; |
/= | i /= 8; | i = i / 8; |
%= | i %= 8; | i = i % 8; |
複合指派運算符與指派運算符的優先級一樣是很低的,隻高于逗号
,
運算符,故像下面這樣的語句
i *= j + k;
相當于
i = i * (j + k);
而不是
(i = i * j) + k;
與指派運算符一樣,複合指派運算符
v += e
構成的表達式的值是複制後
v
的值,故表達式
j += k
的值是
j = j + k
指派後
j
的值。
複合指派運算符與指派運算符一樣,是右結合的,故語句
i += j += k;
相當于
i += (j += k);
6.3 自增運算符和自減運算符
變量的自增(變量值增加1)和自減(變量值減少1)在某些場景下還是很常見的,可以用指派運算符來做的
i = i + 1;
也可以用複合指派縮短語句
i += 1;
C語言中用自增運算符可以實作的更簡潔
i++;
注意
++
以及
--
兩個符号要寫在一起,中間不能有空格。
++
自增運算的效果就是讓變量的值增加1,
--
自減運算的效果讓變量值減1。但是麻煩的是自增
++
可以作為字首(++i),也可以作為字尾(i++),其含義是不同的。下面以自增運算來說明,自減也是一樣的。
自增運算符 | 例子 | 含義 | 示範 |
---|---|---|---|
字首++ | ++i | 先對i自增1,然後使用i的值 | int i = 1;printf("%d", ++i); // 輸出2 |
字尾++ | i++ | 先使用i的值,然後i自增1 | int i = 1;printf("%d", i++); // 輸出1 |
就像複合指派運算符
i += 1;
是表達式,有值一樣,表達式
++i
和
i++
也是有值的,但是
++
作為字首的表達式
++i
的值是自增完1之後的
i
的值作為整個表達式的值,而
++
最為字尾的表達式
i++
的值是自增之前
i
的值作為整個表達式的值。當然,即使
++
放在後面,最後
i
還會自增1的。比如
int i = 0, j = 0, k;
k = i++;
printf("%d %d", k, i); // 輸出0 1
k = ++j;
printf("%d %d", k, j); // 輸出1 1
k = i++;
由于
++
放在i的後面,故表達式
i++
的值是目前
i
的值參與指派運算,故0被賦給了
k
,這之後i的值才自增,自增後i的值為1。
k = ++j;
由于
++
放在
j
的前面,故先對
j
進行自增運算,
j
的初始值為0,自增後為1,此時
j
的值作為
++j
的值參與指派運算,故1被指派給了k。
自減運算符和自增運算符含義類似,
自減運算符 | 例子 | 含義 | 示範 |
---|---|---|---|
字首– | –i | 先對i自減1,然後使用i的值 | int i = 1;printf("%d", --i); // 輸出0 |
字尾++ | i– | 先使用i的值,然後i自增1 | int i = 1;printf("%d", i–); // 輸出1 |
i = 1;
j = 2;
k = ++i + j++;
printf("i是%d,j是%d,k是%d\n"); // 輸出:i是2,j是3,k是4
k = ++i + j++;
等價于
i = i + 1;
k = i + j;
j = j + 1;
雖然++或–運算符簡化了表達式,但是在表達式中多個++或–運算符會讓表達式有些難以了解。建議寫代碼的時候隻使用自增、自減運算符對變量的值進行自增、自減,而不使用自增和自減表達式的值。
// 建議的用法:
i++;
j--;
不建議寫像下面這樣的語句
// 有點難以了解的用法:
k = ++i + j++;
否則,盡管代碼量可能有減少,但是代碼的可讀性會變差。
需要注意,字尾++和字尾–比一進制的正号、負号優先級高,而且都是左結合的。字首++和字首–與一進制的正号、負号優先級相同,而且都是右結合的。
6.4 關系運算符和邏輯運算符
在選擇章節中我們以及介紹了關系運算符和邏輯運算符。這裡再次說明,C語言中沒有邏輯類型,已經邏輯值真和假,關系運算和邏輯運算如果為真,則表達式值為1,如果為假,表達式值為0。而在選擇和循環條件裡面,非零值表示真,零表示假。
i = (5 > 2) + 1; // 相當于 i = 1 + 1;
i = 5 > 4 && 3 < i; // 相當于 i = 0;
當然,這裡隻是舉個例子,實際程式裡面關系運算或者邏輯運算主要用在選擇或者循環條件裡面,很少對它們再進行算術運算。
關系運算和邏輯運算的優先級具有不同的優先級,邏輯運算符具有“短路”特性。詳見關系運算符和邏輯運算符。
6.5 逗号運算符
在C語言中,逗号運算符組成了逗号表達式,循環語句中的for循環語句中可能會出現逗号運算符。
逗号運算符構成的逗号表達式的一般形式:
表達式1, 表達式2, ...
逗号表達式先計算表達式1的值,然後計算表達式2的值,…,直到算出最後一個表達式的值。既然逗号運算符組成了逗号表達式,那麼整個逗号表達式的值是什麼呢?就是最後一個表達式的值,作為整個逗号表達式的值。
逗号運算符組成逗号表達式的例子:
x = 3, y = 4, z = x + y
逗号運算符的優先級是最低的,比指派運算符還低,故上式運算順序為
(x = 3), (y = 4), (z = x + y)
逗号運算符的結合性是左結合的,故上式先計算
x = 3
,再計算
y = 4
,再計算
z = x + y
。
整個表達式的值是最後一個表達式的值,即
z = x + y
的值,這個指派語句的值是賦完值後變量
z
的值,是7。
是以
int x, y, z, t;
t = (x = 3, y = 4, z = x + y);
printf("%d\n", t);
将輸出7。
6.6 表達式求值
下面給出了到目前為止我們學過的運算符的優先級和結合性。注意,該表隻列出了到目前我們學過的運算符,還有其它運算符并沒有列出,是以這裡的優先級有缺失。
優先級 | 類别 | 運算符 | 結合性 |
---|---|---|---|
1 | 自增(字尾) 自減(字尾) | ++ -- | 左結合 |
2 | 自增(字首) 自減(字首) 一進制正号 一進制減号 邏輯非 | ++ -- + - ! | 右結合 |
4 | 乘法類 | * / % | 左結合 |
5 | 加法類 | + - | 左結合 |
7 | 大小關系 | < <= > >= | 左結合 |
8 | 相等關系 | == != | 左結合 |
12 | 邏輯與 | && | 左結合 |
13 | 邏輯或 | || | 左結合 |
14 | 條件 | ? : | 右結合 |
15 | 指派類 | = *= /= %= += -= | 右結合 |
16 | 逗号 | , | 左結合 |
如果一個表達式中參與的運算符很多,而且沒有用小括号強制規定運算先後,那麼可以參考優先級和結合性來推斷運算順序
a = b += c++ - d + --e / -f
按照優先級和結合性得到的運算序列是:
-
a = b += (c++) - d + --e / -f
-
a = b += (c++) - d + (--e) / (-f)
-
a = b += (c++) - d + ((--e) / (-f))
-
a = b += ((c++) - d) + ((--e) / (-f))
-
a = b += (((c++) - d) + ((--e) / (-f)))
-
a = (b += (((c++) - d) + ((--e) / (-f))))
-
(a = (b += (((c++) - d) + ((--e) / (-f)))))
子表達式的求值順序
盡管根據不同的運算符有不同的優先級可以規定運算順序,但是某些表達式的值可能與子表達式的求值順序有關。這裡子表達式指一個總的運算符構成的表達式中操作數可能由表達式組成,它們稱為子表達式。比如
(a + b) * (c - d)
這個表達式由+運算符組成加法運算,但是左右操作數都由表達式組成,分别是
(a + b)
和
(c - d)
。
C語言除了對邏輯與運算符、邏輯或運算符、條件運算符以及逗号運算符中的子表達式定義了求值順序(都是先左後右子表達式的順序),對其它運算符都沒有定義子表達式的求值順序。而一般情況下子表達式的求值順序對于整個表達式的值也沒有影響,比如上例
(a + b) * (c - d)
。不論先計算的是
(a + b)
還是
(c - d)
結果都是一樣的。但是在某些情況下,當子表達式改變了某個操作數的值時,最終表達式的值可能就不同了。比如
a = 5;
c = (b = a + 2) - (a = 1);
第二條語句的結果是未定義的,C标準沒有做規定。對大多數編譯器而言,c的值是6或者2。如果先計算子表達式
(b = a + 2)
,那麼
b
的值為7,
a
的值為1,
c
的值為6,如果先計算子表達式
a = 1
,那麼
b
的值為3而
c
的值為2。
為了避免出現此類問題,一個方法是不在子表達式中使用指派運算符,需要指派的話可以分成多條語句來完成
a = 5;
b = a + 2;
a = 1;
c = b - a;
這樣
c
的值始終是6。
除了指派運算符,還有自增和自減運算符可能會出現類似的問題。在下面的例子中,j有兩個可能的值:
i = 2;
j = i * i++;
如果先左後右計算乘号兩邊的操作數(表達式),會計算2*2,得到4,i最後會自增1,變為3。但是如果先右後左計算乘号兩邊的操作數(表達式),右表達式的值為2,但是i的值會自增1,然後取出i的值作為左表達式的值,為3,故會計算3*2,得到6。
由于C語言标準沒有規定子表達式的求值順序,不同編譯器有不同的實作,故類似
c = (b = a + 2) - (a = 1);
和
j = i * i++;
這樣的語句都會導緻“未定義的行為”。應該避免這樣的情況發生。是以子表達式中盡量不要出現既通路變量的值, 又修改它的值的情況。