← 返回博客首页

字符编码演进:从 ASCII 到 Unicode 与 UTF-8

从一个『是不是两个字符占两个字节』的疑问说起

很多新手以为「一个汉字 = 两个字节」,其实这个说法只在某种旧编码下近似成立。真相是:字节数由编码方案决定,不由字符本身决定——同一个『中』字,在 GBK 里占 2 字节,在 UTF-8 里占 3 字节,在 UTF-32 里占 4 字节。要讲清这个,得从编码的演进说起。

第一次跨越:ASCII 与 7 位有限宇宙

1950–60 年代,ASCII 用 7 位定义了 128 个字符:大小写字母、数字、标点与控制符。它简洁、统一,但也只够英语——128 个名额装不下全世界的文字

'A' = 65 = 0x41 = 1000001     (1 字节,7 位有效)

第二次跨越:扩展编码与乱码的温床

各国想表达自己的文字,就各自定义『0x80 以上的字节是什么』:西欧 ISO-8859-1、中文 GBK/GB2312、日文 Shift-JIS… 麻烦在于互不兼容——同一字节 0xE4 B8 AD(UTF-8 的『中』),按 GBK 读就成了别的字。乱码的本质 = 写入编码与读取编码不一致。

第三次跨越:Unicode 与 UTF-8 各司其职

  • Unicode(字符集):给每个字符一个全局唯一码点。『中』= U+4E2D;
  • UTF-8(编码):把这些码点编码成字节。

UTF-8 变长规则

首字节 字节数 有效位数 覆盖范围
0xxxxxxx 1 7 U+0000–007F(ASCII)
110xxxxx 2 11 U+0080–07FF
1110xxxx 3 16 U+0800–FFFF(含 CJK)
11110xxx 4 21 U+10000+(emoji 等)

后续字节一律 10xxxxxx。解码器读首字节就能确定字长,且纯 ASCII 文本天然合法。

把『中』落到纸上

码点  U+4E2D = 0x4E2D
二进制 0100 1110 0010 1101  (共 16 位 → 需 3 字节)
按 1110xxxx 10xxxxxx 10xxxxxx 展开:
      1110 0100  10 111000  10 101101
      ───────── ──────── ────────
      E4        B8        AD
→ UTF-8 字节: 0xE4 0xB8 0xAD

这就是『一个汉字在 UTF-8 里是 3 字节』的来源。

UTF-16 与 HTML 实体:另两条路

  • UTF-16:基本平面字符占 2 字节,增补平面用代理对占 4 字节。Windows 与许多系统内部用它,但网络与文件更常见 UTF-8;
  • HTML 实体 中:用十六进制码点直接在 HTML 里写字符,不依赖文件编码也能正确显示,适合在受限环境里引用符号。

彻底告别乱码的清单

  • [ ] 所有文本文件统一 UTF-8(无 BOM);
  • [ ] HTTP 响应头带 Content-Type: text/html; charset=utf-8
  • [ ] 数据库建表用 utf8mb4(不是老式 utf8,utf8 装不下 emoji);
  • [ ] 前后端交互约好按 UTF-8 传输、JSON 默认 UTF-8 即可;
  • [ ] 遇到乱码时,先确认『这段字节用什么编码写的、用什么读的』,再转换,而不是乱猜。

自查

用文本转 Unicode/Unicode 转义工具,把『中』、一个 emoji、一个 ASCII 字母分别转成 U+xxxx 码点和 UTF-8 字节,逐个验证上面的变长规则。能亲手把码点展开成 UTF-8 字节、并解释『为什么纯 ASCII 是合法 UTF-8』时,你就彻底看懂编码了。

常见问题

为什么中文会乱码?乱码是怎么来的?

乱码是**编码地狱**的产物:一段字节用了一个编码写入、却被另一个编码读取。例如用 GBK 编码的『中文』俩字,用 UTF-8 解码会显示成怪字符;反过来也一样。因为同一段字节在这两套方案里对应着不同的字符表。杜绝办法:**明确并一致地标注编码**——文件/HTTP 响应头带 `charset=utf-8`,数据库建表带 `utf8mb4`,交互双方约好都按 UTF-8 读写。只要读写两侧编码一致,乱码就不会出现。

Unicode 和 UTF-8 是一回事吗?

不是。**Unicode 是字符集**:给每个字符分配一个唯一编号,叫**码点**,如『中』的 U+4E2D。**UTF-8 是编码方案**:规定怎样把码点序列编码成二进制字节。同一码点可用不同编码方案存:UTF-8(变长 1–4 字节)、UTF-16(2 或 4 字节)、UTF-32(固定 4 字节)。所以『Unicode』回答『这个字符是几号』,UTF-8 回答『这个号用哪些字节表示』,二者是规格与实现的关系。

UTF-8 为什么是变长的?它怎么知道每个字符占几个字节?

UTF-8 用**首字节前缀位**自描述长度:ASCII(U+0000–U+007F)1 字节,首字节以 `0` 开头;两个以上字节的字符,首字节按 `110`/`1110`/`11110` 前缀分别表示 2/3/4 字节,后续字节一律以 `10` 开头。解码器每读到一个首字节,就能确定『这个字符一共几个字节』。这种设计既兼容 ASCII(纯 ASCII 文本就是合法 UTF-8),又能覆盖全部码点,还方便错误检测。

数据库里用 utf8 还是 utf8mb4?

**Web 项目用 `utf8mb4`**。在 MySQL/MariaDB 里,老式 `utf8` 是 utf8mb3 的别名,**最多 3 字节**,无法存储 emoji、某些增补平面符号等需要 4 字节(U+10000 以上)的字符,存进去会报错或被截断。`utf8mb4` 才是真正的『完整 UTF-8』,能容纳所有 Unicode。所以建表、连接串、排序列一律用 `utf8mb4`(排序规则常用 `utf8mb4_unicode_ci`/`utf8mb4_0900_ai_ci`),避免 emoji 或生僻字入库即废。

← 返回博客首页