字符编码:ASCII、Unicode和UTF-8
最近工作中写了个简单的JSON parser,实现了解析字符串的部分功能。JSON标准支持9种转义,其中转义\u
用来支持Unicode字符集里的字符。目前我还没有实现这个功能。联想到经常在代码中看到奇怪的字符,还有”手持两把锟斤拷,口中疾呼烫烫烫“这个梗,尽管知道是因为编码混乱导致的,但不知道具体的原理。今天就来研究一下编码的细节,后面在JSON parser里加上Unicode的支持。
ASCII
ASCII应该是最简单的字符编码,使用7一个bit表示128个字符,其中前32个字符和最后一个字符为控制字符,不可以被显示。我们常用的字母数字、各种标点和运算符号位于0x20-0x7E。很显然,只用ASCII无法表示你现在看到的文字。
Unicode
为了表示更多的字符,各种各样编码方式被发明了出来。用Visual Studio打开一个文本文件然后另存为,可以看到几十种不同的编码。各种人类语言可能有各自的编码方式,定义了各自的符号集合和映射方式。
Unicode是其中的最通用与统一的编码方式,它定义了一个很大的集合,支持各种语言文字的编码,unicode.org可以查到具体的映射表。截至目前为止(2024.09),Unicode最新的15.1.0版本已经支持了149813个字符,这些字符被收录进统一字符集(UCS)中,每个符号对应一个整数,这个数字被称为码点(code point),码点的范围是0x0-0x10FFFF。Unicode把这个范围分成了17个平面:
- 最前面的0x0000-0xFFFF的65536个字符,码点可以被记为U+XXXX,所在的空间被称为基本平面(BMP)。BMP定义了最常见与常用的字符。例如,”卷“在这个平面里,码点为U+5377。
- 剩下的字符,范围U+010000-U+10FFFF,共16个平面(0x01-0x10),每个平面也都有65536个字符,被称为辅助平面(SMP)。
Unicode定义了符号与数字的映射关系,但是这个数字该如何存储呢?例如,计算机存储“卷”的二进制0101 0011 0111 0111b,如何将Unicode与ASCII区别呢?是理解成多个ASCII还是一个Unicode呢?此外,存储“卷”至少需要2个字节,对于SMP上的字符,可能需要更多的字节,如果用3个或4个字节存储,无疑会造成空间浪费。这就是UTF要解决的问题。
UTF-8
UTF(Uniform Transformation Format)是一种码点的存储方式,其中的UTF-8是互联网最流行的编码方式。每种UTF拥有不同大小的编码单元,如UTF-8的编码单元为8位即一个字节,UTF-16和UTF-32的编码单元分别为16位和32位。UTF-8和UTF-16将码点存储至一个或多个编码单元,因此是变长编码方式,UTF-32则是定长编码。
以下为不同范围的Unicode码点如何编码成UTF-8:
0x0000 0000 - 0x0000 007F → 0x0xxxxxxx
0x0000 0080 - 0x0000 07FF → 0x110xxxxx 10xxxxxx
0x0000 0800 - 0x0000 FFFF → 0x1110xxxx 10xxxxxx 10xxxxxx
0x0001 0000 - 0x0010 FFFF → 0x11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
其中所有的x从低到高位依次填入Unicode从低到高位的码点,如果不够则补0。如,“卷”是0x5377,二进制为0101 0011 0111 0111b,处于第三个范围内,将低6位填入UTF-8的低六位,7-12位填入UTF-8第二个字节的低六位,13-16位填入UTF-8最高字节的低四位,得到UTF-8编码为11100101 10001101 10110111b,十六进制为0xE5 8DB7。
可以看出,如果UTF-8的最高位为0,则这个UTF-8编码就只有一个字节;高位有连续几个1,则编码就占几个字节,这样计算机就能知道这个UTF-8编码有多长。ASCII最高位刚好是0,所以刚好第一个范围能容纳所有的ASCII,UTF-8完美兼容ASCII,仅需一个字节保存。
我们可以尝试在VS中新建一个文本,写入一个“卷”字,用不同的编码方式保存,然后用beyond compare查看它的十六进制,可以看到不同的编码结果:
- UTF-8:编码为0xE58DB7,长度为3个字节
- Unicode:编码为0x7753,这是因为VS默认Unicode为小端存储。VS也支持大端方式,这样保存结果即为0x5377
- GB2312:编码为0xBEED