运算符(上)
运算符
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);