二进制编码基础:理解计算机如何表示文本
什么是二进制?
二进制就是只用 0 和 1 两个数字表示一切信息的计数系统。
你可能会想:我们平时用十进制,0 到 9 十个数码,不是挺自然的吗?为什么计算机非要用只有两个数码的二进制?
答案跟硬件有关。计算机最底层是晶体管,晶体管做开关用——要么导通,要么截止。两种状态,恰好对应 0 和 1。二进制就像电灯开关,只有开和关两种状态。你让晶体管表示十种不同的电压级别,不是不行,但那电路会复杂到没人愿意做。
我之前教新人时发现,很多人对二进制的第一反应是”这不就是一堆 0101 吗,跟我写代码有什么关系”。关系大了。你在代码里写的每一个字符、每一张图片、每一段音频,最后到了 CPU 和内存那里,全是二进制。理解了二进制,你才能真正明白”字符编码为什么会乱码”、“为什么 1KB 等于 1024 字节而不是 1000”这些看似简单但其实能拦住一大片人的问题。
二进制的工作原理
原理其实不复杂。我们从最基础的讲起。
比特与字节
一个二进制位叫 bit(比特),它只能存 0 或 1。8 个比特捆在一起叫一个 byte(字节),这是一个非常核心的概念。
为什么是 8 个一组?说实话,这是历史惯性。早期的计算机用 6 位、7 位、8 位的都有,最后 8 位胜出,因为它是 2 的幂,寻址方便,而且正好能塞下一个 ASCII 字符。
一个字节可以表示多少种不同的值?2^8 = 256。从 00000000(十进制 0)到 11111111(十进制 255)。这个 256 后面会反复出现,比如一个 IP 地址的每一段最大就是 255,这就是字节的容量上限。
| 单位 | 位数 | 可表示范围 |
|---|---|---|
| 1 bit | 1 | 0 ~ 1 (2^1 = 2) |
| 1 byte | 8 | 0 ~ 255 (2^8 = 256) |
| 2 bytes | 16 | 0 ~ 65535 (2^16) |
| 4 bytes | 32 | 0 ~ ~42 亿 (2^32) |
二进制怎么表示文字?ASCII
光有 0 和 1 不够,我们要用它来表示字母、数字和符号。最早的标准化方案是 ASCII(American Standard Code for Information Interchange)。
ASCII 用 7 个 bit 来表示一个字符——虽然实际存储时会占 8 个 bit(一个字节),多出来的那一位早期被用作校验位,现在基本上就闲置着。
比如大写字母 A 在 ASCII 表里排第 65 号:
十进制: 65
十六进制: 0x41
二进制: 01000001
小写 a 排第 97 号:
十进制: 97
十六进制: 0x61
二进制: 01100001
注意到没有?大写 A 和小写 a 的二进制,只有第 5 位不一样(从右往左数,第 5 位从 0 翻成了 1)。这不是巧合,这是当年 ASCII 设计者故意留的——大写字母的二进制值加上 32(即 2^5)就等于对应的小写字母。这让大小写转换变成了一个位运算,而不是查表,在那个 CPU 周期按微秒计算的年代,这是非常实用的优化。
二进制与十进制的转换
二进制转十进制:每一位上的 1 代表 2 的对应次幂,加起来就行。
二进制 1101
= 1×2^3 + 1×2^2 + 0×2^1 + 1×2^0
= 8 + 4 + 0 + 1
= 13(十进制)
十进制转二进制:不断除以 2,记下余数,倒序排列。
以 13 为例:
13 ÷ 2 = 6 余 1
6 ÷ 2 = 3 余 0
3 ÷ 2 = 1 余 1
1 ÷ 2 = 0 余 1
余数从下往上读:1101,就是 13 的二进制。
说回正题,这个转换机制理解了之后,hex(十六进制)也就自然明白了——四位二进制正好对应一位十六进制,所以十六进制常用于替代二进制来做简写。你去看内存 dump 或者调试数据,全是 hex,不会直接给你写一大串 0101。
核心特性
| 特性 | 说明 |
|---|---|
| 基数 | 2(只有 0 和 1 两个数码) |
| 最小单位 | bit(比特),一个二进制位 |
| 基本存储单位 | byte(字节),8 个 bit |
| 寻址宽度 | 现代计算机通常是 64 位(8 字节) |
| 最大单字节值 | 255(11111111) |
| 与 hex 的关系 | 4 个 bit = 1 个 hex 字符 |
字节序(endianness)是二进制存储里一个绕不开的话题——大端序(big-endian)把高位字节放低地址,小端序(little-endian)反过来。x86 架构用小端序,网络协议用大端序。我之前在写一个网络包解析工具时,就没注意字节序转换,解析出来的端口号全对不上,debug 了两小时才发现是 ntohs() 忘调了。老实说,字节序问题是二进制层面最常见的低级 bug 之一。
实际应用场景
1. 字符编码
你新建一个文本文件,敲了一个字母 A 保存。文件系统里实际存的不是字母 A 的形状,而是二进制的 01000001。文本编辑器打开这个文件时,读取字节流,查到编码表,把 01000001 渲染成屏幕上你能读的 A。一旦文件的实际字节和声明的编码不匹配,你就看到了乱码——说白了,乱码的本质就是字节流没有按正确的编码表来解读。
2. 文件格式识别
很多文件格式在文件头部留了一个叫”魔数”(magic number)的特定字节序列,用来标识文件类型:
PNG: 89 50 4E 47 — 二进制开头
ZIP: 50 4B 03 04 — PK 开头
PDF: 25 50 44 46 — %PDF
操作系统和软件通过读这个二进制标识来判断文件类型,而不是靠扩展名。你把一个 ZIP 文件改成 .pdf 的扩展名,软件试着解压,读到的还是 PK,它就知道这其实是个 ZIP。
3. 位运算优化
因为一切数据归根到底都是二进制,位运算就成了最底层的性能优化手段:
// 判断奇偶 —— 不靠取模,靠位与
// 奇数的最低位永远是 1
is_odd = (n & 1); // 比 n % 2 快
// 乘除 2 的幂 —— 不靠乘法,靠移位
x = n << 3; // n * 8
y = n >> 2; // n / 4
编译器的优化器很大程度上就是在做这类位级转化。理解二进制,你才能看懂为什么这些操作是等价的。
4. 网络协议
IP 地址的每个段是 1 个字节,范围 0-255。子网掩码本质上是按位与操作。HTTP/2 和很多现代协议把帧头压缩成极小的字段,经常出现 1 位、2 位、3 位这种级别的定义。从应用层看过去一切都很抽象,但线缆上跑的就是这些 0 和 1。
5. 数据存储容量
1KB 为什么等于 1024 字节,不是 1000?因为 2^10 = 1024,正好用 10 个比特位就能寻址 1024 个不同的字节位置。这是纯二进制层面的理由。后来 IEC 搞了 KiB / MiB / GiB 的标准(1024 进制)和 KB / MB / GB(1000 进制)区分开,但习惯的力量太强,大多数场景里 KB 还是指的 1024。
常见误区
误区一:二进制只能表示数字
这是我见过最多的误解之一。二进制本身不限制数据类型——它就是一个存储格式。同样的 4 个字节 01000001 01000010 01000011 01000100,按 ASCII 解释就是 “ABCD”,按 32 位整数解释就是 1094861636。它到底是什么,完全取决于上下文和你怎么解析它。同一个二进制串,换个解释方式就是完全不同的含义。
误区二:1 bit 就是 1 byte
新人特别容易搞混。bit(小写 b)是位,byte(大写 B)是字节。宽带带宽 100Mbps 和下载速度 12.5MB/s 之间的区别,就是 b 和 B 的区别——1 字节等于 8 位,所以 100Mbps 除以 8 才等于约 12.5MB/s。之前有个同事问我:“我家宽带 200M,怎么下载只有 25 兆,是不是被运营商坑了?“没被坑,200Mbps / 8 = 25MB/s,正是满速。
误区三:hex 和二进制是两种不同的东西
其实 hex 就是二进制的紧凑写法。4 个二进制位刚好对应 1 个十六进制位,没有任何信息损失。0x5A 就是 01011010 的简写。不过话说回来,读 hex 确实比读二进制轻松多了,人类的短期记忆一次能处理的东西有限,看一串 32 位的二进制大部分人会眼花,换成一串 8 位的 hex 就好读得多。
二进制 vs 十六进制 vs 八进制
| 二进制 (Base 2) | 八进制 (Base 8) | 十六进制 (Base 16) | |
|---|---|---|---|
| 数码个数 | 2 个 | 8 个 | 16 个 |
| 每 8 位需几位表示 | 8 个字符 | 2.67 个字符 | 2 个字符 |
| 与字节对齐 | 直接对应 | 不完美(3 bit 一组) | 完美(4 bit 一组) |
| 可读性 | 差 | 一般 | 较好 |
| 编程中用法 | 位掩码、底层调试 | Unix 文件权限(chmod 755) | 内存地址、哈希、数据 dump |
hex 和字节的对齐关系是它胜出的关键。1 个字节恰好等于 2 个十六进制字符——不多不少。八进制 3 位一组,跟 8 位的字节对不齐,所以在表示字节数据时 hex 更方便。不过 Unix 文件权限沿用八进制,chmod 755 里的 7 代表 rwx(读4+写2+执行1),这是历史原因。
常见问题
Q: 二进制数怎么快速转十六进制?
把二进制从右往左每 4 位分成一组,每组转成一个十六进制字符。比如 10101100,拆成 1010 (A) 和 1100 (C),就是 0xAC。不用算,可以记住 4 位二进制对应的 16 个十六进制字符——其实只有 16 个,用几次就记住了。
Q: 为什么 C 语言里 char 是 1 字节,但 ASCII 只要 7 位?
ASCII 设计的时候用了 7 位(128 个字符),第 8 位最初是奇偶校验位。后来各家利用第 8 位扩展出了自己的字符集(比如 Latin-1),这也就是为什么早期不同操作系统打开同一份文本文件会出现乱码——第 8 位的 128 个扩展字符在各平台上映射的字符不一样。
Q: 负整数在二进制里怎么表示?
最常见的是补码(two’s complement)。用最高位表示符号(1 表示负),其余位取反再加 1。比如 -1 在 8 位补码下是 11111111。这样做的好处是加减法可以用同一套硬件电路,不用区分正负。
Q: 32 位系统和 64 位系统的”位”是指什么?
指的是 CPU 一次能处理的二进制位数,也可以理解为内存地址的宽度。32 位系统最多寻址 2^32 = 4GB 内存,64 位系统理论上能寻址 2^64——这是个天文数字(约 16EB)。所以 32 位系统插 8GB 内存没用,多出来的那 4GB 系统根本看不见。
Q: 浮点数在二进制里怎么存的?
浮点数用 IEEE 754 标准存储,把 32 位或 64 位分成三部分:符号位(1 位)、指数位(8 位或 11 位)和尾数位。0.1 + 0.2 != 0.3 这个经典问题,根源就是 0.1 和 0.2 在二进制浮点表示下是无限循环小数,存储时被截断了。这不是语言的 bug,是二进制浮点的固有属性。