DevKitLab Logo DevKitLab
进制 / 十六进制 / 二进制

十六进制、二进制与十进制:程序员真正在用的数字读法

一个颜色是 #FF5733。一个文件权限是 chmod 755。一次权限检查是 flags & 0x04。一个常量是 0b1010、或 0xFF、或 255。每一种看着都像自成一派的方言,其实它们是同一个数字换了身衣服——而让这一切不再是摩擦的那点熟练,比看上去要小得多:字节用十六进制,位用二进制,给人看用十进制。

你一次又一次遇见同一批数字,只是每次换了身衣服。一个颜色是 #FF5733。一个文件权限是 chmod 755。一次权限检查读作 flags & 0x04。一个常量是 0b1010、或 0xFF、或 255。每一种写法看着都像自成一派的小方言,而当你需要在它们之间来回——0xFF 真的是 255 吗?chmod 755 是不是我以为的那个意思?——你就去抓一个转换器,或者掰手指头。让这一切不再是摩擦的那点熟练,比看上去要小得多,而它从一个想法开始。

想法就是:十六进制、二进制、十进制不是三种数字——它们是同一个数字的三种写法2550xFF0b11111111 是一个值的三种记法,正如「十五」、「15」、「XV」是一个值的三种记法。一个数字并不拥有一个进制;一个被写下来的数字才有。所以真正的本事不是转换——工具一键就能完成转换——而是知道你为什么要切:你挑那个能让这个数字的结构显出来的写法。当这个数字其实是一堆字节时(颜色、哈希、地址),用十六进制。当单个位本身就承载含义时(标志位、掩码、硬件),用二进制。当只是要给人读一个计数时,用十进制。

这篇文章先花一分钟讲清进制到底是什么——是分组,不是身份——再讲为什么一位十六进制恰好是四个比特,正是这一点让十六进制成为一扇望向二进制的紧凑窗口。然后是你真正会碰到它们的地方:颜色、chmod、位掩码。再然后是那些只有当你能看见比特时才讲得通的位运算,以及那些当你看不见时会咬你的坑——有符号数、字节序。

唯一的核心:进制是你怎么分组,不是这个数字本身

每一个数字都是对某样东西的计数。一个进制,不过是你在开一个新列之前,把它们分组进去的那个「桶」的大小。在十进制(base 10)里,每一列是 10 的幂:255 = 2×100 + 5×10 + 5×1。在十六进制(base 16)里,每一列是 16 的幂,数字用 0–9、再用 A–F 表示十到十五:0xFF = 15×16 + 15×1 = 255。在二进制(base 2)里,每一列是 2 的幂:0b11111111 = 128+64+32+16+8+4+2+1 = 255。每一次都是同一个 255。值就是那个值;进制只决定它怎么拼写——这也是为什么「把十六进制转成十进制」从来不改变那个数字,只改变你看它的视角。

十六进制:一扇望向字节的紧凑窗口

程序员为什么动不动就用十六进制,归结到一个漂亮的巧合:16 是 2⁴,所以一位十六进制恰好是四个比特——一个半字节(nibble)——而两位十六进制恰好是一个字节(8 位,0–255,0x000xFF)。正是这份对齐,是它全部的吸引力。一个用十进制写的字节(77)对它的比特只字不提;同一个字节写成十六进制(0x4D)则干净利落地拆成两个半字节,每个半字节对应你可以直接读出来的四个比特。拿 0x2F 自己算一遍:拆成两个半字节 2F,展开成整字节 0010 1111,再把为 1 的那些列加起来——32 + 8 + 4 + 2 + 1 = 47。所以十六进制其实就是给人看的二进制——字节和比特的全部结构,用四分之一的位数呈现。这就是为什么哈希、颜色、内存地址、字节转储都用十六进制写:它们是一堆字节,而十六进制把字节显出来。

二进制:当单个位本身就承载含义

有时你要的不是那个紧凑视图——你要的就是比特本身,因为每一个都意味着某样东西。那就是二进制,base 2,每一位就是一个比特:开或关、置位或清零。当一个数字压根不是一个数量、而是一组各自独立的开关时,你就降到二进制——一份权限集、一个功能开关整数、一个第 3 位启用某功能、第 6 位表示「出错」的硬件寄存器。在那些场景里,十进制的 13 什么有用信息都没告诉你,而 0b1101 说的是第 0、2、3 位是开的——这才是这个数字真正在搬运的信息。

你真正会碰到它们的地方

