运算符(上)
运算符
C 语言有非常多的运算符(operator),有些可能你都没意识到属于运算符:
优先级 | 运算符 | 描述 | 结合性 |
---|---|---|---|
1 | ++ -- | 后缀自增与自减 | 从左到右 |
1 | () [] | 函数调用、数组下标 | 从左到右 |
1 | . -> | 成员访问 | 从左到右 |
1 | (type){list} | 复合字面量(C99) | 从左到右 |
2 | ++ -- | 前缀自增与自减 | 从右到左 |
2 | + - ! ~ | 正负号、逻辑非、逐位非 | 从右到左 |
2 | (type) | 强制类型转换 | 从右到左 |
2 | * & | 解引用、取地址 | 从右到左 |
2 | sizeof _Alignof | 取大小、对齐要求(C11) | 从右到左 |
3 | * / % | 乘法、除法、余数 | 从左到右 |
4 | + - | 加法、减法 | 从左到右 |
5 | << >> | 左移、右移 | 从左到右 |
6 | < <= > >= | 小于、小于等于、大于、大于等于 | 从左到右 |
7 | == != | 等于、不等于 | 从左到右 |
8 | & | 按位与 | 从左到右 |
9 | ^ | 按位异或 | 从左到右 |
10 | | | 按位或 | 从左到右 |
11 | && | 逻辑与 | 从左到右 |
12 | || | 逻辑或 | 从左到右 |
13 | ?: | 条件 | 从右到左 |
14 | = | 简单赋值 | 从右到左 |
14 | += -= *= /= %= | 算术运算复合赋值 | 从右到左 |
14 | <<= >>= &= ^= |= | 位运算复合赋值 | 从右到左 |
15 | , | 逗号 | 从左到右 |
优先级与结合性
运算符优先级和结合性是最常见的容易犯错的地方。在解析一个表达式的时候,优先级是首先应该考虑的因素。在优先级相同的情况下,我们再讨论结合性。
运算符的优先级大致可以这样记忆:后缀>前缀>乘除余>加减>位移>比较>位运算>条件>赋值>逗号。
至于结合性,是很容易误解的一个概念。我们日常生活中进行的运算都是左结合的,也就是从左到右计算。例如1 + 2 - 3
的含义是(1 + 2) - 3
而不是1 + (2 - 3)
。C 语言也在这方面保持了一致。问题出在我们日常生活中没遇到的运算符,以及平时没当作运算符的运算符。
对于前缀、后缀运算符,因为其性质决定,所以必然只能右结合、左结合。毕竟a[1][1]
解读为a([1][1])
,**a
解读为(**)a
也完全没有意义嘛。不过多个连续的符号,是有可能解读为一个符号的,例如++a
显然不会是+(+a)
,但这就不是结合性的问题,而是符号的问题了。
结合性最典型的示例就是赋值运算符,因为它的语义就是从右往左赋值,因此自然也是从右往左计算的。a = b = c
的含义是a = (b = c)
而不会是(a = b) = c
,就是这个道理。
下面分别详细讲一下这些运算符。
前缀和后缀运算符
首先应该注意到后缀运算符至高无上的地位,他比前缀运算符还要高。所以*p.a
、*p(a)
、*p[a]
、*p++
实际上都是*(p.a)
、*(p(a))
、*(p[a])
、*(p++)
。在进行指针方面的操作的时候尤其需要注意到这一点。
不过考虑到经常使用,C 语言还提供了更简单的方法:对于指向结构的指针p
来说(*p).a
可以写作p->a
,而对于指向函数的指针p
来说(*p)(a)
则可以直接写作p(a)
。
数组下标运算a[n]
实际上等价于(*(a+(n)))
。根据加法交换律,你实际上可以可以写成n[a]
。然而与此同时,请注意细品这三个括号的意蕴。例如,如果你把p[-1]
转写成-1[p]
,那么它的含义就悄悄地变成了-(1[p])
,这是完全不同的含义。此外这种写法在括号更多的时候会碰撞出更多的火花:
int a[] = {1, 2, 3, 4, 5};
int b[] = {6, 7, 8, 9, 10};
2[a][b]; // == 9
sizeof
和类型转换也是运算符,你意识到了吗?如果右侧是表达式,sizeof
是可以不加括号的,例如:
size_t sz = sizeof 0; // sz == sizeof(int)
但是如果是类型,就必须加括号,如sizeof(int)
。目前一切安好,但是你考虑过下面这样的表达式吗:
sizeof (int) * p
它实际上的含义是(sizeof(int)) * p
而不是sizeof((int) *p)
者。你可能觉得这里违背了结合性原则,但实际上,此处sizeof(int)
中的sizeof
根本不是参与表达式解析的运算符了。不过这不影响 C99 的复合字面量,下面的表达式
sizeof (int){0}
的含义仍然是
sizeof((int){0})
综上所示,无论sizeof
后面跟的是什么,括号总能帮助我们很好的消除歧义。
++
和--
运算符既有前缀形式,又有后缀形式,优先级上,始终秉持先后缀再前缀的原则。例如++*p++
便是++(*(p++))
的意思。
需要注意的是,如果你打了奇数个加号,是不能通过编译的。根据贪心法,+++i
会被解读为++(+i)
,而+i
是右值不是左值(这可能算得上是单目加号为数不多的作用之一)。这可能与你的直觉相悖,不是说单目运算符是右结合的吗?还是那句话,这已经不是结合性的问题,而是符号中讨论的问题了。
在 C 语言中,即时没有参数,调用函数也一定需要一个括号:
f(); // 调用无参函数 f
f; // 什么也不做
不带括号的函数名字,还可以取,或退化成它自己的地址,成为函数指针。参见这里。
比较运算符
C 语言的比较运算符是比较朴素的,表达式
a < b < c
的实际含义是
(a < b) < c
也就是说,把a < b
的结果,与c
作比较。如果真实的意图是想要判断它们是否严格升序,正确的写法应该是
a < b && b < c
而下面这段代码
a < b == c < d
的含义可能更为出乎意料。虽然都是比较运算符,但是他们的优先级并 不都是一样的!所以这个表达式的等价于
(a < b) == (c < d)
它表示的意思是要么a < b
和c < d
同时成立,要么同时不成立的时候,表达式为真,即”逻辑同或“,类似的,可以用!=
来作为逻辑异或运算符。不过在这样使用的时候,还是尽可能打上括号,便于理解。
赋值运算符
=
与 ==
对于初学者来说容易犯的一个错误就是混淆比较与赋值运算符。在 C 语言里,=
是赋值运算符,==
才是比较操作数是否相等。例如下面的代码,实际上是把5
赋值给x
,接着判断x
是否非零。
if (x = 5)
printf("x = 5");
再看下面的一个例子,它的本意是跳过文件中的空格、制表符和换行符。
while (c = ' ' || c == '\t' || c == '\n')
c = getc(f);
它实际上是吧 ' ' || c == '\t' || c == '\n'
的值赋值给c
,接着判断c
是否非零,而这一定是真的!
解决上面这个问题的一种方法是把常量放置在左侧:
if (5 = x) // ERROR!
printf("x = 5");
如果你真的想要在条件语句赋值,你可以写成下面这样:
while ((ch = *p++) != 0)
...
实际上,对于任何整数作为条件,!= 0
都能作为最好的注解。
但是要注意,这里的括号是重中之重。这是因为,赋值运算符的优先级比不等于号低。如果不加,写成
while (ch = *p++ != 0)
...
就会被解读为
while (ch = (*p++ != 0))
...
在这里,你或许可以省略为
while (ch = *p++)
...
但是在进行文件操作的时候,你可能会遇到与EOF
的比较,就不能作此简写,这个时候你就不得不小心了。
此外,在 C++ 中还有另一种更为人接受的方式:
while (char ch = *p++)
...
这很明显是赋值(更准确的说,初始化)而不是比较了。注意,别忘了它声明的变量及其作用域。
反过来说,如果你把=
不慎写成了==
,一般也不会引发编译错误,但你的语句便不会产生任何效果。
复合赋值运算符
无论原运算符是什么优先级,复合赋值运算符都是同一等级的,且为右结合。例如
a *= b += c;
应该是
a *= (b += c);
而不是
(a *= b) += c;
此外,因为较低的优先级,可以认为,复合赋值运算符自带了一个“括号的语义”,例如
a *= b + c;
不能直接转写为
a = a * b + c;
而是应该写作
a = a * (b + c);