int 溢出为什么会变成负数?从里程表讲明白二进制回绕

封面图

写业务代码的时候,很少有人会主动去想 int 的边界。直到某天金币显示成 -2147483648,或者伤害算出个巨大的负数,你才意识到——这个数不知道什么时候转了一圈。

这篇文章不用你背补码公式,我们从"里程表"开始,一步步把这件事说清楚。


一句话本质

int 只有 32 个二进制位,能表示的范围是固定的:

-2147483648  ~  2147483647

一旦算出来的结果超出这个范围,多出来的部分不会报错,而是像里程表一样转一圈从头开始。

最直观的比喻:里程表

老式汽车里程表只有 6 位数字。跑到 999999 再走一公里,它不会显示 1000000,而是翻回 000000

int 一模一样,只不过它最高位被借去当"正负号"了,所以翻回去之后不是变成 0,而是变成最小的负数

int.MaxValue + 1  →  -2147483648
int.MinValue - 1  →   2147483647

用十六进制看更清楚:

0x7FFFFFFF  (+2147483647)   最正
       +1
0x80000000  (-2147483648)   一个进位,符号位被"撞"成 1,整个数变成最负

底层原因是:CPU 做加法时只保留低 32 位,第 33 位的进位直接丢掉。丢掉进位 = 转圈。


为什么翻回去是"最负"而不是 0

这是全文最反直觉的一点,画个图就清楚了。

第一步:int 的 32 个位长什么样

位号:  31  30 29 28 ... 2  1  0
       ↑
     符号位 (0=正, 1=负)    └──── 剩下 31 位放数值 ────┘

关键:第 31 位既是符号,也是一个真正的二进制数位。它不是贴在旁边的一个标记,而是参与数值计算的一位。

第二步:MaxValue 加 1,进位怎么冲上去的

2147483647 的二进制是「符号位 0,后面 31 个全是 1」:

进位:  1111111 11111111 11111111 11111111   ← 每一位都被前一位的进位顶爆
      ┌──────────────────────────────────────┐
       0111 1111 1111 1111 1111 1111 1111 1111   2147483647
     + 0000 0000 0000 0000 0000 0000 0000 0001             1
     ───────────────────────────────────────────
       1000 0000 0000 0000 0000 0000 0000 0000   -2147483648
       ↑
     符号位被最后一道进位顶成了 1

999 + 1 = 1000 是同一个道理:每一位都是 1,加 1 就全部翻成 0 并向前进一位,进位一路传到最左端,把符号位的 0 顶成了 1

第三步(关键):为什么"符号位变 1"就等于 -2147483648

因为同一串二进制,有两种读法。CPU 默认按"有符号"读:

位模式            当作无符号数读     当作有符号 int 读
─────────────────────────────────────────────────────
0000...0000              0                    0
0111...1111     2147483647           2147483647     ← 正数到顶了
1000...0000     2147483648          -2147483648     ← 同一个位模式!
1111...1111     4294967295                   -1

看第三行:位模式完全一样是 0x80000000,但只要符号位是 1,读法就整体平移 -2³²

2147483648  -  4294967296  =  -2147483648
(真实位值)   (2^32)        (有符号解释)

所以不是数字"变小"了,而是读法在符号位翻面的那一刻换了规则,一下子跳到了负数区间的另一头。

第四步:把数轴首尾接起来看

既然加满会转圈,那 int 的数轴其实不是一条线,是一个环

        -2147483648 ───── ... ───── -1 ──┬── 0 ── 1 ───── ... ───── 2147483647
              ↑                                                          │
              │                                                          │
              └──────────────────── +1 ───────────────────────────────────┘
                              加 1 就绕到这里

        而"最大值"和"最小值"在这个环上是紧挨着的邻居

和里程表对比,差在哪

里程表(无符号):  999999  +1  →  000000      → 读作 0        (回到起点)
int(有符号)  : 7FFFFFFF +1  →  80000000    → 读作 -2147483648(跳到最负)
                                 ↑
                          同样的"转圈",但因为最高位被借去当符号,
                          转出来的结果不是 0,而是最负的那个数

一句话总结:位是从 全1 翻成 1后面全0,这一步和里程表一样;区别在于 int 把最左边那一位解释成了符号,所以 1后面31个0 不读作 2147483648(超范围了),而被读成 -2147483648


C# 里的实际表现:静默发生

C# 默认是 unchecked(不检查),所以溢出不抛异常、不报警

int a = int.MaxValue;
a = a + 1;              // 编译通过,运行时 a == -2147483648(悄悄变成负数)

int b = int.MaxValue + 1;   // 编译报错 CS0220:常量溢出

注意这两行的差别非常反直觉:

  • 编译期能算出来的常量溢出 → 直接编译失败
  • 运行时才算出来的溢出 → 完全不管

所以 int.MaxValue + 1 编译不过,但换个变量绕一下就能编过 —— 这也是这类 bug 特别难发现的原因。

想让它报错,得显式写 checked

checked { c = a + 1; }   // 抛 OverflowException

最容易咬人的几个场景

1. 资源/伤害/经验累加爆表变负数

玩家金币 2_000_000_000,打一局又拿了奖励,+= 之后变成负数 —— 界面显示负金币,或者"余额不足"判断直接失效,甚至出现负负得正的刷道具漏洞。这是数值型游戏最经典的事故。

2. 乘法溢出比加法更隐蔽

单次加法很难溢出(要 21 亿),但乘法几十万 × 几万就翻了:

int damage = attack * multiplier;   // 攻击力 50000 × 倍率 50000 = 25 亿 → 溢出变负

表面数值玩家看起来都"正常",一进公式就翻车。

3. Math.Abs 的陷阱

Math.Abs(int.MinValue) == int.MinValue   // 还是负数!

因为 -(-2147483648)+2147483648,超出 int 范围,又绕回去了。取绝对值反而得到负数,后续用它算距离/差值就全乱了。

4. 时间戳(2038 问题)

Unix 时间戳用 int(秒)存的话,2038 年 1 月 19 日这一天会溢出,时钟跳到 1901 年。这类字段在存档、活动配置、离线收益计算里很常见。

5. end - start 当 end 小于 start

发奖励时算"应补发时长",如果一方时间比另一方小,减法得到负数,取绝对值又可能撞上第 3 条的坑。


怎么防

做法 说明
资源/时间戳统一用 long 64 位,够用到人类灭亡。最省事的办法,代价只是每字段多 4 字节
求平均别写 (a + b) / 2 a + (b - a) / 2,两边都很大时避免先溢出
关键计算块加 checked 让它炸出来而不是悄悄错。宁可报错,也别静默产生负数
乘法前先预判 long 做中间结果再截断,如 (int)Math.Min((long)a * b, int.MaxValue),顺手还做了上限钳制

结尾

简单记:int 的溢出不是"报错",是"转圈";转圈之后符号会翻,正数变负数。

所以这类 bug 的症状通常很统一:

  • 值突然变成负的
  • 界面显示一个巨大的奇怪数字
  • 判断逻辑莫名其妙失效

下次看到界面里蹦出一个 -2147483648,你就知道——它只是跑完了一整圈。


如果这篇对你有帮助,欢迎点赞收藏。有任何疑问或者遇到别的溢出场景,评论区聊聊。

posted @ 2026-09-14 12:19  鑫鑫哥Adam  阅读(4)  评论(0)    收藏  举报