--- title: "05-字符编码:从 ASCII 到 UTF-8" aliases: - 字符编码 created: 2026-08-29 tags: - 基础与理论 - 信息的表示与处理 --- # 字符编码:从 ASCII 到 UTF-8 > 总篇 [[00-信息的表示与处理]] 里 `char` 只是类型表里不起眼的一行。这篇负责把"字"补齐:数讲透了(补码管整数、IEEE 754 管小数),可文字怎么变成 01?从 ASCII 一路打到 Unicode,顺手破掉"烫烫烫""锟斤拷"这些经典灵异事件。 ## 一、问题的起点:计算机只认识数字 内存里只有 0 和 1,想存"你好"两个字,必须先约定一件事:**每个字对应哪个数字**。这套约定其实是两层: | 层 | 回答的问题 | 例子 | | --- | --- | --- | | **字符集** | 这个字是几号? | "中" → 编号 U+4E2D | | **字符编码** | 这个号怎么存成字节? | U+4E2D → `E4 B8 AD` 三个字节 | 打个比方:字符集是给全世界每个字**发身份证号**,字符编码是规定**身份证号用几位数字、怎么写在卡片上**。两件事独立,混着叫就容易出乱码。 ## 二、ASCII:英语世界的一亩三分地 1967 年定稿。英语就 26 个字母加些符号,**7 位**(128 个字符)足够: | 字符 | 十进制 | 十六进制 | 二进制 | | --- | --- | --- | --- | | `'0'` | 48 | 0x30 | 0011 0000 | | `':'` | 58 | 0x3A | 0011 1010 | | `'A'` | 65 | 0x41 | 0100 0001 | | `'a'` | 97 | 0x61 | 0110 0001 | | 空格 | 32 | 0x20 | 0010 0000 | > 🤔 **注意 `'0'` ≠ 数字 0** —— 字符 `'0'` 存的是 **48**!所以 [[03-位运算应用|位运算]] 那篇里 `c - '0'`(减 48)和 `c & 0x0F`(取低 4 位)能把字符转成真数字。 **彩蛋:大小写只差一位。** 对比 'A' 和 'a' 的二进制——只有第 5 位(0x20)不同: ``` 'A' = 0100 0001 'a' = 0110 0001 ↑ ↑ 0 1 ← 0x20 就是"大小写开关" ``` `c | 0x20` 转小写、`c & 0xDF` 转大写(仅对字母成立)。位运算表里那条"或=置位"的实战应用,就藏在这。 字节是 8 位的,ASCII 只用 7 位——**最高位闲置**。后来各家拿这第 8 位扩展出自家符号(Latin-1 等),同一套 128~255 各家解释各的,混乱的种子此时已埋下。 ## 三、天下大乱:各国自己造表 中文常用字好几千,128 个码位塞不下,于是各汉字文化圈自造**双字节**字符集: | 字符集 | 年代 | 收录 | 特点 | | --- | --- | --- | --- | | GB2312 | 1980 | 6763 汉字 + 682 符号 | 国标第一代;两字节,最高位都为 1 | | GBK | 1995 | 21003 汉字 | 向下兼容 GB2312,Windows 中文版实推 | | GB18030 | 2000 起 | 覆盖全部 Unicode 码位 | 国家强制标准,1/2/4 字节变长 | | Big5(大五码) | 1984 | 繁体 13000+ 字 | 中国台湾地区常用 | 设计上有巧思:**首字节 ≥ 0x81,不碰 ASCII 区间**——纯英文文件在任何中文编码下一个字节都不用改。看个例子: ``` '中' 的 GBK 编码 = D6 D0 1101 0110 1101 0000 ↑ 首字节最高位是 1 → 这不是 ASCII,是双字节字符的开头 ``` 但要命的问题来了:**不同国家的表里,同一个码位对应不同的字**。同一串字节,用 A 表编码、用 B 表解码,出来就是另一副面孔——这就是乱码的根源。1990 年代,中日韩网页互访基本靠猜编码。 ## 四、Unicode:给全世界每个字发身份证 1991 年起,Unicode 联盟干的是"书同文"的大事:**给每个字符一个全球唯一的编号(码点)**,写作 `U+xxxx`: | 字符 | 码点 | | --- | --- | | 'A' | U+0041 | | '中' | U+4E2D | | '🀄' | U+1F004 | | '😀' | U+1F600 | 码点空间 U+0000 ~ U+10FFFF,共 17 个平面 × 65536 ≈ **111 万个位置**。常用字都住在第一个平面(BMP,U+0000~U+FFFF);emoji 之类新面孔在辅助平面。 > ⚠️ **关键认知:Unicode 只发编号,不管编号怎么存。** U+4E2D 存成几个字节?UTF-8、UTF-16、UTF-32 给出三种不同答案。所以说"Unicode 编码"不严谨——Unicode 是字符集,UTF-8 才是编码方式。就像身份证号统一了,但卡片上怎么写还没规定。 ## 五、UTF-8:变长设计的教科书 UTF-8(1993,Ken Thompson 参与)的思路:**常用的短存,生僻的长存**。靠字节开头的"前缀"自报家门: | 码点范围 | 字节数 | 二进制模板 | | --- | --- | --- | | U+0000 ~ U+007F | 1 | `0xxxxxxx` | | U+0080 ~ U+07FF | 2 | `110xxxxx 10xxxxxx` | | U+0800 ~ U+FFFF | 3 | `1110xxxx 10xxxxxx 10xxxxxx` | | U+10000 ~ U+10FFFF | 4 | `11110xxx 10xxxxxx 10xxxxxx 10xxxxxx` | 首字节开头有几个 1,总共就是几个字节;后续字节一律 `10` 开头。完整推导一遍 '中': ``` U+4E2D = 0100 1110 0010 1101 (十六进制逐位展开,16 位) 落在 3 字节区 → 模板 1110xxxx 10xxxxxx 10xxxxxx,共 16 个空位 从高位到低位填入:0100 | 111000 | 101101 1110 0100 10 111000 10 101101 = E4 B8 AD ``` 对照几个实例: | 字符 | 码点 | UTF-8 字节 | 字节数 | | --- | --- | --- | --- | | 'A' | U+0041 | `41` | 1 | | '中' | U+4E2D | `E4 B8 AD` | 3 | | '😀' | U+1F600 | `F0 9F 98 80` | 4 | 'A' 编码后还是 `0x41`,和 ASCII 一字不差——这就是 UTF-8 的第一杀器。 **UTF-8 为什么能一统天下?三个金性质:** 1. **ASCII 完全兼容**——历史上所有英文文本一字节不用改,天然合法。存量数据零迁移成本,这是它赢的根本原因(和浮点数 exp 用无符号存是同一种智慧:让位模式保持某种"有序/兼容"的好性质)。 2. **自同步**——任取一个字节,看开头就知道它是首字节还是中间字节。传输丢包、文件从中间截断,都能重新对齐;且 `0x00` 只出现在 U+0000 的编码里,C 字符串的 `\0` 结尾永远不会被误伤。 3. **字节序无关**——字节顺序固定,大端小端机器存出来一模一样(UTF-16/32 就得靠 BOM 操心字节序,见 2.4 大端小端)。 ## 六、UTF-16 与 UTF-32:一句话带过 | 编码 | 思路 | 代价 / 用处 | | --- | --- | --- | | UTF-32 | 每码点固定 4 字节 | 简单粗暴、随机访问 O(1),但纯英文也膨胀 4 倍,少见 | | UTF-16 | BMP 用 2 字节,辅助平面用代理对 4 字节 | **Java 和 JavaScript 字符串的内部编码** | emoji '😀' = U+1F600,超出 BMP(U+FFFF),UTF-16 得拆成两个"代理字符"存(`D83D DE00`)——**这就是 Java 里 `"😀".length() == 2` 的真相**:不是数错了,是 UTF-16 里它真的占两个 char。 ## 七、乱码考古:三大灵异事件破案 先立总纲:**乱码的本质 = 字节没变,解释错了。** 下面三桩悬案,全是这句话的变体。 **案一:烫烫烫烫烫……** MSVC(Visual Studio)Debug 模式会把**栈上未初始化的内存**填成 `0xCC`(顺带一提,0xCC 正是 x86 的 int3 断点指令——程序一旦跳进去,调试器立刻抓个正着,等于"越界即报警"): ``` 0xCC 0xCC 按 GBK 解 → 烫 ``` 满屏烫烫烫 = 你读了没初始化的栈变量。 **案二:屯屯屯屯屯……** 同一套思路,Debug 模式给**堆内存**填的是 `0xCD`: ``` 0xCD 0xCD 按 GBK 解 → 屯 ``` 屯屯屯 = 你读了没初始化的堆内存。烫在栈上、屯在堆上,两位"质检员"分工明确。 **案三:锟斤拷锟斤拷……(最连环)** ``` ① 某段字节流解码失败 → 解码器把坏字符替换成 U+FFFD( replacement character � ) ② 程序把它存回 UTF-8:U+FFFD = EF BF BD,连续多个就是 EF BF BD EF BF BD EF BF BD ... ③ 这串 UTF-8 字节又被拿去按 GBK 显示(两字节一字): EF BF → 锟 BD EF → 斤 BF BD → 拷 ④ 三字节序列对两字节切分永远错开 → "锟斤拷"无限循环 ``` 一桩案子牵出**两次解码事故**:先错解出 U+FFFD,再错按 GBK 显示。附赠一个常见变体——UTF-8 的"你好"(`E4 BD A0 E5 A5 BD`)被按 GBK 显示,就变成"**浣犲ソ**"。 > 💡 破案口诀:看到乱码别猜,先问两句话——**这串字节原本是什么编码?现在被当成了什么编码?** 用十六进制工具看字节,一切灵异事件都有标准答案。 ## 八、实战避坑清单 | 场景 | 坑 | 正确姿势 | | --- | --- | --- | | MySQL | 字符集 `utf8` 实际是 **utf8mb3**(最多 3 字节),存 emoji 直接报错/截断 | 用 **utf8mb4**(MySQL 8.0 起已是默认) | | Java | `char` 只有 2 字节,装不下 emoji;按 `charAt` 数长度会数错 | 用码点 API(`codePointAt`/`codePoints`)处理"字符" | | Python | Python2 的 str/unicode 双轨制是历史天坑 | 用 Python3;`str` 就是码点序列,读写文件显式 `encoding="utf-8"` | | 网页 | 少了 ``,浏览器按默认编码猜 → 乱码 | HTML 里显式声明 + HTTP 响应头 `Content-Type` | | 文件读写 | 不指定编码,跟着系统 locale 走,换台机器就翻车 | **永远显式 encoding="utf-8"** | ## 九、盲点自测 1. **字符 `'0'` 和数字 0 的区别?** —— `'0'` 存的是 48;`c - '0'` 才是真值。 2. **ASCII 用几位?为什么 char 是 8 位?** —— 7 位 128 字符;装进 8 位字节,最高位闲置,后来被各家扩展占用,埋下乱码伏笔。 3. **大小写转换的位运算?** —— 'A' 与 'a' 只差 0x20 这一位:`c|0x20` 转小写、`c&0xDF` 转大写(仅对字母成立)。 4. **Unicode 和 UTF-8 是什么关系?** —— 字符集 vs 编码方式:Unicode 发码点编号,UTF-8 规定编号怎么落成字节。一个码点可以有多种编码存储。 5. **UTF-8 为什么赢了?** —— ASCII 完全兼容(存量零成本)+ 自同步(可重对齐、`\0` 安全)+ 字节序无关。 6. **'中' 的 UTF-8 几个字节?** —— 3 字节 `E4 B8 AD`;汉字绝大多数在 3 字节区。 7. **烫/屯/锟斤拷分别是什么事故?** —— 烫=Debug 栈 0xCC(int3)按 GBK 解;屯=Debug 堆 0xCD;锟斤拷=U+FFFD 的 UTF-8 字节(EF BF BD)被按 GBK 切分。 8. **MySQL 的 utf8 够用吗?** —— 不够,它是 utf8mb3;emoji 要 utf8mb4。 9. **为什么 Java 里 `"😀".length() == 2`?** —— String 内部是 UTF-16,emoji 在辅助平面,用两个 char 的代理对(D83D DE00)表示。 ## 十、动手玩一把 ```bash # 看一个字符的真实字节(xxd 十六进制转储) echo -n 中 | xxd # e4 b8 ad —— 3 字节,和推导一致 echo -n A | xxd # 41 —— 和 ASCII 时代一模一样 echo -n 😀 | xxd # f0 9f 98 80 # 编码转换 iconv -f GBK -t UTF-8 in.txt > out.txt # GBK 转 UTF-8 ``` ```python # Python 亲手复现三桩乱码案 "中".encode("utf-8") # b'\xe4\xb8\xad' "中".encode("gbk") # b'\xd6\xd0' "你好".encode("utf-8").decode("gbk") # '浣犲ソ' ← 字节没变,解释错了 b"\xcc\xcc".decode("gbk") # '烫' "\ufffd\ufffd".encode("utf-8").decode("gbk") # '锟斤拷' ``` ## 参考资料 - 阮一峰《字符编码笔记:ASCII,Unicode 和 UTF-8》(2007):`https://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html` —— 十多年前的文章,至今仍是中文圈最好的入门 - 《深入理解计算机系统》(CSAPP) 第 2 章 —— 本分区总篇的原著 --- 至此,"数"(补码、IEEE 754)和"字"(字符编码)都闭环了——**信息的表示与处理**系列真正完结。下一站进操作系统。 ⬅️ [[04-浮点数|浮点数]] 🏠 [[00-信息的表示与处理]] 🏁 完结,回总篇收网