字符编码

1. 初代编码

1.1 ASCII

这是最早也是最著名的编码。由于计算机诞生于美国,所以最早的编码主要考虑的是英语环境。

ASCII 总共可以表示 128 个字符。ASCII 只需要 7 位,但在计算机中,数据存储的基本单位是字节 (Byte),1 字节 = 8 比特。所以,ASCII 字符通常存储在一个字节中,最高位(第 8 位)固定为 0

例如:字符 ‘A’ 的 ASCII 码是 65,二进制表示为 01000001

  • 0-31:控制字符(如换行、回车、制表符等,不可打印)。
  • 32-127:可打印字符,包括了大小写英文字母 (a-z, A-Z)、数字 (0-9) 和各种标点符号。

1.2 ANSI 编码(不是 ASCII)

你可能经常听到 ANSI 编码。这是一个极易混淆的概念!

  • 误解:ANSI 是某一种特定的编码。
  • 真相:ANSI 根本不是一种具体的编码,它是一个编码体系的总称。在不同的操作系统和语言环境下,”ANSI” 指代的具体编码是不同的。
    • 在简体中文 Windows 系统中,ANSI 编码默认指的是 GBK。
    • 在日文 Windows 系统中,ANSI 编码默认指的是 Shift_JIS。
    • 在西欧语言 Windows 系统中,ANSI 编码默认指的是 Windows-1252。

1.3 混乱时代

世界不是只有英语。ASCII 无法表示像 éä 这样的欧洲字符,更不用说汉字、日文、韩文等。这个局限性直接导致了“编码大混战”时代的到来。
为了解决 ASCII 的局限,各个国家和地区开始扩展 ASCII,设计自己的编码方案。

大家发现 ASCII 只用了 8 位中的 7 位,第 8 位总是 0。于是,一个聪明的想法诞生了:

  • 当一个字节的最高位是 0 时,就按照原来的 ASCII 来解析。
  • 当一个字节的最高位是 1 时,就表示这是一个扩展字符,后面的 7 位(2^7 = 128)可以用来表示新的 128 个字符。
    这样,一个字节就可以表示 128 + 128 = 256 个字符了。

2. 中文编码

汉字数量庞大,一个字节(最多 256 个字符)远远不够。于是中文编码采用了双字节编码方案。

  • 如果一个字节的最高位是 0,那么它就是一个单字节的 ASCII 字符。
  • 如果一个字节的最高位是 1,那么它就是“汉字”的第一个字节,它需要和紧随其后的另一个最高位也是 1 的字节一起来表示一个汉字。
  • 1个汉字 = 2个字节

2.1 版本历史

  • GB2312 (1980 年):
    • 最早的简体中文编码国标。
    • 收录了 6763 个汉字和一些符号。
    • 基本满足了日常使用,但对于一些罕见字、繁体字、古文字(比如某些人的名字)无能为力。
  • GBK (1995 年):
    • K 代表“扩展”(Kuozhan)。
    • 完全兼容 GB2312。也就是说,所有 GB2312 的编码在 GBK 中是完全一样的。
    • 增加了更多的汉字,包括繁体字和日韩汉字,共收录了 21003 个汉字。
    • GBK 是在简体中文 Windows 系统中最常见的编码。
  • GB18030 (2005 年):
    • 最新的国家标准。它的目标是收录所有可能的 CJK (中日韩) 字符,并兼容 Unicode。
    • 采用变长编码,可以用 1、2、4 个字节来表示字符。
    • 完全兼容 GBK。
    • 收录了超过 7 万个汉字,是目前最全面的中文编码。
  • BIG5
    • Big5 是台湾、香港和澳门地区使用的繁体中文编码。它的设计原理和 GBK 几乎完全一样,也是采用双字节方案。

小结:GBK 是 GB2312 的超集,GB18030 是 GBK 的超集。它们的核心思想都是用高位为 1 的双字节来表示汉字,同时兼容单字节的 ASCII。

2.2 中文编码各占几个字节

  • GB2312: 中文固定 2 个字节。英文 ASCII 一个字节。
  • GBK: 中文固定 2 字节,目前 Windows 简体中文版的默认编码 (ANSI)。英文 ASCII 一个字节。
  • GB18030: 中文固定 2 个或 4 个 字节。英文 ASCII 一个字节。
  • Big5: 中文固定 2 个字节。英文 ASCII 一个字节。繁体中文世界的标准编码。

2.3 有了 utf-8,gbk 还有意义吗

因为历史遗留问题和兼容性成本。

在简体中文版的 Windows 操作系统中,记事本等程序默认的“ANSI”编码就是 GBK。这意味着,无数不了解编码的普通用户在创建和保存文本文件时,实际上是在源源不断地产生新的 GBK 编码文件。这个默认行为极大地延长了 GBK 的生命周期。

GB18030 是中国自己的一套 Unicode 实现方式。这意味着,一个原本是 GBK 编码的文件,它本身就是一个合法的、无需任何修改的 GB18030 文件。这使得从 GBK 到 GB18030 的升级几乎是“无痛”的,极大地降低了迁移成本。****

