C语言:运算符以及++的注意事项

一、运算符

1.1 优先级和结合律

y = 6 * 12 + 5 * 20;
当运算符共享一个运算对象时, 优先级决定了求值顺序。 乘法的优先级最高,所以先进行乘法运算。即,先进行两个乘法运算6 * 12和5 * 20, 再进行加法运算,最后进行赋值运算。
但是, 优先级并未规定到底先进行哪一个乘法。 C 语言把主动权留给语言的实现者, 根据不同的硬件来决定先计算前者还是后者。

当共享一个运算对象时,两边的运算符的优先级相同,结合律决定求值顺序。
例如, 在表达式12 / 3 *2中, 乘和除运算符的优先级相同, 共享运算对象3, 乘和除的结合律方向都是从左往右,因此, 结合律在这种情况起作用。

计算a=1×2+3×4+5×6
在这里插入图片描述
C语言通过优先级和结合律明确求值顺序,如纵向所示。对于横向三个乘法的子表达式,C语言并没有明确规定先计算哪个乘法。

小结:
1、表达式中,先看优先级,如果优先级相同再看结合律,最后决定求值顺序。
2、当共享一个运算对象时,用优先级和结合律决定执行顺序,当共享一个运算符时(即双目运算符,除逗号运算符、逻辑与运算符、逻辑或运算符外),编译器随机选择运算对象。

1.2 运算符优先级和结合律一览表

优先级运算符名称或含义使用方式结合方向说明
1[]数组下标数组名[常量表达式]从左往右
()圆括号(表达式)
函数名(形参表)
.成员选择(对象)对象.成员名
->成员选择(指针)对象指针->成员名
++(后缀)自增运算符变量++
--(后缀)自减运算符变量--
2-负号运算符-表达式从右往左单目运算符
++(前缀)自增运算符++变量名单目运算符
--(前缀)自减运算符--变量名单目运算符
*取值运算符*指针变量单目运算符
&取地址运算符&变量名单目运算符
逻辑非运算符!表达式单目运算符
~按位取反运算符~表达式单目运算符
sizeof长度运算符sizeof(表达式)
3(类型)强制类型转换(数据类型)表达式从右往左
4/表达式 / 表达式从左往右双目运算符
*表达式 * 表达式双目运算符
%余数(取模)整型表达式 % 整型表达式双目运算符
5+表达式 + 表达式从左往右双目运算符
-表达式 - 表达式双目运算符
6<<左移变量 << 表达式从左往右双目运算符
>>右移变量 >> 表达式双目运算符
7>大于表达式 > 表达式从左往右双目运算符
>=大于等于表达式 >= 表达式双目运算符
<小于表达式<表达式双目运算符
<=小于等于表达式 <= 表达式双目运算符
8==等于表达式 == 表达式从左往右双目运算符
!=不等于表达式 != 表达式双目运算符
9&按位与表达式 & 表达式从左往右双目运算符
10^按位异或表达式 ^ 表达式从左往右双目运算符
11|按位或表达式 | 表达式从左往右双目运算符
12&&逻辑与表达式 && 表达式从左往右双目运算符
13||逻辑或表达式 || 表达式从左往右双目运算符
14?:条件运算符表达式1 ? 表达式2 : 表达式3从右往左三目运算符
15=赋值运算符变量 = 表达式从右往左
/=除后赋值变量 /= 表达式
*=乘后赋值变量 *= 表达式
%=取模后赋值变量 %= 表达式
+=加后赋值变量 += 表达式
-=减后赋值变量 -= 表达式
<<=左移后赋值变量 <<= 表达式
>>=右移后赋值变量 >>= 表达式
&=按位与后赋值变量 &= 表达式
^=按位异或后赋值变量 ^= 表达式
|=按位或后赋值变量 |= 表达式
16,逗号运算符表达式,表达式,…从左往右

对于优先级:算术运算符 > 关系运算符 > 逻辑运算符 > 赋值运算符。逻辑运算符中“逻辑非 !”除外。

二、关于++的注意事项

2.1 为什么++的代码效率更高?

