📄 一句话概括

十六进制是二进制的"官方缩写"——4 位并 1 位,两位刚好一个字节,所以颜色、地址全用它。负数没有"负号"可存——工程师试了原码(错)、反码(半对),最后用补码:在 256 格的时钟上,倒拨 5 格和正拨 251 格落在同一点,所以 −1 就是 255、−5 就是 251。03 篇"扔掉溢出位"的秘密,全在这口钟上。


1. 开场:两个你迟早会撞上的谜题

  1. 一串 8 位二进制 11111111——是无符号的 255,还是有符号的 −1?
  2. 颜色代码 #FF8800 里的字母是什么意思?为什么 0~9 不够还要 A、B、C?

这篇一并解开。前半讲十六进制(快),后半讲负数(本篇核心)。

2. 十六进制:给人看的二进制缩写

二进制有个致命缺点:太长。写个 255 要 8 位,写个内存地址要 32~64 位——人根本读不动。十六进制的解法很聪明:

2⁴ = 16,所以恰好每 4 位二进制并成 1 位十六进制;8 位(1 字节)就是 2 位。

十六进制需要 16 个符号:0~9 用完了,补上 A=10、B=11、C=12、D=13、E=14、F=15。

二进制 十六进制 十进制
0000 0 0
0101 5 5
1000 8 8
1010 A 10
1111 F 15
11111111 FF 255

换算是"直通车",不用过十进制:FF8800 逐位拆 → 1111 1111 1000 1000 0000 0000。

💡 十六进制出没的地方(见到就该条件反射"这是二进制的缩写")
  • 颜色代码 #FF8800(网页、设计软件)
  • 内存地址 0x7FFF5FBFF8C0(0x 前缀 = 十六进制)
  • MAC 地址 A4-5E-60-1F-2B-3C、UUID、Unicode 码点 U+4E2D 全是同一件事:把一长串 0/1 压短给人看。

3. #FF8800 解剖:三个字节 = 三盏灯

02 篇讲过:像素颜色 = 红绿蓝三通道,各 0~255(一个字节)。一个字节恰好两位十六进制,所以:

# FF 88 00
  │  │  └─ 蓝 = 0        = 00000000(全关)
  │  └──── 绿 = 0x88 = 136 = 10001000(过半)
  └─────── 红 = 0xFF = 255 = 11111111(拉满)
→ 红加绿 = 橙色

为什么颜色天生适合十六进制?因为 6 个十六进制位 = 3 个字节 = 三通道,严丝合缝。同理,为什么满屏都是 255?——F = 15 = 1111,FF = 255 = 一个字节全开。

4. 负数怎么存:三次演变(本篇核心)

现在解第二个谜题。先明确约束:内存里只有 0 和 1,"负号"这个字符不存在——负号也必须用位来编码。历史上试了三版,值得从头走一遍,因为每一次"打补丁"都暴露一层认知:

第一版:原码——最高位当符号位

最自然的想法:最高位 0 表正、1 表负,其余 7 位照常。

+5 → 00000101      −5 → 10000101

直观,但一做加法就露馅:

5 + (−5):00000101 + 10000101 = 10001010(按位直接加)= −10?错得离谱。 更糟的是 0 有两个:00000000(+0)和 10000000(−0)——电脑判断"等于零"要先处理两个零,处处别扭。

结论:符号位如果只是个"标记",就等于让加法器做加法时还得"看情况",电路无法统一。

第二版:反码——负数把正数按位取反

改进:负数 = 对应正数按位取反(0↔1)。

+5 → 00000101      −5 → 11111010

进步了:5 + (−5) = 00000101 + 11111010 = 11111111,加法方向对了。但两个零还在(00000000 和 11111111),加法在进位绕圈时还差 1。

第三版:补码——反码再加 1,问题根治

最终方案:负数 = 反码 + 1。

