第93章 序列点:什么是序列点?为什么重要?
说实话,序列点这个概念,很多C语言开发者学了几年都没真正搞明白。我记得有一次在团队code review时,看到一段代码:
int i = 0;
int arr[3] = {0};
arr[i] = i++;
当时有个同事说:"这代码没问题啊,不就是把0赋给arr[0]嘛。"
嗯,问题就出在这里。这段代码的行为,其实是未定义的。
为什么会这样?因为这里涉及到了序列点的问题。
什么是序列点?
序列点,说白了就是C语言执行过程中的一些"检查点"。在这些点上,所有之前的副作用(比如变量的修改、内存的写入)都必须完成,之后的副作用还没开始。
你可以把它想象成一条流水线上的质检工位。每个零件经过这个工位时,前面的加工必须全部完成,后面的加工还没开始。
C语言标准中定义的序列点包括:
- 每个完整表达式结束时(分号处)
- 逻辑与 && 和逻辑或 || 的左操作数求值后
- 条件运算符 ?: 的第一个操作数求值后
- 逗号运算符 , 的左操作数求值后
- 函数调用时,所有实参求值完成后、进入函数体之前
核心要点:在两个序列点之间,一个变量最多只能被修改一次。如果被修改了,那它的值也只能用于计算新值,不能做其他读取。
为什么序列点如此重要?
我刚开始做嵌入式开发时,就踩过这个坑。当时写了一段控制电机转速的代码:
int speed = 0;
speed = speed++ + 1; // 未定义行为!
printf("%d", speed);
在Keil编译器上跑得好好的,换到IAR就完全不对了。查了两天才发现是序列点的问题。
为什么不同编译器结果不一样?因为C标准说这种代码是"未定义行为",编译器可以自由发挥。有的编译器先取speed的值再自增,有的先自增再取旧值,结果自然不同。
你想想看,如果你的代码要跨平台、跨编译器,这种写法就是给自己埋雷。
常见的序列点陷阱
我总结了几个最容易踩坑的场景:
| 陷阱代码 | 问题说明 | 正确写法 |
|---|---|---|
i = i++; |
同一变量在两个序列点间被多次修改 | i++; 或 i = i + 1; |
arr[i] = i++; |
下标和赋值操作使用了同一变量的不同状态 | arr[i] = val; i++; |
func(i++, i++) |
函数实参的求值顺序未定义 | func(a, b); a++; b++; |
if (a++ && a--) |
逻辑与的短路特性导致问题 | 分开写,先判断再自增自减 |
警告:千万不要在同一个表达式中多次修改同一个变量,除非你能明确知道序列点的位置。我曾经见过有人写出 i = ++i + i++ 这种代码,结果在不同编译器上得到了三个不同的值。
序列点的实际应用
理解了序列点,你就能看懂一些看似"奇怪"的代码了。比如:
int a = 1, b = 2, c;
c = (a++, b++, a + b); // 逗号表达式,序列点在每个逗号处
这段代码是安全的。因为逗号运算符引入了序列点,a++和b++的副作用在计算a+b之前已经完成了。c的值是4。
再比如:
while (*dest++ = *src++) ; // 字符串拷贝的经典写法
这个也是安全的。因为赋值运算符的右操作数求值完成后,有一个序列点。dest和src的修改不会互相干扰。
如何避免序列点问题?
我个人习惯遵循几条简单规则:
- 一行只做一件事:不要在一行里既修改变量又读取变量
- 自增自减单独成行:i++ 就单独写一行,别跟其他操作混在一起
- 函数参数保持简单:别在参数里写自增自减
- 打开编译器警告:GCC加 -Wall -Wsequence-point,很多问题编译时就能发现
小技巧:如果你不确定某段代码是否有序列点问题,最简单的办法就是拆开写。多写两行代码不会影响性能,编译器优化后生成的机器码是一样的。
序列点知识体系
下面这张图帮你理清序列点的核心逻辑:
这张图把序列点的定义、出现位置和核心规则串在了一起。你只要记住最下面那条规则,就能避开90%的坑。
避坑指南
我曾经在调试一个电机控制程序时,发现电机偶尔会抖动一下。查了半天,最后定位到是这段代码:
// 问题代码
int pos = get_encoder_position();
set_motor_speed(pos++, pos < 100 ? 100 : 50);
这里pos++和pos的读取顺序不确定,导致电机速度时快时慢。改成下面这样就好了:
// 正确写法
int pos = get_encoder_position();
int speed = pos < 100 ? 100 : 50;
set_motor_speed(pos, speed);
pos++;
嗯,多写两行代码,换来的是确定的行为和好心情。
序列点这个知识点,说白了就是告诉你:别在C语言里耍小聪明。老老实实一行做一件事,编译器不会辜负你。
公众号:蓝海资料掘金营,微信deep3321