1. 从一句代码说起:你真的懂算术运算符吗
我之前在带新人做代码评审时,经常看到这样的场景:一个刚工作没多久的同事写了一段循环,里面用到了 i=i++,跑出来的结果和他预期完全不一样,debug 大半天找不到原因。旁边另一个同事扫了一眼说,你把 i++ 和 ++i 搞混了吧。他一脸懵:这俩不都是加1吗?有什么区别?
这也是我写这篇博文的初衷。算术运算符是编程语言里最基础的知识点,+、-、*、/、% 大家天天用,但到了 i++ 与 ++i 这里,很多人就含糊了。说句不好听的,很多工作了两三年的开发者,遇到 i = i++ 这种写法,照样会算错。
这篇内容适合正在学编程的初学者、准备面试的求职者,以及写代码多年但想夯实基础的开发者。我会从内存执行的角度,把 i++ 和 ++i 的底层逻辑讲透,再结合实际开发中的场景和面试题,帮你彻底搞定这个知识点。尤其是最后那几道题,我建议你亲手在环境里跑一遍,比看十篇文章都管用。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 算术运算符全家桶先过一遍
2.1 五种常见算术运算符
先复习一下最基础的内容。Java、C、C++ 等语言里,常见的算术运算符就五种:加法 +、减法 -、乘法 *、除法 /、取余 %。
这里最容易出问题的是除法和取余。很多新手在整数除法上栽过跟头:7 / 2 在整数类型下结果不是 3.5,而是 3。因为两个整数相除,结果还是整数,小数部分直接丢弃,不是四舍五入。而 7 % 2 得到的是余数 1,% 只看余数不看商。
另外,取余运算在负数场景下也有讲究。比如 -7 % 2,在 Java 里结果是 -1,因为 Java 取余结果的符号和被除数保持一致。这一点在 C 语言里行为可能不同,C 标准只规定了 (a / b) * b + a % b 等于 a,但具体符号在不同编译器版本下可能不一样,写跨平台代码时要格外小心。
2.2 自增自减运算符的特殊地位
除了这五种,还有一种特殊的算术运算:自增 ++ 和自减 --。它俩是单目运算符,只操作一个操作数,作用就是给变量加1或减1。
++ 放在变量的左边叫前自增,放在变量右边叫后自增。前自增 ++i 是先让变量加1,再把加完后的值拿去用;后自增 i++ 是先把当前值拿去用,然后再让变量加1。
听上去很简单对不对?但很多人到了复杂表达式里就乱了。别急,我在后面会用内存执行的角度重新讲一遍,保证你听完不再犯迷糊。
3. i++ 与 ++i 的核心差异:从内存执行角度讲透
3.1 一条反编译命令看清本质
先给你一个最直观的方法:把代码编译成字节码,看看底层到底做了什么。
我用 Java 写了两个方法,然后通过 javap -c 命令反编译。这里不贴完整字节码,只讲关键结论:
java复制public static void testPost() {
int i = 0;
int a = i++;
}
public static void testPre() {
int i = 0;
int a = ++i;
}
在字节码层面,i++ 会先把变量当前值压入操作数栈,然后再对局部变量做加1。而 ++i 会先对局部变量加1,再把加完后的值压入栈。
这就解释了那个经典现象:int a = i++ 这条语句里,a 拿到的是 i 加1之前的值;int a = ++i 里,a 拿到的是加1之后的值。
3.2 用一个生活类比理解前后缀
我经常拿“发红包”给新人打比方。
i++ 像是先把你钱包里的钱数拍照留存,然后再往钱包里塞100块。你拍照时留下的金额是老金额,但钱包实际已经变多了。
++i 像是先往钱包里塞100块,再拍照。照片上的金额是新的,钱包也确实是新的。
赋值操作 int a = i++ 就好比把“照片”交给 a。你照片上是旧金额,所以 a 是旧值,但 i 本身已经加1了。++i 则是把新金额的照片交给 a,两者拿到的东西不一样。
类比归类比,真正写代码时,我还是建议你对每一行涉及自增自减的代码,在脑海里模拟一遍“先取值 / 先计算”的顺序。一开始慢一点没事,练多了自然就快了。
4. 实操演示:最经典的 i++ 问题与三张执行表
4.1 案例一:i = i++ 到底等于几
这是面试出现频率极高的一道题:
java复制int i = 0;
i = i++;
System.out.println(i); // 猜猜输出几
这道题,我统计过带过的学员里,第一次能答对的人不超过三成。大部分人说结果是1,理由很简单:i++ 不就是让 i 加1嘛,赋值给自己,当然变1了。
但正确答案是 0。
为什么?因为 i++ 在表达式中的返回值是自增前的旧值。执行过程拆开看是这样的:
text复制第一步:把 i 的当前值(0)放到临时位置保存
第二步:i 自己加1,变成 1
第三步:把第一步保存的临时值(0)赋给 i
所以最终 i 还是 0。i++ 作为表达式,它的值是 0,不是 1。赋值操作发生在自增完成后,但赋的是旧值。
这个题我见过有人在大厂面试时也栽了,所以建议新手一定亲手跑一遍,不要光靠看。
4.2 案例二:for 循环里的 i++ 与 ++i 有区别吗
再看一个日常开发高频场景。很多人写 for 循环都是 i++,偶尔看到有人写 ++i,心里会嘀咕:这两个在这儿到底有没有区别?
java复制for (int i = 0; i < 5; i++) {
System.out.println(i);
}
for (int i = 0; i < 5; ++i) {
System.out.println(i);
}
结论是:在这个场景下,两者的输出完全一样,都是 0 1 2 3 4。因为 for 循环的第三个表达式是独立执行的,它的返回值被丢弃了,没有赋值给其他变量。唯一被执行的操作就是“变量加1”。
但如果你在循环体内部依赖自增表达式的返回值,那就另当别论了。
这里我给出一个对比表,方便你快速查阅:
| 场景 | i++ | ++i | 结果 |
|---|---|---|---|
| 单独执行 | i 加1 | i 加1 | 没有区别 |
| 赋值给变量 | 先返回旧值,再自增 | 先自增,再返回新值 | 赋的值不同 |
| 循环体内使用返回值 | 用的是加1前的值 | 用的是加1后的值 | 可能影响循环逻辑 |
| 传参给方法 | 传入旧值,实参加1 | 传入新值,实参已加1 | 方法内看到的值不同 |
4.3 案例三:复杂表达式一步一步算
来看一个稍微复杂一点的:
java复制int i = 3;
int j = i++ + ++i;
System.out.println("i=" + i + ", j=" + j);
这个题我让不少工作一两年的朋友做过,答错的还不少。核心考点是:i++ 在表达式中用旧值,++i 在表达式中用新值,而且 i 是同一个变量,会被前面的操作影响。
一步一步拆:
text复制int i = 3;
i++ 表达式返回 3,i 变为 4
++i 因为 i 已经是 4,先自增为 5,表达式返回 5
j = 3 + 5 = 8
i 最终是 5
所以输出是 i=5, j=8。
这类题目做错的根本原因,是没有建立“在同一个表达式里,i 的值是动态变化的”这个意识。你要先从左到右,逐步更新 i 的最新值,再计算每一步表达式返回的值。
为了让这个逻辑更清楚,我通常建议直接拆成多条语句改写:
java复制int i = 3;
int temp1 = i++; // temp1 = 3, i = 4
int temp2 = ++i; // i = 5, temp2 = 5
int j = temp1 + temp2; // 8
改写成这样后,几乎不会出错,也方便别人读代码。
5. 很多人会忽略的细节:自增自减与类型、方法参数
5.1 自增运算的复合赋值本质
i++ 其实等价于 i = i + 1?严格说,不完全等价。区别在于,i++ 在底层做的是“读-改-写”三步操作,而 i = i + 1 也是三步。但在 Java 的字节码层面,i++ 被专门优化成了 iinc 指令,这个指令直接对局部变量索引指向的槽位做加1操作,不再额外读写操作数栈。
这里有一个容易被忽略的点:i++ 是给变量加1,不是给表达式加1。所以它不能作用于常量或字面量,比如 5++ 是非法的。原因很简单,常量的值不可变,自增需要修改变量存储位置的内容。
另外,对于浮点类型,float f = 1.5f; f++; 是合法但几乎没用的操作。它会把 1.5 变成 2.5。所以自增自减并不只适用于整数,只是整数场景用得最多。
5.2 自增自减作为参数传给方法时的坑
再看一个实战中容易踩坑的场景:
java复制public static void main(String[] args) {
int i = 0;
print(i++); // 传入的是 0,i 变为 1
print(++i); // i 先变为 2,传入的是 2
}
public static void print(int value) {
System.out.println(value);
}
第一个 print 输出 0,第二个 print 输出 2。很多人会误以为第一个输出 1,第二个输出 2,但实际上第一行传入参数时自增还没真正完成。
这种写法在实际开发中非常不推荐,因为会让读代码的人需要停下来想很久。我见过有同事在日志打印里写 log.info("count=" + count++),结果日志输出的总是旧值,排查问题时被误导了很久。日志该用 count 就老老实实用 count。
5.3 自增自减与左值右值的关系
在 C/C++ 里,i++ 和 ++i 还有一个区别:++i 是左值,可以被取地址或赋值;i++ 是右值,不能取地址。不过在 Java 里,这两个表达式都不是左值,不能写 ++i = 5 或者 i++ = 5,编译器直接报错。
这一点如果是从 C++ 转 Java 的朋友需要特别注意,两门语言的语义确实有差异,不要想当然地套用。
6. 我踩过的最深的坑:循环中的内存一致性问题
6.1 多线程环境里的 i++ 并非原子操作
i++ 看起来只是一行代码,但它在底层是“读-改-写”三步。这意味着在多线程环境下,多个线程同时执行 i++,结果可能小于你期望的总次数。
我举个真实案例。以前写一个订单号生成器,一开始图省事,直接用多线程共享的静态变量做自增:
java复制public static int orderNo = 0;
public static int nextOrderNo() {
return orderNo++;
}
单测单线程跑没问题,一上多线程压测,orderNo 经常重复或跳号。原因就是两个线程同时读到旧值,各自加1再写回,导致丢更新。
这种场景应该用 AtomicInteger 的 incrementAndGet() 方法,或者加同步锁。这是 i++ 相关的另一个层面的坑,我觉得很有必要在这里提一句,因为太多人栽在这上面。
6.2 Java 7 之前还有一个循环陷阱
在 Java 7 之前的某些版本,for (int i = 0; i < list.size(); i++) 这种写法,每次循环都需要调用 list.size(),如果你的 size() 方法有额外计算,性能会有损耗。虽然后续版本 JIT 编译器做了优化,但我们在写代码时,还是应该把不变的计算提到循环外:
java复制int size = list.size();
for (int i = 0; i < size; i++) {
// ...
}
这个和 i++ 本身没直接关系,但既然说到 for 循环和自增,顺便分享出来,也算是个实用经验。很多人用 IDEA 跑代码分析时,也会收到类似的提示。
6.3 忘了重赋值导致的死循环
还有一种常见错误,是在 while 循环里忘记让控制变量自增:
java复制int i = 0;
while (i < 10) {
System.out.println(i);
// 忘记写 i++,这就成了死循环
}
这种代码在开发中特别容易发生,尤其是从 for 循环改成 while 循环时,容易把 i++ 漏掉。排查的时候看逻辑没问题,但程序就是不退出,最后才发现是少了那一行。我的习惯是,写 while 循环时先在顶部写好循环变量的更新语句,再填循环体内容,能有效减少这类低级错误。
7. 常见面试题与自测清单
7.1 五道高频面试题
不管你是准备校招还是社招,i++ 和 ++i 相关的题几乎都会碰到。我整理了几道高频题,你可以先自己做一遍,再对照下文看答案。
java复制// 题目 1
int a = 5;
int b = a++ + a++;
// 题目 2
int i = 2;
int j = i++ + i++ + i++;
// 题目 3
int k = 1;
k = k++ + ++k;
// 题目 4
int m = 10;
int n = --m + m--;
// 题目 5
for (int i = 0; i < 3; ++i) {
System.out.print(i);
}
它们的答案分别是:a=7, b=11;i=5, j=9;k=4;m=8, n=18;012。
这里我把每道题的拆解步骤放在下表里,帮助你对照检查:
| 题目 | 逐步计算 | 最终结果 |
|---|---|---|
| 题1 | b 先取 a=5,a 变 6;再取 a=6,a 变 7;b=5+6=11 | a=7, b=11 |
| 题2 | 三个 i++ 依次取 2、3、4,i 最终为 5;j=2+3+4=9 | i=5, j=9 |
| 题3 | k++ 返回 1,k 变 2;++k 让 k 变 3,返回 3;k=1+3=4 | k=4 |
| 题4 | --m 让 m=9 返回 9;m-- 返回 9,m 变 8;n=9+9=18 | m=8, n=18 |
| 题5 | ++i 在 for 第三个表达式返回值被丢弃,不影响循环次数 | 打印 012 |
这类面试题本质上考察的是一件事:你有没有理清“表达式返回值”和“变量当前值”这两条时间线。一旦脑子里能分清楚这两者,什么题都不怕。
7.2 避坑速查:三个开发原则
经过这么多年写代码、带新人,我总结了三条关于自增自减的使用原则,分享给你:
- 不要在同一个表达式中对同一个变量使用多次自增自减。代码是给人读的,这种写法严重降低可读性,还容易算错。
- 不要把自增自减和赋值混写在一条语句里,除非你非常清楚自己在做什么。
i = i++这类代码不应该出现在正式项目中。 - 如果确实需要用到表达式返回值,优先拆成多步,用临时变量保存中间结果,后续维护成本会低很多。
我见过有团队明确规定禁止在代码里写 i++ 参与表达式的操作,只能单独成行。这个规定虽然有点苛刻,但从代码可维护性角度讲,不无道理。
7.3 实战建议:如何彻底掌握这个知识点
光看不练假把式。我的建议是,打开 IDE,把本文中所有代码案例敲一遍,先猜答案再运行,猜错的一定要停下来想明白为什么。特别是 i=i++ 和那几个复合表达式题,亲手跑一遍之后,你对自增自减的理解会立刻上一个大台阶。
然后你可以自己改造题目,比如把 int i = 3; int j = i++ + ++i; 改成 int i = 3; int j = ++i + i++;,再看看结果变没变、为什么变。这种主动式的试验,比刷十遍文档都管用。
另外,我建议在做题时养成一个习惯:在纸上写出每一步的“临时变量值”和“变量当前值”,就像我在上面演示的那样。这个方法对于理解复杂表达式特别有效,等你熟练了,脑子里的执行路径自然就摸清了。
8. 延伸思考:为什么不同语言对 i++ 的处理不一样
8.1 Java、C# 与 C++ 的细微差别
很多人学完 Java 的 i++ 后,以为所有语言都一样,其实不然。在 C# 里,i = i++ 的结果也是 0,这点和 Java 一致。但在一些更底层的场景,比如 C++ 中重载运算符时,前缀和后缀版本的效率是有区别的。
对于内置类型来说,++i 和 i++ 的机器指令几乎没差别,现代编译器都能优化。但对于自定义类型的迭代器,比如 C++ 的 std::vector::iterator,it++ 需要构造一个临时对象保存旧值,再执行自增,最后返回那个临时对象;而 ++it 直接自增然后返回自身引用,不需要额外构造临时对象。所以在循环里写 C++ 迭代器时,很多人建议用 ++it 而不是 it++,原因就在这里。
Java 里没有运算符重载,所以不存在这个问题。但在 IDEA 代码分析插件里,社区规范通常建议在 for 循环中用 i++,因为 Java 的字节码层面两者没有性能差异,而 i++ 对更多程序员来说更顺眼。
8.2 Python 中没有 i++ 这件事
Python 里没有自增运算符,写 i++ 会直接报语法错误。如果你想给变量加1,得写 i += 1。这和 Python 的设计哲学有关:Python 中整数是不可变类型,每次重新赋值都会产生一个新的整数对象,不存在原地修改内存单元的操作。无独有偶,Rust 也没有 i++,只支持 i += 1。
所以,如果你在多种语言之间切换开发,千万要记得各种语言在自增自减上的差异。我从 Java 切 Python 的时候,就经常手滑打 i++,被语法错误提醒了无数次才改过来。
8.3 这个知识点背后更通用的思维
说回算术运算符本身,i++ 与 ++i 看似是一个小知识点,但它背后反映的是一个更通用的思维:你对“一条语句在计算机里到底做了什么”有没有清晰的认知。
很多人写代码只记结论,不追本质。比如知道 i=i++ 结果是 0,但说不清为什么。一旦题目稍微变形,比如 i=i++++ 或者把 i 换成 map.get(i),就彻底懵了。真正靠谱的方式,是回到计算机执行指令的最小粒度去理解每一步。有了这个底层的视角,任何语法层面的变化都难不倒你。
我在实际带人的过程中,发现能把自增自减讲清楚的人,往往对内存、栈、指令都有比较好的感知。反过来,只会背答案的人,遇到报错或者诡异 bug 时,排查效率通常很低。这个观察不一定绝对,但我觉得很能说明问题。
9. 最后聊两句我的实操体会
回头看看这些年踩过的坑,i++ 和 ++i 大概是所有算术运算符知识点里,让人“看起来会了,其实没会”最典型的一个。
我个人现在的习惯是:单独成行自增自减,绝不参与表达式运算。不是因为我不会算,而是因为代码要写给下一个维护者看,写得太花哨,对别人是一种折磨。好不好维护,比秀不秀操作重要得多。
如果你现在正准备面试,把上面那五道题算清楚就够了,不必再去造更复杂的组合。面试官大概率不会为了难为人而故意搞个地狱难度的表达式,他更想看到的是你能否用清晰的逻辑拆解问题。你能一步步讲出执行过程,比直接报出正确答案更能体现水平。
还有一个小建议,看这类知识点时,不要只搜“i++ 和 ++i 的区别”,多搜“自增自减原理”“i=i++ 为什么等于0”这类带结果的词,更容易搜到别人踩坑后的真实复盘。你看到的问题越真实,学到的东西越扎实。
