这一章在干嘛?
把 C 的操作符一网打尽,再讲清两件容易被忽略的事:表达式求值时的 隐式类型转换 ,以及 优先级 ≠ 求值顺序 ——后者是未定义行为重灾区。
5.1 操作符全景
| 家族 | 成员 | 要点 |
|---|---|---|
| 算术 | + − * / % | 整数除法向零截断;% 只用于整型 |
| 移位 | << >> | 左移右补 0;右移有符号数左边补符号位(算术移位);移位数 ≥ 位宽是未定义 |
| 位操作 | & ^ | ~ | 清零用 &、置位用 |、翻转用 ^;常用于寄存器操作 |
| 赋值 | = += −= <<= … | 复合赋值少打字且语义精确;赋值表达式本身有值(= 右结合) |
| 单目 | ! ~ ++ −− + − * & sizeof | ++ 前置先加后用、后置先用后加;sizeof 是编译期求值 |
| 关系 | > >= < <= == != | 结果是 0 或 1;== 和 = 手滑是最经典的 bug |
| 逻辑 | && || | 短路求值:左边已定生死就不看右边;保证先算左边 |
| 条件 | ?: | 唯一三目操作符;「表达式版的 if-else」 |
| 逗号 | , | 从左到右依次求值,取最右边的值;for 步进里常见 |
| 下标/调用/成员 | [] () . -> | 优先级最高的一档;a[i] 就是 *(a+i) |
/* 位操作的寄存器三板斧 */
reg &= ~(1u << n); /* 清第 n 位 */
reg |= (1u << n); /* 置第 n 位 */
reg ^= (1u << n); /* 翻转第 n 位 */
5.2 布尔值、左值与右值
C 没有真正的布尔类型(C99 前教科书时代的写法):零为假、一切非零为真。比较和逻辑操作符产生的是 int 的 0 或 1。写条件时直接把「真值判断」交给语言即可:
if (count != 0) /* 可以,但啰嗦 */
if (count) /* 惯用:非零即真 */
if (ptr) /* 指针判空:等价于 ptr != NULL */
if (!ok) /* 取反 */
**左值(lvalue)**是「能标识一个存储位置」的表达式,可以出现在赋值号左边;右值只是值。变量是左值,字面量是右值,a + b 是右值。解引用 *p 是左值——这正是指针能改写目标的原因(第 6 章展开)。赋值要求:左边必须是左值,所以 a + b = 3; 编译报错。
5.3 隐式类型转换:整型提升与算术转换
表达式里类型不一致时,编译器按固定规则「升格」后再算,全程不需要你写 cast——但也因此容易不知不觉丢精度:
- 整型提升:char、short、位段在表达式里一律先升成 int(放不下就 unsigned int);
- 算术转换:两个操作数类型不同时,较低-ranked 的转成较高的:long double → double → float → unsigned long → long → unsigned int → int;
- 赋值转换:右边的值转成左边类型再存——收窄可能截断(如 double → int 丢小数,int → char 截字节)。
char c = 200; /* char 通常是有符号:实际存成 -56 */
int a = -1;
unsigned u = 1;
if (a < u) /* 假!a 被转成 unsigned,变成 42 亿 */
printf("less\n"); /* 不会执行——有符号 vs 无符号经典陷阱 */
记住这条:
有符号与无符号比较时,有符号一方被转成无符号。-1 会变成巨大的正数。同时使用两族类型前先统一符号性。
5.4 优先级 ≠ 求值顺序:未定义行为重灾区
优先级决定怎么加括号,但不决定先算谁后算谁。除了 &&、||、?:、逗号四个有序操作符外,C 不规定同表达式中各操作数的求值顺序:
c = c + 1; /* OK */
c = c++ + 1; /* 未定义!同一序列点两次修改 c */
i = i++ + ++i; /* 未定义! */
x = f() + g(); /* f 和 g 谁先跑?未指定——有依赖时拆开写 */
可靠的写法只有一条:一个表达式里别对同一变量又读又写。拆成多条语句,顺序自己说了算。
优先级速记口诀:
单目 > 算术 > 移位 > 关系 >(位与 > 位异或 > 位或)> 逻辑 > 条件 > 赋值 > 逗号。拿不准就加括号——括号不花钱,bug 才花钱。
1. 短路求值带来什么保证?举个惯用法。
&& 和 || 保证先求左边、且必要时才求右边。惯用法:if (p != NULL && p->value > 0)——先判空再解引用;while (i < N && a[i] != target)——先判边界再取下标,顺序反了就越界。
2. 为什么 if (-1 < 1u) 为假?
有符号与无符号比较时,-1 被隐式转换成 unsigned int,变成约 42 亿,大于 1,条件为假。混用两族整数前先统一符号性或显式 cast。
3. i = i++ 这一类的表达式错在哪?
同一序列点内对 i 既读又写且无顺序约束,属于未定义行为——编译器可以做任何事。优先级只决定结合方式,不决定求值顺序;改写成两条语句即可。