"乱码"几乎是每个写过代码的人都遇到过的问题,根源往往就在 ASCII、Unicode、UTF-8 这三个概念被混为一谈。它们其实处在完全不同的层次上,理清关系之后,乱码从哪来、怎么避免就一目了然。
ASCII(American Standard Code for Information Interchange)是最早的字符编码标准,用 7 位二进制数(0-127)表示英文字母、数字和常用符号,外加一些控制字符(换行、回车等)。它的局限很明显:只覆盖英语世界的字符,中文、日文、俄文字母、emoji 等完全没有涵盖,这也是为什么后来需要 Unicode 这样能容纳全世界字符的标准。
Unicode 的目标是给几乎所有人类使用过的文字系统(包括汉字、各种字母、符号、emoji)都分配一个唯一的编号,叫"码点"(Code Point),写作 U+XXXX 的形式,比如"中"字的码点是 U+4E2D。
关键点在于:Unicode 只是一套"字符→编号"的映射表,它本身并不规定这些编号该怎么存储成计算机能处理的字节序列——这正是 UTF-8、UTF-16、UTF-32 这些"编码方式"要解决的问题。
UTF-8 是目前互联网上最主流的 Unicode 编码方式,它是一种变长编码,用 1 到 4 个字节表示一个码点:
| 码点范围 | 字节数 | 首字节模式 |
|---|---|---|
| U+0000 ~ U+007F | 1 字节 | 0xxxxxxx(和 ASCII 完全一致) |
| U+0080 ~ U+07FF | 2 字节 | 110xxxxx 10xxxxxx |
| U+0800 ~ U+FFFF | 3 字节 | 1110xxxx 10xxxxxx 10xxxxxx(大部分汉字在这个区间) |
| U+10000 ~ U+10FFFF | 4 字节 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx(emoji 等) |
UTF-8 完全兼容 ASCII——码点 0-127 在 UTF-8 下就是单字节,和 ASCII 编码的字节完全相同,这也是 UTF-8 能被广泛采纳的重要原因:老系统按 ASCII 处理英文内容不会出任何问题。首字节的高位模式(比如 110、1110)明确标识了这个字符总共占几个字节,解码器一眼就能判断字符边界。
除了 UTF-8,还有 UTF-16(定长与变长混合,2 或 4 字节,Windows 和 Java 内部常用)和 UTF-32(固定 4 字节,实现简单但浪费空间,用得较少)。三者编码的都是同一套 Unicode 码点,只是字节层面的表示方式不同。
"乱码"归根结底是编码和解码用的规则不一致:一段文本用 UTF-8 编码存成字节,如果打开时被误判成用 GBK(中国大陆早期常用的中文编码,和 UTF-8 完全不兼容)去解码,字节和字符的对应关系就全乱了,显示出来的自然是一堆看不懂的符号。这也是为什么处理文本时,"这段字节是用什么编码方式产生的"和"打开时按什么编码方式解码"必须严格对应——文件、网页、数据库如果没有明确声明编码(比如 HTML 里的 <meta charset="utf-8">),接收方就只能猜,猜错了就是乱码。
在线工具:ASCII编码解码 · UTF-8编码解码 · Unicode编码解码