运算符是 C 的动词:它们接收值并产生新的值。C 大约有 40 个运算符,但其中十几个就涵盖了大部分代码,而这十几个中有两个的陷阱足够危险,值得单独用一节来讲。
算术运算符
(格式字符串中的 %% 会打印一个百分号字面量;单独的 % 会开始一个格式说明符。)
五个二元算术运算符是 + - * / %,另外还有用于取负的一元 -,以及基本上什么也不做的一元 +。
整数除法陷阱
两个整数做 a / b 时,执行的是整数除法,余数会被丢掉。这里没有四舍五入,结果会向零截断。
最后这种情况就是会一直留到生产环境中的 bug。赋值给 double 并没有用:在赋值发生之前,除法 total / count 已经算完了,结果已经是 int。修复方法是在表达式内部把一个操作数变成浮点数,通常用强制类型转换。
还有两条相关的规则:整数除以零是未定义行为,通常会让程序崩溃;INT_MIN / -1 会溢出。
取模运算符
% 给出余数,只能用于整数:
用 x % 2 == 0 判断偶数,以及用 % n 让下标在数组中循环,是两种最常见的日常用法。
遇到负数时,C 的 % 结果的符号与被除数相同:
所以当 x 可能为负数时,x % 2 == 1 不能正确判断奇数:-7 % 2 是 -1。应该改用 x % 2 != 0。
对浮点数求余时,% 会导致编译错误;请使用 math.h 中的 fmod()。
赋值和复合赋值
= 用于存储一个值。复合形式把一个运算和存储结合在一起:
x += 5 的意思是 x = x + 5,但它只对 x 求值一次,当目标是 arr[compute_index()] 这样的东西时,这一点就很重要。
赋值本身也是一个表达式,它的值就是被赋予的值,所以 a = b = c = 0 能正常工作(从右往左依次赋值),这也是为什么误把 == 写成 = 时代码仍能悄无声息地编译通过。
自增和自减
++ 加一,-- 减一。它们都有前缀和后缀两种形式:
作为单独的语句,两者可以互换;在 for 循环头中习惯写 i++,并没有什么深层原因,只是传统。
你绝对不能做的,是在同一个表达式中修改一个变量的同时,又多次使用它:
int i = 5;
int x = i++ + i++; /* undefined behavior */
arr[i] = i++; /* undefined behavior */
printf("%d %d", i++, i);/* undefined behavior */
C 没有规定这些子表达式的求值顺序,所以结果不只是“未指定”,而是整个程序的行为都是未定义的。把语句拆成两条,歧义就消失了。
比较运算符
比较运算符有六个,它们都产生一个 int:真为 1,假为 0。
结果是普通的 int,而不是专门的布尔类型,这是 C 的一个标志性特点。由此带来的影响(包括 = 和 == 的错误)请看“C 语言中的布尔值”一节。
有两种比较错误是 C 特有的。连写比较并不像数学中那样工作:if (1 < x < 10) 永远为真,因为 1 < x 的结果是 0 或 1,然后再拿这个结果和 10 比较。应该写 if (x > 1 && x < 10)。另外,用 == 比较字符串比较的是指针,而不是文本:请使用 string.h 中的 strcmp。
逻辑运算符
&& 是与,|| 是或,! 是非。三者都把任何非零值当作真。
它们会短路求值:&& 一旦遇到为假的操作数就停止,|| 一旦遇到为真的操作数就停止。剩下的部分永远不会被求值,这不只是一种优化,而是可以依赖它来保证安全的语言保证:
如果把这两个判断的顺序调换,传入 NULL 的那次调用就会解引用空指针并崩溃。先检查,再使用:&& 链中的顺序本身就是逻辑的一部分。
条件运算符
C 唯一的三目运算符,用于在两个值之间做选择:
condition ? value_if_true : value_if_false。它是一个表达式,所以可以用在 if 语句无法出现的地方,比如 printf 的参数中或初始化语句中。尽量写得简短;嵌套的条件运算符很快就会变得难以阅读。
位运算符简介
位运算符作用于整数的各个二进制位:
它们用于标志位、掩码和硬件寄存器。有两点需要注意:& 和 | 不是 && 和 ||(它们不会短路,而且是逐位运算);对有符号值做移位,或者移位位数超过类型宽度,都是未定义行为。做位运算时请使用无符号类型。
优先级
当表达式中没有括号时,由这张表决定谁和谁结合。优先级从高到低排列:
| 级别 | 运算符 | 结合性 |
|---|---|---|
| 1 | () [] -> . x++ x-- | 从左到右 |
| 2 | ! ~ ++x --x +x -x *p &x sizeof (type) | 从右到左 |
| 3 | * / % | 从左到右 |
| 4 | + - | 从左到右 |
| 5 | << >> | 从左到右 |
| 6 | < <= > >= | 从左到右 |
| 7 | == != | 从左到右 |
| 8 | & | 从左到右 |
| 9 | ^ | 从左到右 |
| 10 | 按位或 | 从左到右 |
| 11 | && | 从左到右 |
| 12 | 逻辑或 | 从左到右 |
| 13 | ?: | 从右到左 |
| 14 | = += -= *= /= %= 等 | 从右到左 |
| 15 | , | 从左到右 |
实际影响如下:
a + b * c /* means a + (b * c) */
a < b == c < d /* means (a < b) == (c < d) - rarely what you meant */
x & 1 == 0 /* means x & (1 == 0), i.e. x & 0 - a classic bug */
*p++ /* means *(p++) - dereference p, then advance it */
第三行值得记住:== 比 & 结合得更紧,所以位掩码判断需要加括号,写成 (x & 1) == 0。
没有人能记住全部十五个级别,也没有必要。只要记住 * 优先于 +,比较运算符优先于 &&,&& 优先于 ||,其他情况一律加括号:
if ((flags & MASK) != 0 && (count > 0 || force)) { ... }
这一行有一些多余的括号,但正因为有了它们,代码才更好。
常见问题
C 语言中 % 是做什么的?
% 是取模运算符:它给出整数除法的余数。17 % 5 是 2。它只能用于整数:用在 float 或 double 上会导致编译错误,浮点数对应的是 math.h 中的 fmod()。
为什么在 C 语言中 5 / 2 的结果是 2?
因为两个操作数都是整数,所以 C 执行整数除法,丢掉小数部分。要得到 2.5,把其中一边变成浮点数:5 / 2.0,或者对一个操作数进行强制转换:(double)a / b。
i++ 和 ++i 有什么区别?
两者都会让 i 加一。++i(前缀)先自增,再返回新值;i++(后缀)先返回旧值,再自增。作为单独的语句,两者完全相同;只有在使用结果时才有区别,例如 int b = a++; 和 int b = ++a;。
C 语言中的运算符优先级是什么?
当表达式中没有括号时,各个运算符结合的先后顺序。*、/ 和 % 比 + 和 - 结合得更紧,+ 和 - 又比比较运算符更紧,比较运算符比 && 更紧,然后是 ||,最后是赋值。拿不准时就加括号:它没有任何代价,还能消除疑问。