Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

2.3 Unicode与UTF编码

Unicode(统一码、万国码)是为了解决传统编码的不兼容问题而产生的,它的目标是包含全世界所有语言的字符,为每个字符分配唯一的编码,从根本上解决乱码问题。

Unicode的设计思想

Unicode的核心思想很简单:

给全世界所有的字符分配一个唯一的数字编号(称为码位,Code Point),不管是什么平台、什么语言、什么程序。

Unicode的范围

Unicode使用0到0x10FFFF的码位空间,总共可以表示1,114,112个字符,目前已经分配了超过14万个字符,包含了几乎所有人类语言的字符、符号、表情符号等。

Unicode的码位通常表示为 U+xxxx 的形式,其中xxxx是十六进制的数值:

  • U+0041 表示字符 ‘A’
  • U+4E2D 表示汉字 ‘中’
  • U+1F600 表示笑脸表情 ‘😀’

Unicode平面

Unicode的码位空间分为17个平面(Plane),每个平面包含65536个字符:

  • 第0平面(基本多语言平面,BMP):U+0000 ~ U+FFFF,包含了最常用的字符,几乎所有常用汉字都在这个平面
  • 第1-16平面(辅助平面):U+10000 ~ U+10FFFF,包含了生僻字、古代文字、表情符号等不常用的字符

UTF编码系列

Unicode只是定义了字符的码位,但是如何将这些码位转换为实际的二进制字节序列进行存储和传输,这就是UTF(Unicode Transformation Format)编码要解决的问题。

常用的UTF编码有三种:UTF-8、UTF-16、UTF-32,各有优缺点。

UTF-32编码

UTF-32是最简单的编码方式:

  • 每个字符固定用4个字节(32位)表示
  • 直接将码位的数值转换为4字节的二进制
  • 是定长编码,处理非常简单

优点

  • 定长编码,字符串处理非常方便,计算长度、随机访问都是O(1)的复杂度

缺点

  • 空间浪费太大,对于英文文本,UTF-32的体积是ASCII的4倍
  • 兼容性差,很多系统和软件都不支持
  • 实际使用很少

UTF-16编码

UTF-16使用变长编码:

  • BMP平面的字符(U+0000 ~ U+FFFF)用2个字节表示
  • 辅助平面的字符(U+10000 ~ U+10FFFF)用4个字节表示(称为代理对)

代理对原理: 辅助平面的码位减去0x10000,得到20位的数值,分为高低各10位:

  • 高10位加上0xD800,得到高代理(范围0xD800~0xDBFF)
  • 低10位加上0xDC00,得到低代理(范围0xDC00~0xDFFF)

这样两个2字节的数值组成一个4字节的代理对,表示一个辅助平面的字符。

优点

  • 空间效率比UTF-32高,大部分常用字符只需要2字节
  • 定长(对于BMP平面),处理比较方便
  • Java、JavaScript、C#等语言内部的字符串表示就是UTF-16

缺点

  • 仍然有空间浪费,英文文本是ASCII的2倍
  • 存在字节序问题,需要BOM标记
  • 兼容ASCII但不兼容扩展ASCII

UTF-8编码

UTF-8是目前使用最广泛的Unicode编码,也是互联网的事实标准编码。它是一种变长编码,使用1-4个字节表示不同的字符:

UTF-8编码规则

码位范围字节数编码格式
U+0000 ~ U+007F10xxxxxxx
U+0080 ~ U+07FF2110xxxxx 10xxxxxx
U+0800 ~ U+FFFF31110xxxx 10xxxxxx 10xxxxxx
U+10000 ~ U+10FFFF411110xxx 10xxxxxx 10xxxxxx 10xxxxxx

编码规则说明

  • 单字节字符:最高位为0,低7位就是ASCII码,完全兼容ASCII
  • 多字节字符:第一个字节的前n位都是1,第n+1位是0,表示这个字符占n个字节;后续的每个字节都是以10开头
  • 这种设计让UTF-8具有自同步性,从任意字节开始都能快速定位到字符的边界

示例:

  • ‘A’(U+0041):单字节 → 01000001(和ASCII完全一样)
  • ‘中’(U+4E2D):三字节 → 11100100 10111000 10101101
  • ‘😀’(U+1F600):四字节 → 11110000 10011111 10011000 10000000

UTF-8的优点

  1. 兼容ASCII:英文文本用UTF-8编码和ASCII完全一样,现有ASCII文本不需要修改就能用UTF-8打开
  2. 空间效率高:对于中文来说,UTF-8每个字符占3字节,和GBK差不多;对于英文来说只占1字节,比UTF-16省一半空间
  3. 无字节序问题:UTF-8不需要考虑字节序,不需要BOM标记
  4. 自同步性:编码设计合理,即使部分字节损坏,也不会影响后续的字符解码
  5. 广泛支持:几乎所有现代系统、编程语言、软件都原生支持UTF-8

UTF-8的缺点

  • 变长编码,字符串处理复杂度高,计算长度、随机访问都是O(n)的复杂度
  • 中文占3字节,比GBK的2字节略大

BOM(字节顺序标记)

BOM(Byte Order Mark)是一种特殊的标记,放在文本文件的开头,用来表示文本的编码和字节序。

常见BOM标记

编码BOM标记
UTF-8EF BB BF
UTF-16 LE(小端)FF FE
UTF-16 BE(大端)FE FF
UTF-32 LE(小端)FF FE 00 00
UTF-32 BE(大端)00 00 FE FF

BOM的问题

  1. UTF-8的BOM是非标准的:UTF-8本身没有字节序问题,不需要BOM,很多系统和软件不支持带BOM的UTF-8文件
  2. 兼容性问题:很多程序处理带BOM的文件时会出现问题,比如Shell脚本、JSON文件、代码文件等,如果带BOM会导致执行错误
  3. 隐藏字符:BOM是不可见字符,很多编辑器不会显示,出现问题时很难排查

最佳实践

  • UTF-8文件不要加BOM
  • UTF-16和UTF-32如果跨平台使用,建议加BOM,否则要明确指定字节序
  • Windows系统的记事本默认会给UTF-8文件加BOM,这是很多乱码问题的根源,尽量不要用记事本编辑代码文件

编码选择建议

  1. 存储和传输优先选择UTF-8:这是目前的事实标准,兼容性最好,空间效率高
  2. 程序内部字符串表示:根据语言特性选择,比如Java/JS用UTF-16,Go/Rust用UTF-8,Python3用Unicode字符串
  3. 中文环境下如果需要兼容旧系统:可以考虑用GBK,但优先选择UTF-8

思考问题

  1. UTF-8、UTF-16、UTF-32各有什么优缺点?分别适合什么场景?
  2. 为什么UTF-8是目前使用最广泛的编码?
  3. BOM是什么?为什么UTF-8文件不建议带BOM?
  4. 计算一下“中国ABC“这个字符串用UTF-8、UTF-16、GBK编码分别占多少字节?