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个字节是容纳不了的。需要两个字节的二进制数字才能完全容纳。一旦多于一个字节,就需要考虑存储和传输问题了。相关问题有二:
给定一个字符序列,计算机如何知道这是由多个字节组成的Unicode字符,还是单个字节组成的几个ASCII字符? 一般的英文字符和数字,只需要一个字节就能表示,而Unicode却规定了至少两个字节,如果所有英文字符都按双字节存储,会造成大量的存储空间浪费。 给定一个双字节的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只有两条简单的编码规则:
对于单字节字符,字节首位为0,后7位为这个字符对应的unicode二进制数字编码。这部分其实就是ASCII码。 对于n字节(n>1)字符,第一个字节的前n位为1,第n+1位为0;后面的第2-第n个字节的前两位为10。每个字节除了刚才指定的这几个位之外,其余用字符的unicode二级制数字码依次填充。 编码规则用图表表示如下: Unicode范围(16进制) UTF-8编码方式 000000 - 00007F 0xxxxxxx 000080 - 0007FF 110xxxxx 10xxxxxx 000800 - 00FFFF 1110xxxx 10xxxxxx 10xxxxxx 010000 - 10FFFF 11110xxx 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 }; char16_t data_utf16[]={0x6C49 ,0x5B57 }; char32_t data_utf32[]={0x00006C49 ,0x00005B57 };
字节序 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-16BE UTF-16LE UTF-32BE UTF-32LE 0x006c49 6c 49 49 6c 00 00 6c 49 49 6c 00 00 0x020c30 d8 43 dc 30 30 dc 43 d8 00 02 0c 30 30 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 BOM EF BB BF UTF-16LE FF FE UTF-16BE FE FF UTF-32LE FF FE 00 00 UTF-32BE 00 00 FE FF
根据BOM就能识别出正确的字节序,从而得到正确的编码方式了。 注意,UTF-8的编码方式,其实是规定好了字节顺序的,因此BOM不是必须的。一般不建议在UTF-8文件中加BOM。