简介
每个开发者迟早都会遇到文本编码的字母汤:ASCII、Unicode、UTF-8、UTF-16、码点、代理对。把它们当作魔法咒语很容易,但理解基本原理将帮你避免 bug、安全漏洞和性能问题。本指南带你从 7 位 ASCII 时代走到现代 Unicode,解释每一层是什么、为什么重要以及如何在代码中正确使用它们。
ASCII:7 位基础
ASCII(美国信息交换标准代码)是字符编码的鼻祖。它使用 7 位表示 128 个字符:控制字符(0–31)、可打印字符(32–126)和 DEL(127)。每个程序员都应该知道的关键范围:
| 类别 | 范围(十进制) | 示例 |
|---|---|---|
| 控制字符 | 0–31 | LF(10),CR(13) |
| 空格 | 32 | ' ' |
| 数字 | 48–57 | '0'=48,'9'=57 |
| 大写字母 | 65–90 | 'A'=65,'Z'=90 |
| 小写字母 | 97–122 | 'a'=97,'z'=122 |
记忆技巧:'0'=48,'A'=65,'a'=97。小写 = 大写 + 32。
为什么 ASCII 仍然重要
- UTF-8 向后兼容 ASCII。每个 ASCII 字符串都是有效的 UTF-8 字符串。
- 许多网络协议(HTTP、SMTP)仍然在头部使用 ASCII。
- 理解 ASCII 有助于调试编码问题:如果你看到 'A' 显示为 65,你就知道编码很可能是 ASCII 或 UTF-8。
Unicode:通用字符集
ASCII 的致命缺陷:128 个字符无法表示中文、阿拉伯语、表情符号,甚至法语带音调字母。Unicode 通过为每种书写系统(过去和现在)中的每个字符分配一个唯一数字(称为码点)来解决这个问题。
- 码点用十六进制书写,前缀为
U+:U+0041表示 'A',U+4E2D表示 '中'。 - Unicode 编码空间有 1,114,112 个可能的码点(U+0000 到 U+10FFFF)。
- 目前大约分配了 150,000 个;其余为保留或私用区。
平面和 BMP
编码空间分为 17 个平面,每个平面 65,536 个码点。平面 0 是基本多文种平面(BMP),涵盖大多数现代文字(拉丁、西里尔、中日韩、阿拉伯等)。平面 1–2 包含历史文字、表情符号和罕见的中日韩字符。平面 15–16 是私用区。

编码 Unicode:UTF-8、UTF-16、UTF-32
码点只是一个抽象数字。要存储或传输它,我们需要一种编码。主要的三种:
UTF-32
- 每个码点存储为固定的 4 字节整数。
- 简单但浪费:一个 10 KB 的 ASCII 文件变成 40 KB。
- 很少用于存储;有时在内部用于处理。
UTF-16
- 每个码点使用 2 或 4 个字节。
- BMP 码点(U+0000–U+FFFF)使用 2 个字节。
- U+FFFF 以上的码点使用代理对:两个 16 位单元,范围在 U+D800–U+DFFF。
- 代理不是字符;它们是编码产物。它们绝不应出现在 UTF-8 或 UTF-32 中。
- 用于 JavaScript 字符串、Java 和 Windows API。
UTF-8
- 变长:每个码点 1–4 个字节。
- ASCII 字符(U+0000–U+007F)使用 1 个字节——与 ASCII 相同。
- 非 ASCII 字符使用具有特定前缀模式的多字节序列:
| 字节 1 | 字节 2 | 字节 3 | 字节 4 | 码点范围 |
|---|---|---|---|---|
| 0xxxxxxx | U+0000–U+007F | |||
| 110xxxxx | 10xxxxxx | U+0080–U+07FF | ||
| 1110xxxx | 10xxxxxx | 10xxxxxx | U+0800–U+FFFF | |
| 11110xxx | 10xxxxxx | 10xxxxxx | 10xxxxxx | U+10000–U+10FFFF |
关键属性:字节 0x00–0x7F 从不出现在多字节序列中,因此基于 ASCII 的字符串操作(空终止、搜索 \n 或 ,)在 UTF-8 上无需修改即可工作。
工作示例:从码点到 UTF-8 字节
让我们编码表情符号 😄(U+1F604)。
- 码点:U+1F604 = 0x1F604 = 128,516(十进制)。
- 它在 4 字节范围内(U+10000–U+10FFFF)。
- 0x1F604 的二进制:
0001 1111 0110 0000 0100(21 位)。 - UTF-8 4 字节模式:
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx。 - 用 21 位值填充
x位:11110 000(0xF0) |000 11111→11110000 1001111110 011000→1001100010 000100→10000100
- 结果:
F0 9F 98 84(十六进制)。
在我们的 文本转 Unicode 转换器 中试试,看看字节。
常见陷阱
- 混淆码点和字节:UTF-8 字符串中的“字符”可能是 1–4 个字节。永远不要假设
strlen()返回字符数。 - UTF-8 中的代理对:从不会出现;如果你看到它们,数据编码错误。
- 字节顺序标记(BOM):UTF-16 文件通常以
U+FEFF开头表示字节序。UTF-8 BOM(EF BB BF)是可选的,但可能破坏 ASCII 工具。 - 规范化:像 'é' 这样的字符可以表示为单个码点(U+00E9)或 'e' + 组合重音(U+0065 U+0301)。它们在视觉上相同但字节不同。在比较之前使用 Unicode 规范化(NFC、NFD)。
- 大小写转换依赖于区域设置:土耳其语 'i' → 'İ'(带点的大写 I),而不是 'I'。不要依赖简单的 ±32 来处理非 ASCII 字符。
何时使用哪种编码
| 编码 | 最适合 | 避免用于 |
|---|---|---|
| UTF-8 | 网页、Unix/Linux、API、存储 | 需要按码点随机访问 |
| UTF-16 | JavaScript、Java、Windows API | 需要 ASCII 兼容性或 ASCII 密集型文本的空间效率 |
| UTF-32 | 内部处理(罕见) | 存储或网络传输 |
常见问题
Unicode 和 UTF-8 有什么区别?
Unicode 是字符集——从数字到字符的映射。UTF-8 是 Unicode 的一种编码——一种将这些数字表示为字节的方式。其他编码包括 UTF-16 和 UTF-32。
为什么我的字符串长度看起来不对?
在许多语言中,len() 或 length 返回代码单元的数量(UTF-8 的字节,UTF-16 的 16 位字),而不是码点或可见字符。例如,JavaScript 中 "😄".length 返回 2,因为它是一个代理对。使用库函数来计数码点或字素簇。
什么是 BOM,我应该使用它吗?
UTF-16 文件开头的字节顺序标记(U+FEFF)告诉读取者字节是大端还是小端。对于 UTF-8,BOM(EF BB BF)是不必要的,因为 UTF-8 没有字节序问题,但某些 Windows 工具会添加它。它可能混淆 Unix 工具;除非必要,否则避免使用。
如何在代码中处理表情符号?
表情符号是 U+FFFF 以上的码点,因此在 UTF-8 中需要 4 个字节,在 UTF-16 中需要代理对。有些表情符号是序列(例如,肤色 + 基础表情符号)。使用支持字素簇的库(例如 Python 的 grapheme,JavaScript 的 Intl.Segmenter)来计数可见字符。
我可以在任何地方使用 UTF-8 吗?
几乎可以。UTF-8 是网页和 Unix/Linux 中的主导编码。Windows 历史上偏爱 UTF-16,但最新版本更全面地支持 UTF-8。对于新项目,UTF-8 通常是最安全的选择。