3. 统一编码

为了解决全球性的混乱,Unicode 应运而生。

3.1 Unicode (统一码、万国码)

  • 核心思想:Unicode 本身不是一种编码方式,而是一个字符集(Character Set)。
  • 它的目标是:为世界上每一个字符分配一个唯一的、全球通用的编号。这个编号被称为码点(Code Point)。
  • 表示方式:通常写作 U+XXXX 的形式,其中 XXXX 是十六进制的数字。
    • ‘A’ 的码点是 U+0041
    • ‘ 中 ‘ 的码点是 U+4E2D
    • ‘😂’ (笑哭表情) 的码点是 U+1F602

Unicode 就像一本收录了全球所有文字的巨型字典,每个字都有一个固定的“页码”(码点)。它只规定了“哪个字对应哪个号”,但没有规定这个号在计算机里应该如何用字节来存储。

3.2 UTF (Unicode Transformation Format)

如何把 Unicode 的码点(数字)保存为计算机中的字节序列?这就是 UTF 家族要解决的问题。UTF 就是 Unicode 字符集的实现方式(编码规则)。常见的 UTF 实现有:UTF-8, UTF-16, UTF-32。

这是另一个极度容易混淆的点:Unicode vs UTF-8!

  • Unicode 是一个标准、一个字符集、一本字典。它定义了 '中' 的编号是 U+4E2D
  • UTF-8 是一种编码规则、一种存储格式、一种查字典的方法。它规定了如何把 U+4E2D 这个编号变成二进制字节序列 E4 B8 AD 存到硬盘上。

UTF-32 (定长编码):

  • 原理:最简单粗暴,每个字符都使用 4 个字节来存储。
  • 优点:转换简单,查找快。
  • 缺点:极度浪费空间。一个英文字母 ‘A’(Unicode 码点 U+0041),本来 1 个字节就够了,现在却要用 00 00 00 41 四个字节来存,空间浪费了 3/4。因此很少使用。

UTF-16 (变长编码):

  • 原理:对常用的字符(码点在 U+0000 到 U+FFFF 之间)使用 2 个字节存储。对于超出这个范围的字符(如很多 emoji 表情),则使用 4 个字节。
  • 应用:Windows 内核、Java、JavaScript 内部的字符串表示都广泛使用 UTF-16。
  • 问题:存在字节序(Byte Order)问题。一个 2 字节的数 C8 34,是先存 C8 再存 34(大端序 Big-Endian),还是先存 34 再存 C8(小端序 Little-Endian)?为了解决这个问题,引入了 BOM (Byte Order Mark)。文件开头的 FE FF 或 FF FE 用来表明字节序。

3.3 UTF-8 (变长编码,当今互联网标准)

兼容 ASCII,对英文世界非常友好。没有字节序问题,BOM 是可选的(通常不加)。空间效率高,尤其在东西方文字混合的文本中。已成为互联网上最主流的编码方式。

占用字节:UTF-8 根据字符的 Unicode 码点(Code Point)范围来决定使用多少个字节。

  • U+0000 到 U+007F (标准 ASCII):1 个字节
  • U+0080 到 U+07FF (拉丁文、希腊文等):2 个字节
  • U+0800 到 U+FFFF (大多数常用字符,包括绝大部分中日韩汉字):3 个字节
  • U+10000 到 U+1FFFFF (不常用字符、Emoji 等):4 个字节

UTF-8 原理

原理(精髓所在):这是一种极其聪明的变长编码,它根据字符的 Unicode 码点大小,使用 1 到 4 个字节来表示一个字符。

  1. 对于单字节的符号(ASCII 字符):码点在 U+0000U+007F 之间,UTF-8 使用 1 个字节表示。编码后的字节,最高位是 0。这使得 UTF-8 完美兼容 ASCII! 一个只包含英文的 ASCII 文件,其本身就已经是一个合法的 UTF-8 文件了。
  2. 对于 n 字节的符号 (n > 1):
    • 第一个字节的前 n 位都设为 1,第 n+1 位设为 0
    • 后面 n-1 个字节,前两位都设为 10
    • 剩下的二进制位,全部用来填充该字符的 Unicode 码点。

我们用 “中” (U+4E2D) 来举个例子:

  1. “中”的码点是 4E2D (十六进制),转为二进制是 0100 1110 0010 1101
  2. 这个数在 U+0800 和 U+FFFF 之间,所以 UTF-8 规定使用 3 个字节 来表示。
  3. 3 字节的模板是:1110xxxx 10xxxxxx 10xxxxxx
  4. 将 “中” 的二进制 0100 1110 0010 1101 从后往前依次填入 x 中:
    1110**0100** 10**111000** 10**101101**3 个字节,每个字节 8 个数字
  5. 将这三个字节转为十六进制,就是: E4 B8 AD
  6. 所以,“中”字的 UTF-8 编码就是 3 个字节 E4 B8 AD

UTF-8 最多几个字节

当前标准:最多 4 字节,最初设计理论上最多 6 字节,已经被废弃。

现代的UTF-8解码器会将超过4字节的序列或者超长编码视为非法,从而提高了系统的安全性。

