c语言中 i++ 这个东西,几乎是每个学C语言的人最早接触的几个运算符之一。它看上去很简单,就是把变量 i 的值加 1。但事情并不完全是这样,这里面其实藏着一些细节,搞不清楚的话,以后可能会踩到一些莫名其妙的坑。
先说说 i++ 和 ++i 最直接的区别
i++ 和 ++i 都能让 i 的值增加1,这一点没错。 它们的核心区别在于它们作为“表达式”的时候,返回的值不一样。
++i(前自增): 意思是“先增加,再使用”。它会先把i的值加 1,然后整个表达式返回加 1 之后 的新值。i++(后自增): 意思是“先使用,再增加”。它会先把i原来 的值拿出来作为整个表达式的返回值,然后再给i的值加 1。
说起来有点绕,直接上例子看得最清楚。
假设我们有个变量 i,初始值是 1。
c
int i = 1;
int j;
现在我们用 ++i 给 j 赋值:
c
j = ++i;
这里的执行顺序是:
1. ++i 先执行,i 的值从 1 变成 2。
2. 然后,++i 这个表达式返回 i 的新值,也就是 2。
3. 最后,把 2 赋值给 j。
所以,这行代码跑完,i 的值是 2,j 的值也是 2。
接着看 i++:
c
// 为了对比,我们重新把 i 设为 1
i = 1;
j = i++;
这里的执行顺序就不同了:
1. i++ 先返回 i 的原始值,也就是 1。
2. 把这个原始值 1 赋值给 j。
3. 然后,i 的值自己再加 1,从 1 变成 2。
所以,这行代码跑完,i 的值是 2,但是 j 的值是 1。
这就是它们最根本的区别:一个返回新值,一个返回旧值。
在 for 循环里,用哪个有差吗?
在实际写代码的时候,最常见到 i++ 的地方就是 for 循环了。像这样:
c
for (int i = 0; i < 10; i++) {
// ... do something
}
在这种情况下,i++ 和 ++i 其实没什么区别。 因为在 for 循环的第三部分(就是 i++ 那里),我们只关心 i 的值有没有增加,并不关心这个表达式本身返回的是什么值。那部分返回的值被直接丢弃了。
所以,下面这两种写法在功能上是完全一样的:
c
for (int i = 0; i < 10; i++) { / ... / }
for (int i = 0; i < 10; ++i) { / ... / }
虽然功能一样,但是有些老程序员或者一些编程规范会建议使用 ++i。 理由是,从原理上讲,i++ 需要一个临时的存储空间来保存 i 的原始值,以便返回它。 而 ++i 是直接在 i 本身上操作,理论上开销会小一点。不过说实话,对于 C 语言里的 int 这种基本数据类型,现在的编译器都非常聪明,基本上都能把这点差别优化掉,导致最终生成的机器码一模一样。 所以性能上的差异可以忽略不计。不过,养成用 ++i 的习惯也不是坏事,尤其是在后来学习 C++ 的时候,对于复杂的用户自定义类型(比如迭代器),++i 的效率优势会体现出来。
真正需要小心的地方:“副作用”和“序列点”
i++ 这种操作,除了计算出一个值,还会修改变量本身的状态,这种行为在编程里有个专门的词,叫“副作用”(Side Effect)。 当你在一个表达式里多次使用带有副作用的操作时,就可能掉进一个大坑,这个坑叫做“未定义行为”(Undefined Behavior)。
C 语言标准规定了一些“序列点”(Sequence Point),你可以把它理解为程序执行过程中的一些“结算点”。 在一个序列点,之前所有的副作用都必须执行完毕。比如,一个分号 ; 就是一个序列点。
问题出在两个序列点之间。C 语言标准并没有严格规定一个表达式内部各个子表达式的计算顺序。 这就导致了下面这种代码的行为是“未定义的”:
c
int i = 5;
int a = ++i + i++; // 不要这么写!
这行代码的结果是什么?答案是:不知道。
某个编译器可能会先算 ++i,此时 i 变成 6,表达式返回 6。然后再算 i++,此时 i 还是 6,表达式返回 6,然后 i 再变成 7。最后结果是 6 + 6 = 12。
另一个编译器可能先算 i++,此时 i 是 5,表达式返回 5,然后 i 变成 6。再算 ++i,此时 i 变成 7,表达式返回 7。最后结果是 5 + 7 = 12。
还有更复杂的可能,比如两个 i 的值是先读取出来,再统一做自增。
简单来说,当你在两个序列点之间(比如在同一个分号结束的语句里),对同一个变量进行了多次修改,最终结果依赖于这些修改和读取的先后顺序时,程序的行为就是未定义的。 “未定义”意味着任何事情都可能发生:程序可能崩溃,可能算出错误的结果,也可能碰巧得到了你想要的结果。 换个编译器,换个优化级别,结果都可能不一样。
所以,核心原则就是:不要写那种让人一眼看不出执行顺序的、依赖副作用的代码。 宁愿多写几行,把步骤拆分开,代码会清晰很多,也安全很多。
例如,上面那段有问题的代码,应该改成这样:
c
int i = 5;
int temp1 = ++i; // i 变成 6, temp1 是 6
int temp2 = i++; // i 变成 7, temp2 是 6
int a = temp1 + temp2; // a 是 12
这样写,虽然代码长了,但是逻辑清晰,结果唯一,不会有任何歧义。
底层到底发生了什么?
从更底层的汇编指令来看,++i 和 i++ 的区别也很有趣。虽然这取决于具体的CPU架构和编译器,但一个典型的例子可能是这样的:
对于 ++i (前自增),编译器可能会生成类似这样的指令:
1. INC i (直接在内存中给变量 i 加 1)
2. MOV eax, [i] (把 i 的新值加载到寄存器 eax,用于后续计算)
而对于 i++ (后自增),可能就是这样:
1. MOV eax, [i] (先把 i 的旧值加载到寄存器 eax)
2. INC i (然后才在内存中给 i 加 1)
你可以看到,i++ 的确需要先“备份”一下旧值。 这也从底层解释了为什么它需要一个额外的步骤。
总而言之,i++ 看起来简单,但深入了解它的工作方式,特别是它和 ++i 的区别,以及由副作用引发的未定义行为问题,是写出健壮、可预测的 C 语言代码的基础。基本原则就是,如果只是为了让变量加 1,不使用表达式的返回值,那么用哪个都行。如果在一个复杂的表达式里用,一定要搞清楚返回的是新值还是旧值。而且,绝对要避免在同一个语句里对同一个变量做多次自增自减,那是自找麻烦。

七点爱学
评论前必须登录!
立即登录 注册