你写过无数个int a = -5;,但你停下来想过没有——那个-5,在内存里到底长什么样?
内存里只有 0 和 1,没有"负号"这个东西。那"负"这件事,是怎么被塞进 32 个比特里的?
直觉会告诉你:这还不简单,最高位放个符号位,0 是正、1 是负,剩下 31 位存绝对值。这就是原码。听起来天衣无缝,可你真拿它做一次加法,就露馅了。
原码:看着顺眼,一算就崩
用 4 位(方便演示)算一下1 + (-1):
+1的原码是0001-1的原码是1001(最高位符号位是 1)
直接相加:0001 + 1001 = 1010,最高位是 1,表示负数,值是-2。
1 + (-1) = -2?错得离谱。
更尴尬的是,原码里还有两个"零":0000是+0,1000是-0。CPU 每次判断"结果是不是零",都得特判两次。加法器看到这玩意儿,只想掀桌。
反码:离对近了一步,但还差一点
有人想:负数的表示,把符号位之外的所有位取反行不行?这就是反码。
-1的反码 =1001取反(符号位不动)→1110
再算1 + (-1):0001 + 1110 = 1111。1111是-0(反码的负零)。结果是零了,方向对了,但那个恼人的-0还在。
补码:多走一步,减法当场失业
在反码基础上再加 1,就是补码:
-1的补码 =1110 + 0001 = 1111
注意,4 位里-1的补码是1111。再算1 + (-1):0001 + 1111 = 10000,4 位存不下最高位的进位,直接丢掉,剩下0000。等于 0。
成了。而且这次,-0也消失了——补码里0000是唯一的零。
为什么"取反再加一"这么神?钟表早就告诉过你
这背后的原理,其实是一个你从小就会的概念:模。
想象一块 12 点的钟。现在是 12 点,你想"倒拨 5 小时"到 7 点;也可以"正拨 7 小时",同样到 7 点。因为钟的模是 12,-5和+7在模 12 下是同一个东西。
计算机里的 n 位二进制,模是2^n。所谓-x的补码,本质就是2^n - x,也就是"把负数抬到模的另一个等价值上"。
那"取反再加一"是从哪冒出来的?拆一下就清楚了:
2^n - x = (2^n - 1 - x) + 12^n - 1是一串全 1(比如 4 位就是1111)。全 1 减去 x,等于把 x 的每一位取反。所以"取反"对应(2^n - 1 - x),再"加 1"补上那个+1。就这么来的,没有魔法。
补码顺手办了另一件大事:减法消失了
CPU 里其实没有减法电路。那7 - 3是怎么算的?
把-3转成补码,然后7 + (-3 的补码)直接走加法器:
0111 (7) + 1101 (-3 的补码) = 10100 → 丢掉进位 → 0100 (4)一条加法电路,同时干加法和减法的活。硬件省了一大块,这是补码最硬核的价值——把"别扭"一次性留给"人理解",把"简单"永久留给"机器执行"。
顺带:为什么 8 位是 -128 ~ 127,多出一个 -128?
8 位补码能表示的范围是-128 ~ 127,负数比正数多一个。原因就在于那个"唯一的零":正数占掉0 ~ 127共 128 个,剩下1000 0000这个最高位是 1 的数,不再需要表示-0了,就腾给-128。
408 提示:补码是"数据的表示与运算"里的核心,选择题、计算题都爱考。重点抓三件事——补码的定义与转换、“取反加一”、补码的表示范围与溢出判断。
一个反直觉的设计,凭什么活了半个世纪
补码看着别扭:你要读一个负数,得先"取反加一"才算得回它的值。但设计者清楚地知道,这种"别扭"只发生在人看的时候,而"爽"却发生在机器每一次执行加法的时候。用一次性的理解成本,换一辈子的执行成本,这笔账,划算得不能再划算。
关于"数据表示"还有一件更反直觉的事——浮点数加减法里,0.75 + 0.125这种看着整整齐齐的数,CPU 也不能直接加,要先"对阶"再"舍入"(0.75 + 0.125,CPU 竟不能"直接"加?浮点加减的对阶与舍入,一次算给你看)。补码和它,其实是同一套逻辑:机器的"简单",从来不是白来的。
从 ENIAC 到今天你手里的手机,CPU 的指令集换了一茬又一茬,唯独"负数用补码存"这件事,半个多世纪纹丝不动。真正好的设计,不是"你觉得顺眼",而是几十年后回头看,发现找不到一个更好的替代。
想把这些底层原理串成体系,为计算机考研打好底子,推荐 B站【408实验室】的《数据结构》系统跟学。