运算符、表达式、求值与类型转换
本节目标
查询 C23 运算符、表达式分组、算术转换、求值顺序、位运算、sizeof 与 alignof 的核心规则。
本章用于查询表达式的分组、类型与副作用规则。遇到一行难读的代码,依次确认操作数类型、分组方式、转换后的结果类型,以及副作用之间是否有确定顺序。
表达式、操作数与结果类型
查询规则: 先找运算符要求的操作数类别与类型,再应用隐式转换,最后判断表达式的结果类型和值类别;不要只凭某个变量声明猜结果。
| 要检查的层次 | 问题 | 示例结论 |
|---|---|---|
| 表达式类别 | 是算术、比较、赋值还是函数调用? | left + right 是加法表达式 |
| 操作数 | 运算符接收什么类型? | 本例的两个操作数均为算术类型 |
| 转换 | 运算前发生了什么提升或共同类型转换? | int 与 double 相加时,int 转为 double |
| 结果 | 结果是什么类型?是否指代对象? | 3 + 0.5 的结果类型是 double |
int count = 3;
double offset = 0.5;
double total = count + offset; // count 转为 double,结果为 3.5
需要核对基本类型、对象与声明时,回到基本类型、对象、声明与初始化。
优先级、结合性与括号
查询规则: 优先级和结合性决定表达式如何分组,不决定操作数的求值先后。优先级解决不同运算符怎样结合,结合性解决同一优先级怎样结合;括号是表达意图的首选工具。
| 写法 | 按语法分组 | 更清楚的写法 |
|---|---|---|
a + b * c | a + (b * c) | 复杂上下文中显式写括号 |
a = b = 0 | a = (b = 0) | 可拆成两条赋值语句 |
a - b - c | (a - b) - c | 需要强调运算顺序时写出括号 |
int subtotal = 4;
int unit_price = 3;
int quantity = 2;
int total = subtotal + (unit_price * quantity);
括号只能改变分组,不能把本来未指定的操作数求值顺序变成从左到右。
算术运算与常用类型转换
查询规则: 小整数类型先做整数提升;不同算术类型再做常用算术转换,得到共同类型后执行运算。
| 规则 | 结果或边界 |
|---|---|
| 整数提升 | bool、char、short 等通常先提升为 int,不能表示其全部值时提升为 unsigned int |
| 常用算术转换 | 混合整数或浮点运算先转换到规则确定的共同类型 |
| 整数除法 | 商向零截断;7 / 2 为 3,-7 / 2 为 -3 |
| 余数 | 可表示时满足 (a / b) * b + a % b == a,余数与被除数同号或为零 |
| 有符号溢出 | 有符号整数溢出是未定义行为 |
| 无符号运算 | 无符号整数运算按该类型最大值加一取模;回绕有定义,但仍可能是业务错误 |
int numerator = 7;
int denominator = 2;
int quotient = numerator / denominator; // 3
double ratio = (double)numerator / denominator; // 3.5
int remainder = numerator % denominator; // 1
整数 / 或 % 的除数为 0 时是未定义行为;执行前先验证除数不为零。
比较、逻辑与条件运算
查询规则: 比较结果是 int 类型的 0 或 1;&& 与 || 从左到右求值并短路;条件运算符 condition ? when_true : when_false 只求值被选中的后一个或第三个操作数。
| 运算 | 确定顺序 | 安全用途 |
|---|---|---|
left && right | 先求值 left;为零时不求值 right | 先检查指针,再访问所指对象 |
| `left | right` | |
condition ? a : b | 先求值条件,只选择 a 或 b | 在两个值之间选择 |
const int *value = 0;
int positive = value != 0 && *value > 0; // 短路避免解引用空指针
const char *label = positive ? "positive" : "not-positive";
逻辑运算数只按零或非零判断真假;结果不是保留原操作数的值。
赋值、自增自减与复合赋值
查询规则: 赋值先把右侧结果转换为左侧对象的类型;复合赋值只求值左操作数一次;前缀形式产生更新后的值,后缀形式产生更新前的值。把一次状态变化留在一条独立语句中最易审查。
| 写法 | 对象变化 | 表达式产生的值 |
|---|---|---|
count = 3 | count 变为 3 | 赋值后的值 |
count += 2 | 相当于按规则计算再转换回 count 的类型,左侧只求值一次 | 更新后的值 |
++count | 先增加 | 增加后的值 |
count++ | 后增加 | 增加前的值 |
int counter = 1;
int previous = counter;
counter += 2; // 副作用单独完成
printf("%d->%d\n", previous, counter); // 输出:1->3
不要为了缩短代码,把同一对象的多次更新和其他读取塞进一个表达式。
位运算与移位
查询规则: 位标志优先使用无符号整数;& 查询位、| 设置位、^ 翻转位、~ 逐位取反。移位计数必须非负且小于提升后左操作数的位宽。
| 操作 | 位掩码含义 | 边界 |
|---|---|---|
flags & mask | 查询掩码中的位 | 与 0u 比较得到清晰条件 |
| `flags | mask` | 设置掩码中的位 |
flags & ~mask | 清除掩码中的位 | mask 与 flags 选相容的无符号类型 |
value << count、value >> count | 左移或右移 | 计数越界是未定义行为;有符号移位还有额外风险 |
const unsigned read_mask = 1u << 0;
const unsigned execute_mask = 1u << 1;
unsigned permissions = read_mask;
int can_read = (permissions & read_mask) != 0u;
int can_execute = (permissions & execute_mask) != 0u;
对有符号负数右移的结果由实现定义,有符号左移还可能触发未定义行为;位级协议和标志应采用宽度明确或经确认足够宽的无符号类型。
求值顺序与副作用
查询规则: 先找标准明确的“先序于”关系;没有该关系时,再判断是允许多种次序,还是同一对象的冲突副作用让行为失去定义。未指定表示标准允许多种顺序,未定义表示标准不再约束程序行为。
| 情况 | 分类 | 做法 |
|---|---|---|
&&、` | ` 左侧与右侧 | |
| 函数实参之间、普通二元运算的操作数之间 | 求值先后通常未指定 | 结果不得依赖某一先后顺序 |
| 未排序地修改同一对象两次,或修改并读取该对象来计算另一结果 | 未定义行为 | 拆成多条有完整顺序的语句 |
int counter = 1;
int before = counter;
counter = counter + 2;
printf("%d->%d\n", before, counter); // 输出:1->3
例如函数 combine(next_left(), next_right()) 的两个实参谁先调用可能未指定;只要两个调用彼此独立,程序仍然有效。不要把“编译器常这样做”当成语言保证。
显式转换、大小与对齐
查询规则: 显式转换只在确实要改变解释或运算类型时使用;sizeof 查询对象表示所占字节数,C23 的 alignof(type-name) 查询类型的对齐要求。
| 工具 | 结果类型 | 求值规则或限制 |
|---|---|---|
(target_type) expression | 目标类型 | 可能丢失范围或精度,不能修复本身无效的运算 |
sizeof(type-name) / sizeof expression | size_t | 普通表达式操作数不求值 |
sizeof 与 VLA | size_t | 操作数类型是可变长度数组时,为确定大小可能发生求值 |
alignof(type-name) | size_t | 查询类型的有效对齐要求,不接收普通表达式 |
sizeof 的结果类型是 size_t,应用 %zu 输出。普通 sizeof 操作数不求值;操作数类型是可变长度数组时是例外,运行期可能需要计算其大小。
int numerator = 7;
int denominator = 2;
double ratio = (double)numerator / denominator;
size_t integer_bytes = sizeof(int);
size_t integer_alignment = alignof(int);
显式转换应放在运算发生前:(double)(numerator / denominator) 只是把已经截断的整数结果转换为浮点数。
下面的代表性程序把整数与浮点除法、无符号权限掩码和分步更新放在同一份可编译示例中;每个输出都不依赖未指定的操作数求值顺序。
#include <stdio.h>
int main(void) {
int numerator = 7;
int denominator = 2;
const unsigned read_mask = 1u << 0;
const unsigned execute_mask = 1u << 1;
const unsigned permissions = read_mask;
int counter = 1;
const int previous_counter = counter;
printf("integer-division=%d\n", numerator / denominator);
printf("floating-division=%.1f\n", (double)numerator / denominator);
puts((permissions & read_mask) != 0u ? "can-read=yes" : "can-read=no");
puts((permissions & execute_mask) != 0u ? "can-execute=yes"
: "can-execute=no");
counter += 2;
printf("ordered-update=%d->%d\n", previous_counter, counter);
return 0;
}
输出:
integer-division=3
floating-division=3.5
can-read=yes
can-execute=no
ordered-update=1->3
常见陷阱
| 陷阱 | 风险分类 | 修复 |
|---|---|---|
| 把优先级当成求值顺序 | 逻辑误判 | 用括号表达分组,再单独检查先序关系 |
整数相除后才转换为 double | 精度已丢失 | 在除法前转换一个操作数 |
| 有符号运算越过类型范围 | 未定义行为 | 运算前做范围检查或重新选择合适类型 |
| 依赖无符号回绕处理业务边界 | 结果有定义但语义可能错误 | 显式检查上限和下限 |
| 移位计数为负或不小于类型位宽 | 未定义行为 | 移位前验证计数范围 |
| 在一个表达式中重复更新同一对象 | 可能产生未定义行为 | 将每次更新拆成独立语句 |
期待 sizeof(i++) 增加普通整数 i | 操作数通常不求值 | 把需要发生的副作用写成独立语句 |
安全改写时,可以沿用统一顺序:先验证除数、范围和移位计数,再执行一次运算,最后在独立语句中更新状态。这样不引入新机制,也更容易把每一步对应到上表中的规则。
下一章继续查询这些表达式怎样进入 if、循环和跳转语句:条件、循环与跳转。