工具拆解

二进制、十六进制,以及为什么负数要用补码

为什么十六进制淘汰了八进制?-1 为什么是全 1?同样 16 个 1 为什么能读成 65535 也能读成 -1?把进制的选择逻辑和补码的来历讲清楚。

同一个数,二进制、八进制、十进制、十六进制都能写。选哪个不是随便定的, 背后是「这个数要被用来干什么」。

进制是为场景服务的

  • 二进制:看位域、看标志位。每一位直接对应硬件里的一根线
  • 十六进制:读内存、看协议、查寄存器。4 位一个字符,和字节边界对得整齐
  • 十进制:业务逻辑。人需要直观比较大小
  • 八进制:历史遗留,现在只剩 Unix 文件权限(0755)还在用

为什么十六进制赢了八进制?关键在「一个字节是 8 位」。十六进制两位正好表示一个字节 (0xFF),八进制需要三位,永远对不齐。看内存 dump 的时候, 十六进制是 4 个字符一组两个字节,八进制就散成一片。

至于二进制,虽然最长,但它是唯一「所见即所得」的进制:每一位在屏幕上的位置, 就是它在总线上的位置。分析位域的时候,没有替代品。

补码:让减法消失

计算机里只有加法器,没有减法器。这不是偷懒,是刻意的设计—— 如果能用加法实现减法,电路可以少一大块。

补码就是这个技巧。规则很简单:负数 = 该数加上 2 的位宽次方。

以 8 位为例:

  • -1 → -1 + 256 = 255 → 11111111
  • -2 → -2 + 256 = 254 → 11111110
  • -128 → -128 + 256 = 128 → 10000000

这样 5 + (-3) 就变成 5 + 253 = 258,8 位截断后是 2 —— 正好是正确答案。 硬件不需要知道哪个数是负的,加法器照常工作。

一个最好记的规律

-1 在任何位宽下都是「全部位为 1」。

8 位是 0xFF,16 位是 0xFFFF,32 位是 0xFFFFFFFF。 原因很直观:从全 0 借位减 1,每一位都要向前借,结果全是 1。

这个规律在调试时特别好用。你看到 0xFFFFFFFF,第一反应应该是「-1 或者初始化值」, 而不是「四十二亿」。

不对称:负数比正数多一个

8 位补码的范围是 -128 到 127,不是 -127 到 127。

因为 256 个位模式要分给正负两边,0 占了一个,剩下 255 个奇数,没法平分。 多出来的那个给了负数,因为 10000000 这个位模式如果按「负零」处理, 会有 +0 和 -0 两个零,比较和判等都会变复杂。所以它被定义成 -128。

同样的道理:16 位是 -32768 到 32767,32 位是 -2147483648 到 2147483647。 取值范围永远是「负的多一个」。

同样的位,两种读法

0xFFFF 这 16 个 1,当无符号读是 65535, 当 16 位有符号读是 -1。

硬件层面完全一样,都是 16 根线上的高电平。差别只在编译器怎么解释。 这也是跨语言、跨协议传数据时最容易吵架的地方—— 「我发的是 65535,你怎么收到 -1」。

所以协议定义里必须明确写清有符号还是无符号。C 语言里 int 和 unsigned int 的位模式可能一样,一次隐式转换就能翻转语义。

位宽截断是个安静的坑

把 64 位的值塞进 32 位字段,高位会被静默丢弃。程序不报错,结果就是错的。

最典型的场景:时间戳、内存地址、哈希值这类「本来就需要 64 位」的量, 被放进 32 位字段。截断之后数值变得毫无规律,排查起来非常费劲。

工具里我加了「已按 N 位截断」的提示,就是为了让这件事显式暴露出来, 而不是悄悄发生。

常用值的对照表

记住这几个数,能覆盖大部分日常判断:

  • 0xFF = 255(8 位全 1)
  • 0x7F = 127(8 位最大有符号正数)
  • 0x80 = 128 / -128(8 位符号位是 1、其余全 0)
  • 0xFFFF = 65535 / -1
  • 0x7FFFFFFF = 2147483647(32 位最大有符号正数)
  • 0xFFFFFFFF = 4294967295 / -1

看到 0x7F 就知道「再加一就溢出到符号位了」,看到 0x80 就知道「这是负数的最小值」。这类直觉比查表快。

打开「进制转换」→ 更多文章