Skip to main content

运算符(上)

运算符

C 语言有非常多的运算符(operator),有些可能你都没意识到属于运算符:

优先级运算符描述结合性
1++ --后缀自增与自减从左到右
1() []函数调用、数组下标从左到右
1. ->成员访问从左到右
1(type){list}复合字面量(C99)从左到右
2++ --前缀自增与自减从右到左
2+ - ! ~正负号、逻辑非、逐位非从右到左
2(type)强制类型转换从右到左
2* &解引用、取地址从右到左
2sizeof _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 < bc < 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);