UTF-8 标准和解码的实现
字符集编码的历史
ASCII码
ASCII 码诞生于上世纪 60 年代的美国,它将英文字符和二进制位之间的关系做了统一规定:将 128 个英文的字符映射到一个字节的后 7 位,最前面的一位统一规定为 0。因此 ASCII 码正好使用一个字节存储一个字符,又被称为原始 8 位值,由于最高位始终为 0 ,也被称为 7 位 ASCII 编码。在 ASCII 编码中,将数字 0 映射到 48,将大写字母 A 映射到 65,将小写字母 a 映射到 97 等等。
ASCII 编码简单好用,只占用一个字节,但它只能表示 128 个字符。
非 ASCII 编码
有一些编码会允许使用一个字节的所有 bit 位都用来表示字符,这样一个字节最多就能表示 256 个字符了,比如 Latin-1 编码。
简体中文常见的编码方式是 GB2312,使用两个字节表示一个汉字,所以可以表示 65536(256 x 256)个常用的汉字符号。
Unicode 编码
Unicode 将世界上所有的符号都纳入其中,对世界上所有的符号都赋予一个独一无二的编码,那么,满足了在同一个文本信息中混合使用不同的语言文字的需求。2023 年 9 月 12 日发布的 Unicode 15.1.0 版本已经收录了 149,813 个字符,其中还包含了很多 emoji 符号。每个字符都被映射至一个整数编码,编码范围为 0~0x10FFFF 。注意,这里仅仅用了三个字节而已。
Unicode 编码创建时面临的几个问题:
兼容性的问题:如何才能区别 Unicode 和 ASCII ?计算机怎么知道 3 个字节表示一个符号,而不是分别表示三个符号呢?
存储效率的问题:英文字母只用一个字节表示就够了,如果 Unicode 统一规定,每个符号用 3 或 4 个字节表示,那么每个英文字母前都必然有 2 到 3 个字节是 0,这对于存储来说是极大的浪费,文本文件的大小会因此大出 2~3 倍,这是无法接受的。
Unicode 编码采用了变长存储方式解决了存储效率的问题,采用了特殊标志位解决了兼容性的问题。为此,Unicode 规定了几种储存编码的方式,这些方式被称为 Unicode 转换格式 UTF。经常听到的Unicode 为表现形式,UTF-8 为存储形式。即 UTF-8 解码之后为 Unicode ,Unicode 可以编码成 UTF-8 。同样,存储形式也可以是UTF-32,但是存储的内容解码后依然表现为Unicode。存储形式不唯一,但是内容的表现形式是唯一的。
UTF-8 编码标准
每种 Unicode 转换格式都会把一个编码存储为一到多个编码单元,如 UTF-8 的编码单元为 8 位的字节;UTF-16 的编码单元为 16 位,即 2 个字节;UTF-32 的编码单元为 32 位,即 4 个字节。这里单字节作为一个存储单元,是不存在字节的大端和小段的问题的。但是如果使用 2 字节和 4 字节作为一个存储单元,在存储时会涉及到大小端的问题。大端模式和小端模式的多字节数据在内存中的排列方式有所不同。比如0x0001 在大端模式下被存储为 \x00\x01,而在小端模式下被存储为\x01\x00,与我们的阅读顺序刚好相反 。
所以,为了简单,大家一般使用的是UTF-8 编码标准。
前面提到,Unicode 编码采用了变长存储方式解决了存储效率的问题,采用了特殊标志位解决了兼容性的问题。具体体现如下:
对于单字节符号,字节的第一位设为 0 ,后 7 位为这个符号的 Unicode 码,以兼容 7 位的 ASCII 编码。
对于使用 X 个字节存储的符号,第一个字节的前 X 位设置为 1 ,第 X+1 位设置为 0 。后面字节的前 2 位一律设置为 10 ,剩下的位置依次填充这个符号的 Unicode 码。
当前UTF-8 编码标准一共支持到了4字节的编码,原则上可以支持8字节的,不过万国文字加起来也没有那么多的。编码规则的表格如下,字母 x 表示可用于编码的位,即 Unicode 码分布的位置:
Unicode 码范围(十六进制) UTF-8 编码方式 (二进制)
0x00 ~ 0x7F 0xxxxxxx
0x80 ~ 0x7FF 110xxxxx 10xxxxxx
0x800 ~ 0xFFFF 1110xxxx 10xxxxxx 10xxxxxx
0x10000 ~ 0x10FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
总结:
如果一个字节的第一位是 0 ,则这个字节单独就是一个ASCII字符
如果第一位是 1 ,则连续有多少个 1 ,就表示当前字符占用多少个字节。所以原则上支持8个字节也就不难理解了。这些都是协议头信息,不是协议数据
后面每个字节的前两位都是10,相当于协议头,不是协议数据
第一个字节的前5位中1的个数(0前面的)表示了该变长编码的实际字节长度(1-4字节),也就是协议长度。
C++解码实现
解码规则
对于 UTF-8 编码的字符,其长度信息并不仅仅由首字节的高 5 位决定。实际上,UTF-8 编码规则中使用了多字节编码来表示较大的 Unicode 字符。
在 UTF-8 编码中,根据首字节的高位,可以确定字符的长度范围:
如果首字节的高位是 0xxxxxxx,则表示该字符是单字节字符,长度为 1。
如果首字节的高位是 110xxxxx,则表示该字符是双字节字符,长度为 2。
如果首字节的高位是 1110xxxx,则表示该字符是三字节字符,长度为 3。
如果首字节的高位是 11110xxx,则表示该字符是四字节字符,长度为 4。
其他首字节的高位模式是无效的,不符合 UTF-8 编码规则。
对于长度超过 4 的字符,UTF-8 编码规则使用了更多的字节来表示。
长度为 5 的字符采用 5 字节编码,首字节的高位是 111110xx。
长度为 6 的字符采用 6 字节编码,首字节的高位是 1111110x。
然而,需要注意的是,UTF-8 编码规范中规定了 Unicode 字符的范围,而且并不是所有的 Unicode 字符都可以用 UTF-8 编码表示。UTF-8 编码只能表示 Unicode 字符集中的一部分。
在处理 UTF-8 编码时,我们需要根据首字节的高位来确定字符的长度,并根据长度信息来解码后续的字节。对于长度超过 4 的字符,可能需要使用更多的逻辑来处理。这包括检查后续字节的格式和范围,以确保正确解码字符。
加速后的解码规则
我们可以按照上述规则一个一个条件判断去完成解析,但是这会带来较大的性能损失,因为这会让CPU中的分支预测器疲于奔命,每次预测错误都会带来一定的性能损失。所以, Christopher Wellons 想出来了一种a branchless decoder,即无分支的解码器,主要根据前5位组成数字的所有可能性进行查表,完成解码。
再看一下上面的表格,我们来梳理一下首字节的数字规律。
长度 通配符 最小值 最大值
1 0**** 00000(0) 01111(15)
2 110** 11000(24) 11011(27)
3 1110* 11100(28) 11101(29)
4 11110 11110(30) 11110(30)
ok,前5位规律如下,其他的数值都是非法的UTF-8 编码,返回长度0就行了。
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 如遇到加密压缩包,请使用WINRAR解压,如遇到无法解压的请联系管理员!
7. 本站有不少源码未能详细测试(解密),不能分辨部分源码是病毒还是误报,所以没有进行任何修改,大家使用前请进行甄别!
66源码网 » UTF-8 标准和解码的实现