递增运算符具有代码紧凑、效率高等优点。递增运算符之所以效率高,是因为它和实际的机器语言指令很相似,所以一条C语句转换成的机器码相对较少。尽管如此, 随着商家推出的C编译器越来越智能, 这一优势可能会消失。 一个智能的编译器可以把x = x + 1当作++x对待。

x=x+1, x+=1, x++哪个效率更高?
说法1:
x=x+1最低,因为它的执行过程如下:
(1)读取右x的地址。
(2)x+1
(3)读取左x的地址。
(4)将右值传给左边的x(编译器并不认为左右x的地址相同)。

x+=1其次,其执行过程如下:
(1)读取右x的地址。
(2)x自增1。
(3)将得到的值传给x(因为x的地址已经读出)。

x++效率最高,其执行过程如下:
(1)读取右x的地址。
(2)x自增1。

说法2:
这3条语句都应该对应两次访问内存操作,一次加法操作,所以效率应该一样。

2.2 ++的优先级

++出现在其作用的变量前面,这是前缀模式; ++出现在其作用的变量后面, 这是后缀模式。
++在使用时,前缀和后缀的优先级是不同的。
后缀++的优先级 > () > 前缀++的优先级

++的错误用法:(x+y)++;
因为递增和递减运算符只能影响一个变量(或者, 更普遍地说, 只能影响一个可修改的左值),而组合x*y本身不是可修改的左值。

2.3 一个语句中用太多递增运算符

情况一:

printf("%10d %10d\n", num, num*num++);

理想中的:打印num, 然后计算num*num得到平方值, 最后
把num递增1。
但事实上, 修改后的程序只能在某些系统上能正常运行。
该程序的问题是:当 printf()获取待打印的值时,可能先对最后一个参数求值, 这样在获取其他参数的值之前就递增了num。
所以, 本应打印:5 25
却打印成:6 25
它甚至可能从右往左执行, 对最右边的num(++作用的num) 使用5, 对
第2个num和最左边的num使用6, 结果打印出:6 30

在C语言中, 编译器可以自行选择先对函数中的哪个参数求值。 这样做提高了编译器的效率, 但是如果在函数的参数中使用了递增运算符, 就会有一些问题。

相似的例子:
printf("%10d %10d\n", num, num++);
printf("%10d %10d\n", num, num=1);

情况二:

ans = num/2 + 5*(1 + num++);

同样, 该语句的问题是: 编译器可能不会按预想的顺序来执行
你可能认为, 先计算第1项(num/2) , 接着计算第2项(5*(1 + num++))
但是,编译器可能先计算第2项, 递增num, 然后在num/2中使用num递增后的新值。 因此, 无法保证编译器到底先计算哪一项。

情况三:

n = 3;
y = n++ + n++;

可以肯定的是,执行完这两条语句后,n的值会比旧值大2。
但是,y的值不确定。
1、编译器可以使用n的旧值(3)两次,然后把n递增1两次,这使得y的值为6,n的值为5。
2、编译器使用n的旧值(3)一次,立即递增n,再对表达式中的第2个n使用递增后的新值,然后再递增n,这使得 y 的值为 7,n 的值为5。

遵循以下规则, 很容易避免类似的问题:
1、如果一个变量出现在一个函数的多个参数中,不要对该变量使用递增或递减运算符;
2、如果一个变量多次出现在一个表达式中,不要对该变量使用递增或递减运算符。

小结:

(表达式1+(表达式2)
例如:
int a = 1;
int b = 2;
int c = 3;
int d = 4;
(a + b)+ (c + d);

这种表达式1的优先级和表达式2的优先级相同,编译器对于先计算哪一个是随机的,如果表达式1和表达式2中的内容没有联系,那么无论是哪个先计算,结果都是正确的。

如果表达式1和表达式2存在共同的变量且该变量有赋值操作(++或=),那么不清楚哪个表达式先计算,不清楚什么时候赋值,最终造成计算结果的不确定。
针对这种情况下,有些编译器会报警告:
warning: operation on ‘i’ may be undefined [-Wsequence-point]

何时执行递增?副作用和序列点

三、参考资料

[1]: C Primer Plus 第六版
[2]: x=x+1,x+=1,x++效率的两种说法


版权声明:本文为weixin_42258222原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。