Menu

C 语言运算符:算术、逻辑运算符和优先级

值得掌握的每个 C 运算符:算术、赋值、比较、逻辑和自增运算符,以及整数除法陷阱、取模运算遇到负数时的行为,还有一张可靠的优先级表。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

运算符是 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 语言中的运算符优先级是什么?

当表达式中没有括号时,各个运算符结合的先后顺序。*、/ 和 % 比 + 和 - 结合得更紧,+ 和 - 又比比较运算符更紧,比较运算符比 && 更紧,然后是 ||,最后是赋值。拿不准时就加括号:它没有任何代价,还能消除疑问。

Coddy 编程语言插图

和 Coddy 一起学编程

开始