我理解的Unicode、UTF-8之间的关系

Unicode和UTF-8是程序员经常遇到的词汇,基本上涉及文字处理的程序,都离不开这两个概念。可是,一会儿Unicode,一会儿又是UTF-8,它们之间到底是什么关系, 你弄明白了吗?
为了搞懂这个问题,有一些基本概念需要提前安利一下。

计算机只能直接处理数字

我们知道,计算机能直接处理的只有二进制数字,因为CPU的基本功能是进行数字的加减乘除四则运算、与或非等逻辑运算、算数和逻辑移位操作、比较数值、变更符号,以及计算主存地址等操作。所有其它数据,比如文本、图
像、音视频等,都需要先转化成数字才能被CPU进行处理。

字符

人类的语言和文字由一个个字符构成,字符包括文字(比如英文字母、汉字)、标点符号以及其他符号等。每种语言和文字都有自己的字符,全世界的字符加起来有好几百万种。

字符编码

由于计算机只能处理二进制数字,而我们人类文字却由字符组成。需要一种编码标准,为每一个字符指定一个二进制数字,来代替字符输入和存储到计算机中。

ASCII编码

ASCII编码是最初的编码标准。它极其简陋,只有128个字符编码,规定了英语26个字母字符和空格、逗号等其他一些常用字符的二进制编码。

ASCII码的局限

ASCII编码对于英语来说足够了。但是世界上语言这么多,每种语言又有几百到几千甚至几万个字符,ASCII码不足以表示这么多字符,于是各个国家都先后制定了自己的编码标准。这些标准都是在ASCII码基础上做了大规模的扩充,兼容ASCII码没问题,但是相互之间就不兼容了。因为对于不同的编码标准,同一个二进制编码可能代表了不同的字符,而相同的字符在不同编码标准中所对应的二进制编码也不一样。这就产生了大量的转换难题。乱码问题就是因为编码识别错误,或转换错误造成的。

计算机系统编码的局限

一般的计算机操作系统,只能支持两种编码混用,一种是ASCII编码,另一种是本地语言编码。计算机系统不支持多种编码的混用。比如同时使用中文GBK、中文繁体BIG5、日文Shift_JIS等。

Unicode

想象一下,如果有一种编码,能够包含地球上的所有文字符号,并指定唯一编码,那前面提到的多种编码转识别难题就迎刃而解了。Unicode就是为了解决这种各自为政的混乱局面产生的。Unicode是一种字符编码方案,包括字符集和字符编码表。它囊括了世界上的所有符号,为每种语言的每个字符都设置了一个独一无二的二进制编码。

Unicode的表示问题

由于Unicode意图囊括世界上所有字符(目前有100多万个字符),它必然需要一个很大的字符集。这个字符集的二进制整数范围很广,像ASCII那样的1个字节是容纳不了的。需要两个字节的二进制数字才能完全容纳。一旦多于一个字节,就需要考虑存储和传输问题了。相关问题有二:

  1. 给定一个字符序列,计算机如何知道这是由多个字节组成的Unicode字符,还是单个字节组成的几个ASCII字符?
  2. 一般的英文字符和数字,只需要一个字节就能表示,而Unicode却规定了至少两个字节,如果所有英文字符都按双字节存储,会造成大量的存储空间浪费。
  3. 给定一个双字节的Unicode字符,在存储和传输时,第一个字节在前,还是第二个字节在前?即Big Endian和Little Endian问题。

Unicode编码表只是规定了字符和两个字节二进制数字之间的逻辑对应关系,并没有规定这个二进制数字应该怎么存储和传输。

UTF-8

为了解决上述几个问题,UTF-8编码产生了。确切的说,UTF-8编码是Unicode的一种编码实现方式。除了UTF-8,还有UTF-16,UTF-32等。

UTF-8一个最大的特点是,它是一种变长的编码方式。它使用1-4个字节来表示一个字符。
UTF-8只有两条简单的编码规则:

  1. 对于单字节字符,字节首位为0,后7位为这个字符对应的unicode二进制数字编码。这部分其实就是ASCII码。
  2. 对于n字节(n>1)字符,第一个字节的前n位为1,第n+1位为0;后面的第2-第n个字节的前两位为10。每个字节除了刚才指定的这几个位之外,其余用字符的unicode二级制数字码依次填充。
    编码规则用图表表示如下:
Unicode范围(16进制)UTF-8编码方式
000000 - 00007F0xxxxxxx
000080 - 0007FF110xxxxx 10xxxxxx
000800 - 00FFFF1110xxxx 10xxxxxx 10xxxxxx
010000 - 10FFFF11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

举个例子:“汉”字的Unicode编码是0x6C49。0x6C49在0x0800-0xFFFF之间,使用3字节模板:1110xxxx 10xxxxxx 10xxxxxx。将0x6C49写成二进制是:0110 1100 0100 1001, 用这个比特流依次代替模板中的x,得到:11100110 10110001 10001001,用16进制表示是E6 B1 89。

由UTF-8编码反向解读出二进制数字也很简单。从第一个字节开始判断,如果第一个字节的第一位为0,则这个字节单独构成一个字符;而如果第一个字节是1,往后数连续有几个1,则表示这个字符占用了连续几个字节。

UTF-8、UTF-16、UTF-32

除了最常用的UTF-8编码实现外,Unicode字符集还可以采用UTF-16、UTF-32等编码实现方式。
下面用一个例子简答解释一下。
例如,“汉字”对应的Unicode编码是0x6c49和0x5b57,分别用三种编码表示为:

1
2
3
char8_t  data_utf8[]={0xE6,0xB1,0x89,0xE5,0xAD,0x97}; //UTF-8编码
char16_t data_utf16[]={0x6C49,0x5B57}; //UTF-16编码
char32_t data_utf32[]={0x00006C49,0x00005B57}; //UTF-32编码

字节序 Big Endian 和 Little Endian

字节序有两种,Big Endian大端序和Little Endian小端序,分别简写为BE和LE。对于一个双字节字符来说,如果第一个字节在前面就是大端序,如果第二个字节在前面就是小端序。
根据字节序的不同,UTF-16可被实现为UTF-16BE和UTF-16LE,UTF-32可被实现为UTF-32BE和UTF32-LE。举例说明:
例如,汉字的“汉”,Unicode编码为0x6c49,分别表示为:

Unicode编码UTF-16BEUTF-16LEUTF-32BEUTF-32LE
0x006c496c 4949 6c00 00 6c 4949 6c 00 00
0x020c30d8 43 dc 3030 dc 43 d800 02 0c 3030 0c 02 00

那么,计算机如何知道某个文件到底使用哪种字节序呢?
Unicode标准建议使用BOM(Byte Order Mark)来区分字节序。在传输字节流之前,先传输被作为BOM字符的“零宽无中断空格”(zero width no-break space)字符,用一个未定义的编号FEFF表示。正好是两个字节。
各种UTF编码的BOM如下:

UTF编码Byte Order Mark
UTF-8 without BOM
UTF-8 with BOMEF BB BF
UTF-16LEFF FE
UTF-16BEFE FF
UTF-32LEFF FE 00 00
UTF-32BE00 00 FE FF

根据BOM就能识别出正确的字节序,从而得到正确的编码方式了。
注意,UTF-8的编码方式,其实是规定好了字节顺序的,因此BOM不是必须的。一般不建议在UTF-8文件中加BOM。