十六进制编码:为什么程序员离不开 Hex
什么是十六进制?
先给你看一段东西:
Offset 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000 89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52
00000010 00 00 01 00 00 00 01 00 08 06 00 00 00 1F 15 C4
这是一张 PNG 图片的前 32 个字节。左边的 89 50 4E 47 是 PNG 的文件签名——但如果你不熟悉十六进制,看到的就只是一堆数字和字母的随机排列。
说实话,我第一次 debug 的时候看 hex dump 完全是天书。一堆 0A、FF、7B 摆在那,完全不知道在表达什么。后来才明白,这东西其实简单得离谱——它只是换了一种方式写数字而已。
十六进制(Hexadecimal,简称 Hex)就是一种基数为 16 的计数系统。日常用的十进制是基数 10,用 0 到 9 这十个符号;Hex 用 0 到 9 加上 A 到 F,一共 16 个符号。十六进制中的 A 等于十进制的 10,F 等于 15。
说白了,它就是二进制的人类友好版本。
工作原理
为什么偏偏是 16?不是 14 或 18?
因为 2 的 4 次方 = 16。一个十六进制数字正好能表示 4 个二进制位(bit)的所有可能组合:
二进制 十六进制 十进制
0000 0 0
0001 1 1
0010 2 2
0011 3 3
0100 4 4
0101 5 5
0110 6 6
0111 7 7
1000 8 8
1001 9 9
1010 A 10
1011 B 11
1100 C 12
1101 D 13
1110 E 14
1111 F 15
这就是十六进制最核心的优势:一个 Hex 字符刚好等于 4 个 bit,两个 Hex 字符刚好等于 1 个字节(8 bit)。这个映射是完美的,不多不少。
转换实操
拿十进制数字 42 来说。转十六进制的方法很直接——不断除以 16,收集余数:
42 ÷ 16 = 2 余 10 → 余数 10 = A
2 ÷ 16 = 0 余 2 → 余数 2 = 2
从下往上读,42 的十六进制表示是 2A。反过来,2A = 2×16 + 10 = 32 + 10 = 42,验证无误。
再拿一个字节 11001011(二进制)来说。直接一分为二——高 4 位 1100 = C,低 4 位 1011 = B,所以就是 CB。根本不用经过十进制。我之前考试手算二进制-Hex 转换的时候,就是先把中间结果写成十进制再转 Hex,绕了一大圈,后来被室友笑话了——直接用 4 位分组,一秒出结果。
0x 前缀是什么鬼?
在代码里你经常能看到 0x2A、0xFF 这种写法。0x 不是 Hex 数据本身,它只是一个前缀标记,告诉读代码的人(和编译器):“后面跟的是十六进制数”。
不同语言有不同的约定:
| 前缀 | 语言/场景 | 示例 |
|---|---|---|
0x | C, Java, JS, Python, Go | 0xFF = 255 |
$ | 汇编, Pascal, 旧式 BASIC | $FF = 255 |
h 后缀 | 部分汇编方言 | FFh = 255 |
# | HTML/CSS 颜色 | #FF0000 = 红色 |
有趣的是,CSS 颜色里用的是 # 而不是 0x。我第一次写前端时就在 background-color: 0xFF0000 上栽了跟头——浏览器完全不认,查了 MDN 才发现要用 #。
核心特性
| 特性 | 说明 |
|---|---|
| 基数 | 16,用 0-9 和 A-F(大小写不敏感) |
| 与二进制的关系 | 1 个 Hex 字符 = 4 bit,2 个 Hex 字符 = 1 字节 |
| 膨胀率 | 比原始二进制大 100%(1 字节变 2 个字符) |
| 可读性 | 远优于二进制字符串,是人类阅读字节数据的最佳折中 |
| 大小写 | 不敏感——0x0A 和 0x0a 完全等价 |
| 标准化 | 几乎所有编程语言内置支持 |
大小写不敏感这一点,日常开发可能觉得理所当然,但在 Base64 的世界里大小写代表完全不同的值——Base64 对大小写敏感。所以 Hex 在需要人工核对哈希值或者手动输入密钥的时候格外友好。
实际应用场景
1. 哈希值显示
SHA-256、MD5 等哈希算法的结果是一串二进制字节,没人能直接看懂。十六进制是标准的呈现格式:
SHA-256("hello") = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
Git 的 commit hash 也是十六进制。你在 git log 里看到的 commit d3a9f1e 本质上是一个 160 位的 SHA-1 哈希值,十六进制让它可以被人类读出来和比对。
2. 颜色代码
前端开发者每天和 Hex 颜色打交道——#FFFFFF 是白色,#000000 是黑色,#FF5733 是一种橙色。
六位 Hex 颜色码的结构是:#RRGGBB。FF = 255(红色通道全亮),00 = 0(绿色通道全暗),33 = 51(蓝色通道偏暗)。你把它们写在一起就是 #FF0033。用三个通道各 8 位(共 24 位),正好对应 1600 多万种颜色。
3. 二进制文件调试(Hex Dump)
这是 Hex 真正的硬核场景。无论文件是什么格式——图片、可执行文件、网络数据包、数据库文件——你都能用一个 hex dump 工具展示它的原始字节内容:
$ xxd -l 64 hello.png
00000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452 .PNG........IHDR
00000010: 0000 0100 0000 0100 0806 0000 001f 15c4 ................
00000020: 4489 0000 0001 7352 4742 00ae ce1c e900 D.....sRGB......
00000030: 0000 0425 7445 5874 6461 7465 3a63 7265 ...%tEXtdate:cre
左边是偏移量(十六进制),中间是原始字节(十六进制),右边是 ASCII 可视字符。三栏布局让 hex dump 成为逆向工程和底层调试的终极武器。
我之前排查一个文件解析的 bug,表面上看起来是 JSON 解析失败,实际原因是一个文本文件开头藏着一个 UTF-8 BOM(EF BB BF)。用 hex dump 打开一看,三个多余的字节赫然在目——如果不是 hex dump,这问题根本看不到,所有文本编辑器都把它们隐藏了。
4. 内存地址
如果你写过 C/C++ 或者用过调试器,内存地址清一色是十六进制。比如 0x7ffc8b3a。这不是程序员装酷——计算机内存总量通常是 2 的幂(4GB、8GB),用十六进制表示地址比十进制自然太多了。0xFFFFFFFF 一眼就知道是 32 位地址空间的最大值,十进制 4294967295 完全失去了这种”边界感”。
5. Unicode 码点
Unicode 码点通常用十六进制表示:U+4E2D 是”中”字,U+1F600 是表情 😀。BMP(基本多文种平面)之外的字符需要超过 16 位,Hex 也能轻松表示——U+1F600 一眼就能看出这个字符在哪个范围。
常见误区
误区一:Hex 是一种加密
可能是 0x 前缀自带的神秘感造成的吧。Hex 完全不是加密——它就是一种数字的书写格式。0x41 和十进制的 65 写出来的值一模一样,只是换了一副面貌。我之前见过一个项目把用户的邮箱地址转换成十六进制存进数据库,标注为”加密存储”,这跟明文存储真的没有任何区别。
误区二:十六进制字符串比原始数据更”小”或更”快”
正好相反。一个字节写成 Hex 变成两个字符,传输体积翻倍。如果你在网络 API 里用 Hex 传输二进制数据,而客户端本来就能接收 binary,你就是在用双倍带宽。
Base64 为什么比 Hex 更常见于二进制传输场景?就是因为 Base64 只膨胀约 33%,Hex 膨胀 100%。传输效率完全不在一档。
误区三:0x 开头的数字是十进制不可读的
我以前真的这么想过——看到 0x7FFF 就觉得排斥,本能想把它转成十进制再理解。后来发现这完全多此一举。0x7FFF 在十六进制视角下是”0111 1111 1111 1111”,一眼就知道是有符号 16 位整数的最大值。换成十进制 32767,这个”结构信息”全丢了。十六进制让你看到数据的形状,不只是它的数量。
Hex vs 其他编码方式
| Hex (Base16) | Binary | Decimal | Base64 | |
|---|---|---|---|---|
| 每字节字符数 | 2 | 8 | 1-3 | ~1.33 |
| 膨胀率 | 100% | 700% | 不定 | ~33% |
| 人类可读 | 高 | 极低 | 中 | 低 |
| 与二进制的映射 | 完美(4 bit/字符) | 直接 | 无直接映射 | 6 bit/字符 |
| 主要用途 | 哈希、颜色、调试 | 底层硬件 | 日常 | 数据传输 |
| 大小写敏感 | 否 | 无 | 无 | 是 |
Binary 一个字节需要 8 个字符来写,完全没法读。Decimal 的问题是跟二进制的映射不直接——你没法一眼从 255 推出它的二进制形式。Hex 在这两项上恰好是最佳平衡点:不太长,又能保留二进制结构的清晰可见性。
不过话说回来,Hex 也有它的不爽之处。当数据量一大,满屏幕的十六进制字符带来的辨识疲劳是真实存在的。看多了之后 8 和 B,0 和 D 在某些字体下几乎分不出来——所以选一个好的等宽字体对经常读 hex dump 的人来说不是小事。
常见问题
Q: 为什么十六进制用 A-F 而不是其他符号?
纯历史原因加上实用主义。26 个字母里 A-F 是前 6 个,自然选择。如果继续往下用到第十六个字母 P,也是可行的——但 A-F 的记忆负担小,而且刚好够 6 个额外的符号(总共 16 个 = 10 个数字 + 6 个字母)。早期的十六进制文档里甚至有厂商用过 u-z 但被 A-F 统一掉了。
Q: 大写 A-F 和小写 a-f 有区别吗?
语义上完全没区别,0xFF = 0xff = 255。不过约定俗成的倾向是:代码里常量多用大写 0xFF,CSS 颜色多用小写 #fff(虽然大小写都合法),哈希字符串多用小写。MDN 的 CSS 文档里颜色示例几乎全用小写,但你在 C 语言里写的 0xFFFF 用大写几乎是本能——没什么道理,审美惯性。
Q: 怎么在代码里做 Hex 转换?
几乎所有语言都内置了。JavaScript 里:
// 十进制 → 十六进制
(255).toString(16); // "ff"
// 十六进制 → 十进制
parseInt("ff", 16); // 255
Python 里:
hex(255) # '0xff'
int("ff", 16) # 255
不过提醒一句,JavaScript 的 toString(16) 不会自动补前导零。(15).toString(16) 返回 "f",如果你期望和某个格式对齐(比如颜色必须二位),记得自己 padStart(2, "0")。
Q: Hex dump 里的 ASCII 区域为什么有些字符显示为 .?
Hex dump 通常会在右侧附上 ASCII 对照区域。ASCII 里 0x20 到 0x7E 是可打印字符(字母、数字、标点),之外的都是控制字符(如 0x0A = 换行、0x00 = null)或超出范围,无法在终端里展示。xxd 这类工具就用一个 . 代替不可打印字符,表示”这里有字节,但不可见”。这不是数据损坏了,只是它不可打印。
Q: Hex 可以直接用来做数学运算吗?
完全可以。0x0A + 0x05 = 0x0F(10 + 5 = 15)。当你熟练了之后,在十六进制里做加减法甚至比十进制更直观——因为进位规则是 16,和字节的边界天然对齐。不过老实说,大部分人是没法完全脱离十进制心算的,一般场景里知道 0x10 = 16、0xFF = 255、0xFFFF = 65535 这几个关键值就够用了。