同一个数,二进制、八进制、十进制、十六进制都能写。选哪个不是随便定的, 背后是「这个数要被用来干什么」。
进制是为场景服务的
- 二进制:看位域、看标志位。每一位直接对应硬件里的一根线
- 十六进制:读内存、看协议、查寄存器。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 / -10x7FFFFFFF= 2147483647(32 位最大有符号正数)0xFFFFFFFF= 4294967295 / -1
看到 0x7F 就知道「再加一就溢出到符号位了」,看到 0x80
就知道「这是负数的最小值」。这类直觉比查表快。