跳到主要内容

运算符、表达式、求值与类型转换

本节目标

查询 C23 运算符、表达式分组、算术转换、求值顺序、位运算、sizeof 与 alignof 的核心规则。

本章用于查询表达式的分组、类型与副作用规则。遇到一行难读的代码,依次确认操作数类型、分组方式、转换后的结果类型,以及副作用之间是否有确定顺序。

表达式、操作数与结果类型

查询规则: 先找运算符要求的操作数类别与类型,再应用隐式转换,最后判断表达式的结果类型和值类别;不要只凭某个变量声明猜结果。

要检查的层次问题示例结论
表达式类别是算术、比较、赋值还是函数调用?left + right 是加法表达式
操作数运算符接收什么类型?本例的两个操作数均为算术类型
转换运算前发生了什么提升或共同类型转换?intdouble 相加时,int 转为 double
结果结果是什么类型?是否指代对象?3 + 0.5 的结果类型是 double
int count = 3;
double offset = 0.5;
double total = count + offset; // count 转为 double,结果为 3.5

需要核对基本类型、对象与声明时,回到基本类型、对象、声明与初始化

优先级、结合性与括号

查询规则: 优先级和结合性决定表达式如何分组,不决定操作数的求值先后。优先级解决不同运算符怎样结合,结合性解决同一优先级怎样结合;括号是表达意图的首选工具。

写法按语法分组更清楚的写法
a + b * ca + (b * c)复杂上下文中显式写括号
a = b = 0a = (b = 0)可拆成两条赋值语句
a - b - c(a - b) - c需要强调运算顺序时写出括号
int subtotal = 4;
int unit_price = 3;
int quantity = 2;
int total = subtotal + (unit_price * quantity);

括号只能改变分组,不能把本来未指定的操作数求值顺序变成从左到右。

算术运算与常用类型转换

查询规则: 小整数类型先做整数提升;不同算术类型再做常用算术转换,得到共同类型后执行运算。

规则结果或边界
整数提升boolcharshort 等通常先提升为 int,不能表示其全部值时提升为 unsigned int
常用算术转换混合整数或浮点运算先转换到规则确定的共同类型
整数除法商向零截断;7 / 23-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 类型的 01&&|| 从左到右求值并短路;条件运算符 condition ? when_true : when_false 只求值被选中的后一个或第三个操作数。

运算确定顺序安全用途
left && right先求值 left;为零时不求值 right先检查指针,再访问所指对象
`leftright`
condition ? a : b先求值条件,只选择 ab在两个值之间选择
const int *value = 0;
int positive = value != 0 && *value > 0; // 短路避免解引用空指针
const char *label = positive ? "positive" : "not-positive";

逻辑运算数只按零或非零判断真假;结果不是保留原操作数的值。

赋值、自增自减与复合赋值

查询规则: 赋值先把右侧结果转换为左侧对象的类型;复合赋值只求值左操作数一次;前缀形式产生更新后的值,后缀形式产生更新前的值。把一次状态变化留在一条独立语句中最易审查。

写法对象变化表达式产生的值
count = 3count 变为 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 比较得到清晰条件
`flagsmask`设置掩码中的位
flags & ~mask清除掩码中的位maskflags 选相容的无符号类型
value << countvalue >> 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 expressionsize_t普通表达式操作数不求值
sizeof 与 VLAsize_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) 只是把已经截断的整数结果转换为浮点数。

下面的代表性程序把整数与浮点除法、无符号权限掩码和分步更新放在同一份可编译示例中;每个输出都不依赖未指定的操作数求值顺序。

expression_report.c
#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、循环和跳转语句:条件、循环与跳转