C语言-第6章-运算符和表达式
文章目录
-
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 | 乘法类 | * / % | 左结合 | ||
|---|---|---|---|---|---|
| 7 | 大小关系 | < <= > >= | 左结合 | ||
| 8 | 相等关系 | == != | 左结合 | ||
| 12 | 逻辑与 | && | 左结合 | ||
| 13 | 逻辑或 | 左结合 | |||
| 14 | 条件 | ? : | 右结合 | ||
| 15 | 赋值类 | = *= /= %= += -= | 右结合 | ||
| 16 | 逗号 | , | 左结合 |
如果一个表达式中参与的运算符很多,而且没有用小括号强制规定运算先后,那么可以参考优先级和结合性来推断运算顺序
a = b += c++ - d + --e / -f
按照优先级和结合性得到的运算序列是:
a = b += (c++) - d + --e / -fa = 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++;
如果先左后右计算乘号两边的操作数(表达式),会计算22,得到4,i最后会自增1,变为3。但是如果先右后左计算乘号两边的操作数(表达式),右表达式的值为2,但是i的值会自增1,然后取出i的值作为左表达式的值,为3,故会计算32,得到6。
由于C语言标准没有规定子表达式的求值顺序,不同编译器有不同的实现,故类似c = (b = a + 2) - (a = 1);和j = i * i++;这样的语句都会导致“未定义的行为”。应该避免这样的情况发生。因此子表达式中尽量不要出现既访问变量的值, 又修改它的值的情况。