数 原码 反码 补码(实际采用)
+5 00000101 00000101 00000101
−5 10000101 11111010 11111011
−1 10000001 11111110 11111111
+0 00000000 00000000 00000000
−0 10000000 11111111 (+1 溢出 → 00000000,两个零合并成一个!)

验证加法:5 + (−5) = 00000101 + 11111011 = 1 00000000——第 9 位溢出,扔掉(03 篇那一招),正好剩 00000000 = 0。✓

📝 演变的逻辑(这段比结论重要)

原码 → 反码 → 补码不是三套并列的理论,而是一条"发现问题 → 打补丁 → 根治"的路线:原码死在"符号位只是装饰、加法器没法统一处理";反码救了一半但两个零还在;补码加 1 这个动作顺手把 −0 推过一圈、和 +0 重合——零只有一个了,加法器从此不用看任何情况,一视同仁地加。

5. 补码为什么"合法":256 格的时钟

补码不只是"凑出来的技巧",它有坚实的数学身份——模运算。想象一口 8 位专用的钟:只有 256 个刻度(0~255),转满 256 回到原地。

在这口钟上:

📝 为什么范围是 −128 ~ +127?负的反而多一个?

256 个位组合要分给正负。补码规定上半圈(128~255)当负数、下半圈(0~127)当非负。这样 0 占一个位置后,正数(含 0)128 个、负数也是 128 个——但正数最大 127、负数最小 −128,负端比正端多 1,因为 0 这个"占位"算进了正侧。这是方案自洽的代价,不是 bug。

6. 回答开场谜题:255 还是 −1?——看上下文

现在两道题都有了答案:

7. 常见误区

⚠️ 误区一:十六进制是电脑内部在用的进制

电脑内部只有二进制。十六进制是写给人看的缩写,跟中文备注一样,机器从不"使用"它。 [!warning] 误区二:负数 = 符号位 + 绝对值 那是原码——已被淘汰。真实的负数存的是补码(模意义下的"正拨量"),所以负数做加法不用任何特殊处理。 [!warning] 误区三:−1 的补码 11111111 是"设计得巧" 不是巧,是必然:钟上 −1 本来就该在"满一圈差一格"的位置。你推任何负数都会落在它该在的格子。 [!warning] 误区四:补码取负 = 改符号位 取负的正确操作:按位取反再 +1(或"从右往左找到第一个 1,左边全部取反")。直接改最高位得到的是原码,做加法会错。 [!warning] 误区五:8 位补码能表示 −128~+128 上限是 +127。−128~+127 共 256 个数,一个不多一个不少;"128 的负数"(−128 的相反数)在这套编码里存不下,硬存会绕圈出错。

8. 可折叠自测题

❓ - Q1. 为什么偏偏是 16 进制,而不是 10 或 8?

2⁴=16:4 位二进制并 1 位十六进制,2 位 = 1 字节,切分天然对齐。8 进制(3 位并 1)和字节(8 位)对不齐,所以字节时代十六进制完胜。 [!question]- Q2. 手算 #FF8800 的 RGB 三个值。 FF=255、88=8×16+8=136、00=0。红 255、绿 136、蓝 0 → 橙色。 [!question]- Q3. 原码和反码各死在什么问题上? 原码:符号位只是装饰,加法器没法统一处理,且 +0/−0 两个零。反码:加法方向对了,但两个零仍在、进位绕圈还差 1。补码加 1 后两零合一、加法完全统一。 [!question]- Q4. 用 8 位补码算 10 − 4,写出过程。 10 = 00001010;−4:4=00000100 → 反 11111011 → +1 = 11111100。 相加:00001010 + 11111100 = 1 00000110,扔掉第 9 位 → 00000110 = 6。✓(03 篇做过,现在你知道每一步为什么合法了) [!question]- Q5. 11111111 到底是 255 还是 −1?10000000 呢? 都对——取决于程序把它声明成有符号还是无符号。11111111:无符号 255 / 有符号 −1;10000000:无符号 128 / 有符号 −128。存储相同,读法(类型)决定含义。


9. 下一步学什么


10. 相关笔记