这一章在干嘛?

把 C 的操作符一网打尽,再讲清两件容易被忽略的事:表达式求值时的 隐式类型转换 ,以及 优先级 ≠ 求值顺序 ——后者是未定义行为重灾区。

5.1 操作符全景

5.2 布尔值、左值与右值

5.3 隐式类型转换:整型提升与算术转换

5.4 优先级 ≠ 求值顺序:未定义行为重灾区

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 才花钱。