三个日常例子把这些概念具体说明:

  • 颜色是三个字节的十六进制#FF5733 就是 FF 57 33——红 0xFF(255)、绿 0x57(87)、蓝 0x33(51),每个都是一个 0 到 255 的字节。#RRGGBBAA 再加第四个字节表示透明度。一旦你把颜色看成三个字节,#FFFFFF(每个字节都拉满 → 白)和 #000000(全零 → 黑)就不再神秘。
  • Unix 权限是八进制——第三种进制chmod 755 既不是十进制也不是十六进制;它是八进制(base 8),每一位恰好是三个比特:r w x7111 = 读+写+执行;5101 = 读+执行、不可写。所以 755 就是给「属主/同组/其他」的 rwx r-x r-x。八进制之所以在这里活下来,恰恰因为「每位三比特」正好对上那三个权限位。(现代代码把它写成 0o755。)
  • 标志位是你用位运算去测的比特。一组被塞进一个整数的选项,每个选项是 2 的幂、于是各占一个比特:READ = 1 (0b001)WRITE = 2 (0b010)EXEC = 4 (0b100)。你用 OR 把它们合起来(READ | WRITE0b011),用 AND 测其中一个(flags & WRITE 在那一位置位时非零),清掉一个则跟它的反码做 AND。flags & 0x04 里的那个 0x04,不过是第 2 位披了件十六进制的外衣。

位运算:一旦看得见比特就一目了然

这些运算按位逐个进行,而当你把操作数写成二进制或十六进制的那一刻,它们就从晦涩变得直观:

  • AND(&——只有两边都是 1 的位才是 1。用来位或掩码x & 0xFF 保留低字节、把其余清零。
  • OR(|——任一边是 1 就是 1。用来位:x | 0x04 把第 2 位打开。
  • XOR(^——两边不同的位才是 1。用来翻转位,也出现在一些简单的校验算法里。
  • NOT(~——把固定位宽内的每一位都翻过来,所以结果取决于类型有多少位宽。
  • 移位(<<>>——把比特整体往左或往右滑。对无符号数、且没发生溢出时,x << 1 相当于 ×2、x << 8 把一个值整整抬高一个字节;对非负数,x >> 4 丢掉低四位。(对有符号数,右移可能做符号扩展,具体行为取决于语言。如果需要可预测地处理位字段,优先使用无符号类型,并遵循具体语言对右移和溢出的规定。)移位正是你打包和拆包字段的方式——(r << 16) | (g << 8) | b 就把三个各自独立的字节拼成一个颜色。

试试 0b1100 & 0b1010,答案明明白白是 0b1000;换成十进制的 12 & 10,就成了一道谜题。这就是在做位运算之前先切换写法的全部理由。

容易出错的地方

有两处,这套整齐的图景会失效,两处都值得知道:

  • 有符号 vs 无符号,以及二的补码。同一串比特,可以按无符号读,也可以按有符号读。计算机用二的补码(two’s complement)存负数,在固定位宽的表示里,最高位当符号——所以在 8 位二的补码里,字节 0xFF 按无符号是 255,按有符号是 -1。这正是那个经典 32 位溢出背后的机制:一个有符号的 32 位计数器封顶在 0x7FFFFFFF,再加一步就会越过它、翻向一个很大的负数——这也是为什么许多用有符号 32 位整数存时间的 Unix 系统会撞上 2038 年问题。(至于语言怎么应对这次溢出,各不相同——有的回绕,有的抛错,有的是未定义行为。)
  • 字节序(endianness)。当一个数字跨越好几个字节时,这些字节可以按大端(最高位字节在前)或小端(最低位在前)存放。值 0x01020304 在大端布局里存成 01 02 03 04,在小端布局里存成 04 03 02 01——所以一段看着「反了」的字节转储,往往不是损坏,只是小端。

还有两个小便利收个尾。前缀 0x(十六进制)、0b(二进制)、0o(八进制)存在,是为了让一个写下来的数字自报进制——0x10 是 16,0b10 是 2,10 是十。而十六进制或二进制里的前导零只是宽度0x0F0xF 是同一个值,那个多出来的零只是补齐到一个整字节、方便看。

快速参考

当你在实际代码里遇到一个看着陌生的数字,先把它归位,再动手算:

  1. 先读前缀0x 十六进制,0b 二进制,0o(或老 C 里前导的 0)八进制,没有前缀 → 十进制。六位十六进制前面一个 # → 一个 RGB 颜色。
  2. 让进制去对上结构。字节(颜色、哈希、地址)→ 十六进制。各自独立的位(标志、掩码、寄存器)→ 二进制。给人看的计数 → 十进制。权限 → 八进制。
  3. 一位十六进制 = 4 比特,两位 = 一个字节。就这一个事实,就能在脑子里把大多数十六进制转成比特、再转回来。
  4. 要做位运算,先切到二进制或十六进制,然后用 AND 测/掩码、OR 置位、XOR 翻转、移位挪字段。
  5. 值看着离谱地不对时,想想有符号和字节序——一个本该很小却巨大的数,往往是把有符号当无符号读了,或者字节是另一种端序。(一条乱掉的 URL 里那些 %XX 对,也是十六进制字节——同一套本事就能读它们。)

至于超出心算的一切——一个 64 位掩码、一个不常见的进制、核对一个二的补码值——一个进制转换器会把十六进制、十进制、二进制、八进制并排列出来,让你一眼就看见同一个值在每种记法里的样子。这一切底下,还是那个唯一的核心:它们是同一个数字换了身衣服,而所谓熟练,不过是知道哪身衣服能让结构显出来