Unicode Standard and Encoding

0x0. 前记

人老了脑子也没办法记那么多了,有时候还傻傻分不清 Unicode 和 UTF-8、GBK 之间的关系,这次有机会重新整理,复习一下记下来。

0x1. What'‘s Unicode

引用 Wikipedia 原句:

Unicode is a computing industry standard for the consistent encoding, representation, and handling of text expressed in most of the world'‘s writing systems.

The standard consists of a set of code charts for visual reference, an encoding method and set of standard character encodings, a set of reference data files, and a number of related items.

所以 Unicode 不单单是给字符统一、规范、特定的编码,是一套标准。其包含字符和唯一(unique)数值之间的对应转换、编码方式以及相关一些项目。就好比桶装方便面不止包含面条,还有调味料和叉子。

0x2. How Unicode works

Unicode 标准中为每个字符分配一个 unique code point,即字符和数值是一一对应关系,例如字符 é 对应数值 233(0xE9)。code point 范围为 0hex ~ 10FFFFhex,总共可容纳 1114112 字符,如此大的空间自然没有全部分配完。

通常,一个 Unicode 标准字符可以用 “U+” 跟随一个十六进制数来表示,如 U+0061 代表小写字母 a。

嗯,似乎到现在为止能很好理解 Unicode 标准,那么看看这图:

Unicode Planes

表格来自 Wikipedia: https://en.wikipedia.org/wiki/Unicode

如表中所示,Unicode 标准中将特定的 code point 分配到不同的 plane 中。如 plane0 中只需要两个字节即可表示一个 Unicode 字符,Unicode 标准将第一个平面命名为 Basic Multilingual Plane(BMP)。

0x3. Why Unicode

传统 ASCII 字符集已经满足不了国际化的需求,所以 Unicode 的存在自然不言而喻。但是扯了这么久,还没讲到 UTF-8 和 GBK 与 Unicode 之前的关系?

我想各位可能都有一个疑惑(之前的我,所以记下这篇文章):既然有了 Unicode 标准来规定字符对应的数值,那么只要存储对应的数值,读取时即可转换并渲染为特定的字符,为啥还需要 UTF-8、GBK 等编码方法?

年轻人有些想法总是好的,正好我手头有一个字符串:Hello 世界,那么通过 Unicode 标准直接存放到内存中。先查一下表,对应的 Unicode 数值应该为(忽略字母和中文之间的空格):

手头的字符串

因为字母的数值范围在 255 之内,所以用一个字节存储即可,字符串使用 \0 结尾,内存里大概是这样(为了方便叙述使用了 Big Endian 方式存储):

内存概况

接下来使用 C 代码尝试把字符从内存读取出来:

int main()
{
    // 假设使用上述方式存储字符串
    char* lpStr = "Hello世界";
    
    while (lpStr) {
        printf("%c", lpStr++);
    }
    
    return 0;
}

到这里问题出现了:每次只读取一个字节进行显示,对于字母来说是正常的。但是对于中文 U+4E16(世)这两个字节来说明显太吝啬了,反之每次读取两个字符,对于字母和大于两个字节的字符也是一个问题。

解决字符如何存储、读取的方法,就是使用字符编码。

0x4. 抛砖引玉,UTF-8 编码方式

UTF 全称为 Unicode Transformation Formats,UTF-8 是一种可变长度编码,先看看 UTF-8 的编码规则表:

UTF-8 Encode

乍看之后一头雾水,事实上比较容易理解,UTF-8 的三种编码方式:0xxxxxxx,11xxxxxx,10xxxxxx。

  • 0 开头表示为单个字节,所以 UTF-8 可以兼容 ASCII 标准中的编码方法。
  • 11x 开头表示为多字节字符,其中 1 的总位数为字符的总字节数。
  • 10 开头则代表该字节为上个字节的连续字节。

多说无益,拿 Wikipedia 上的例子来讲,使用 UTF-8 对欧元字符 € 进行编码存储:

  1. 字符 € 对应的 Unicode 数值为 U+20AC
  2. 查阅上表可知该范围坐落在 U+0800 和 U+FFFF 之间,为双字节,使用三个字节对其进行编码(因为需要同步位和标志位,所以两个字节自然不能使用两个字节进行编码)
  3. U+20AC 对应的二进制数值为 100000 10101100,因为是对双字节进行编码,但是位数不足 16 位,所以在高位补 0,其结果为:00100000 10101100
  4. 使用 1110xxxx 对第一个字节进行编码:11100010
  5. 接着是第二个字节,10xxxxxx 代表该字节与上个字节连续,编码为:10000010,同理第三个字节则为剩余位数:10101100
  6. 所以最终以 11100010 10000010 10101100 的方式呈现,用十六进制方式表示:E2 82 AC

那么问题解决了,程序只需要判断标志位即可正确地对字符进行转换、显示,无论是单字节还是多字节字符。而对于无效编码格式的字符,UTF-8 使用 “�” (U+FFFD) 字符进行代替。