3.4 乱码问题

乱码的本质是:解码器(如文本编辑器、浏览器)用了错误的“密码本”(编码)去解读一堆字节数据。
理解了原理,解决乱码就变得简单了:“用什么编码保存,就用什么编码打开”。
终极建议:在任何时候,如果可以自己选择,请无脑使用 UTF-8。它能最大程度地避免乱码问题,是通向世界的通行证。

4. 思考问题

4.1 为什么 utf-8 没有 order 问题

字节序问题只在处理大于 1 个字节的数据类型时才会出现。一个整数有两个字节:12 (高位字节) 和 34 (低位字节) 就会遇到大端和小端问题。

而 UTF-8 每个字节的开头几位都有特殊含义,告诉解码器这个字节的角色是什么,以及整个字符序列有多长。

  • 0xxxxxxx: 如果一个字节以 0 开头,它就是一个单字节字符 (ASCII)。处理完毕,读下一个。
  • 110xxxxx: 如果以 110 开头,它标志着一个 2 字节序列的开始。解码器知道必须再往后读 1 个字节。
  • 1110xxxx: 如果以 1110 开头,它标志着一个 3 字节序列的开始。解码器知道必须再往后读 2 个字节。
  • 11110xxx: 如果以 11110 开头,它标志着一个 4 字节序列的开始。解码器知道必须再往后读 3 个字节。
  • 10xxxxxx: 如果以 10 开头,它是一个后续字节 (continuation byte),它不是字符的开头。

关键点:表示一个字符的字节序列顺序是固定的,不可颠倒。字节的顺序是由 UTF-8 的编码规则本身严格定义的,所以不存在“大端”还是“小端”的选择。

关于 UTF-8 的 BOM

你可能听说过“带 BOM 的 UTF-8”。这是一个需要澄清的常见误解。
确实存在一个 UTF-8 的 BOM,它的字节序列是 EF BB BF。但是,这个 BOM 不用于指示字节序! 因为 UTF-8 根本没有字节序问题。
它的唯一作用是,作为一个“签名”或“魔法数”,明确地告诉编辑器或程序:“这个文件是 UTF-8 编码的”。这在一些环境下(特别是 Windows)很有用,可以帮助程序区分一个文件是 UTF-8 还是本地的 ANSI 编码(比如 GBK)。
在很多其他环境(如 Linux/Unix 世界),UTF-8 的 BOM 反而被认为是不好的实践。

4.2 数据库字符集的 utf8-bin 又是什么

我们需要把 utf8-bin 拆成两部分来看:

  1. utf8: 这是字符集 (Character Set)。
  2. bin: 这是校对规则 (Collation)。

utf8mb4

重要陷阱:在 MySQL 中,utf8 是一个历史遗留的别名,它实际上指的是 utf8mb3,即每个字符最多使用 3 个字节来存储。这意味着它无法存储像 Emoji 表情或者某些生僻汉字这样需要 4 个字节的 Unicode 字符。
现代推荐:如今在 MySQL 中,推荐使用 utf8mb4 字符集,它才是完整实现了 UTF-8 标准,能支持所有 Unicode 字符(最多 4 个字节)。

使用 utf8mb4 而不是 utf8,以支持包括 Emoji 在内的所有字符。

校对规则 (Collation)

校对规则定义了如何比较和排序字符集中的字符。它回答了这样的问题:
- 'a' 和 'A' 是否相等?(大小写是否敏感)
- 'é' 和 'e' 是否相等?(重音符号是否敏感)
- 排序时,'a' 和 'B' 谁应该排在前面?

各种校对规则:

  • utf8-bin 直接比较每个字符的二进制字节值 (byte value)。
  • utf8_general_ci 是一套通用的、不区分大小写的比较规则。

4.3 经典的烫烫烫是什么问题

在 C/C++ 等语言中,当你声明一个局部变量(尤其是在栈上分配的数组),但没有给它赋初始值时,这块内存里存储的是什么呢?答案是:不确定。它可能是上一个函数调用留下的“垃圾数据”。
Microsoft 的 Visual C++/Visual Studio 编译器在调试模式 (Debug Mode) 下,会特意用一些固定的“魔法数字”去填充这些未初始化的内存区域。

这些数字不是随机的,而是精心挑选的,以便在调试器中一眼就能认出来。

  • 0xCDCDCDCD: 这是用来填充未初始化的栈内存的。CD 是 C/C++ 中常见的魔法数字之一。
  • 0xDDDDDDDD: 这是用来填充已释放的堆内存(比如 free() 或 delete 之后的内存)的。
  • 0xCCCCCCCC: 这也是一个经典的填充值,常用于 C++ 中未初始化的栈变量。

在那个年代(以及现在很多中文 Windows 系统),默认的控制台/命令行编码是GBK或GB2312。
巧合就发生在这里:

  • 两个字节的 CD CD 按照 GBK 编码进行解码,恰好对应汉字——“烫”。
  • 两个字节的 CC CC 按照 GBK 编码进行解码,恰好对应汉字——“屯”。

5. 参考资料