一、前言
提起字符编码,大家脑子里肯定会联想起一连串的名词,诸如ASCll码、Unicode、UTF-8、大端小端之类。
但它们具体是什么意思,存在什么关联,很多人却搞不清楚。原因是关于字符编码的知识,我们日常接触到的本身就比较碎片化,时间一长用不到,自然记不清楚了。
所以本文旨在将字符编码的相关知识串联起来,让读者理清楚它们之前的关联所在,这样才能真正的理解字符编码背后的深意。
二、字符编码的历史
1、ASCII(American Standard Code for Information Interchange) 美国信息交换标准代码
1967年,美国人率先创造出了ASCII码,它使用一个字节对一些字母和符号进行编码,并规定一个ASCII码占用7位,第八位最高位不使用,它可以表示的范围为0x00-0x7f,一共128个字符。
后来一些欧洲国家对ASCII码做了扩展,将自己国家语言中的一些字母利用闲置的最高位编入,比如,法语中的é的编码为130(二进制10000010)。这样一来,这些欧洲国家使用的编码体系,可以表示最多256个符号。
2、ANSI(American National Standard Institite) 美国国家标准协会
对于ASCll码的扩展确实解决了一部分欧洲国家的语言编码问题,但对于大部分亚洲国家来说,文字很多,留有扩充的这128个字符简直是杯水车薪,于是乎ANSI码产生了。确切的说ANSI不是一种特定的编码类型,而是一个组织(美国国家标准协会),或者泛指一种编码标准。
每个国家可以自己制定自己文字的编码规则,符合ANSI的标准并被收录后就叫ANSI编码。
所以换句话说,ANS编码是一种统称,它对不同国家/地区的文字有不同的编码方式
比如中国的ANSI编码对应就是中文GB2312标准,日本就是JIT标准,香港,台湾对应的是BIG5标准等等。
ANSI的标准也很简单,就是ASCII码的码位(0x00-0x7f)你不能用,换句话说ASCII在任何ANSI码中都应该是相同的,其他码位随便占,一共占几个字节你自己决定。
这样一来,每个国家自己的文字都可以被正常显示了,而且还可以和英文混合显示。
虽然这样已经很好了,但ANSI码依旧有两个问题没有解决。第一个是它无法做到不同国家的语言同时显示,比如中文和日文,原因显而易见,因为对应两套不同的ANSI码。
第二个是计算机无法知道一个字符到底需要几个字节,只能从编码本身区分。这两个问题的答案,都将会在后文给出解答。
3、CodePage 代码页
代码页,最早来自IBM,后来被微软,oracle 、SAP等广泛采用。
因为ANSI编码每个国家都不一致,不兼容,可能导致冲突,所以系统在处理文字的时候,必须要告诉计算机你的ANSI是哪个国家和地区的标准。
这种国家和标准的代号(其实就是字符编码格式的代号),微软称为Codepage
代码页,其实这个代码页和字符集编码的意思是一样的。告诉你代码页,本质就是告诉了你编码格式。
4、Unicode(Universal Code) 统一码
4.1) Unicode 的定义
前面提到,ANSI的缺点是无法同时显示两种国家的语言。于是Unicode出现了,它是一张包含全世界所有文字的一个编码表。
不管你用的上,用不上,不管是现在用的,还是以前用过的,只要这个世界上存在的文字符号,统统给你一个唯一的编码,这样就不可能有任何冲突了。而且同时显示多国文字也没有问题。
Unicode编码范围是:0x000000-0x10FFFF,需要三个字节存储,可以容纳1114112个字符。Unicode 5.0版本中,才用了238605个码位。
4.2) UCS、UCS-2、UCS-4
Unicode字符集可以简写为UCS(Unicode Character Set)。有UCS- 2、UCS-4两种,UCS-2用两个字节对最世界上最常用的6万多个字符编码,UCS-4用4个字节编码,可以容纳两亿个码位,但世界上的文字远达不到这个数量。
狭义上的Unicode只是一个编码表,或者说它只是提供了一个理想的映射关系。具体要如何在计算机中实现这张巨大的编码表呢?或者换句话说,如何区分十六进制数0x516D到底对应中文的"六",还是对应两个英文字母“Qm”呢?
要知道,在ASNI码中0x00-0x7f是ASCII码的专属区域,一定不会有这样的冲突存在。所以为了解决这些问题,我们必须引入Unicode的编码方式,用于在计算机中实现Unicode字符集。
4.3) UCS 的实现方案
4.3.1 UTF-32(UCS Transformation Format 32bit)
UTF-32比较简单粗暴,基于UCS-4,用四个字节32位为一个单位来标识文字、存储数据。这样就不会产生冲突,比如’ABC’就是 00 00 00 41 00 00 00 42 00 00 00 43
但这样有个显而易见的问题,就是会有很多的空字节并没有写入信息,这会造成很多的内存浪费。因为对于绝大多数字符编码并不会超过两个字节,否则也不会有UCS-2这种东西了。基于这个问题,更加先进的UTF-16产生了。
4.3.2 UTF-16(UCS Transformation Format 16bit)
UTF-16基于UCS-2,采用两字节编码,对于U+0000-U+FFFF之间的两字节编码字符,直接表示即可。但对于三字节字符(U+10000-U+10FFFF),就无法表示了,于是UTF-16做了一些扩充,用于表示两字节以上的字符编码,具体处理如下:
- 对编码U减去0x10000,得到U’。这样U+10000~U+10FFFF的三字节编码就变成了 0x00000~0xFFFFF。 最大范围变为了只有20位
- 用20位二进制数表示U’=yyyyyyyyyyxxxxxxxxxx ,x、y各十位。
- 接着我们将前10位和后10位分为两个字节,并在高位使用特定的比特填充,得到结果用W1和W2表示,W1=110110yyyyyyyyyy,W2=110111xxxxxxxxxx,则我们发现 W1 = D800—DBFF,W2 = DC00—DFFF。
- 这样,我们就将一个三字节的Unicode编码转为了2个2字节的UTF-16编码
但是由于这种算法的存在,造成UTF-16中的 U+D800~U+DFFF 变成了无定义的字符,所以索性这个区域就被称为代理区(Surrogate),并不对应任何字符。
当然,UTF-16即使是采用了固定两字节编码的方式,但对于只占一字节的ASCII码来说,还是存在一定的内存浪费问题,所以就有了UTF-8这种彻底解决内存浪费的方法。
4.3.3 UTF-8(UCS Transformation Format 8bit)
UTF-8其实是一种MBCS方案,可变字节的。到底需要几个字节表示一个文字,我们不知道,而是需要根据这个符号的unicode编码来决定,最多4个字节。具体的编码规则如下:
UCS | 位序列 | 第一字节 | 第二字节 | 第三字节 | 第四字节 |
---|---|---|---|---|---|
U+0000 … U+007F | 00000000-0xxxxxxx | 0xxxxxxx | |||
U+0080 … U+07FF | 00000xxx-xxyyyyyy | 110xxxxx | 10yyyyyy | ||
U+0800 … U+FFFF | xxxxyyyy-yyzzzzzz | 1110xxxx | 10yyyyyy | 10zzzzzz | |
U+10000…U+1FFFFF | 00000000-000wwwxx- xxxxyyyy-yyzzzzzzz | 11110www | 10xxxxxx | 10yyyyyy | 10zzzzzz |
UTF-8的特点是对不同范围的字符使用不同长度的编码。对于0x00-0x7F之间的字符,UTF-8编码与ASCII编码完全相同,且也使用一个字节,所以彻底解决了使用UTF-32和UTF-16造成的内存浪费问题。
UTF-8编码的最大长度是4个字节。从上表可以看出,4字节模板有21个x,即可以容纳21位二进制数字。Unicode的最大码位0x10FFFF也只有21位,所以足够了。
我们以“汉”字为例着重讲述UTF-8的编码过程,“汉”字的Unicode编码是0x6C49。
0x6C49在0x0800-0xFFFF之间,使用用3字节模板 1110xxxx 10xxxxxx 10xxxxxx。
将0x6C49写成二进制是:0110 1100 0100 1001,用这个比特流依次代替模板中的x,
得到:11100110 10110001 10001001,即E6B189。
我们注意到,两字节的中文使用UTF-8编码后变为了三字节,而UTF-16则依旧是两个字节。所以中文较多的文本使用UTF-16,会更节省内存。而在英文较多的文本中,比如代码源文件,UTF-8的内存利用率则更胜一筹。
最后,我们注意到UTF-8编码每个字节的高位都有几个固定的值,相信大家也能看出一些端倪,比如第一个字节中有几个1就对应这是几字节编码,其后的0用于将高位的1与编码隔开。非第一字节以10开头用于解决我们后文要说的大小端问题。
4.4) BE(Big Endian) 大端、LE(Little Endian) 小端
我们在进行多字节信息存储和传输时,都需要考虑大小端问题。
所谓大端,就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。小端正好相反,低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
是不是觉得很绕口,我也是,所以你可以只记住下面这句话:
大端更加符合人类的阅读习惯,而小端更符合计算机的阅读习惯。
我们将计算机内存抽象为一个作业本,每一页都对应一个内存地址,从第一页到最后一页对应了内存的低地址到高地址。人类的阅读、写作习惯是从左到右,所以对于二进制数,人类也保留了相同的习惯。比如对于二进制数0x12 34 56 78来说,人们将它写在内存中,也按照了文字的写作顺序,于是从第一页(低地址)到最后一页(高地址)将二进制数从高字节到低字节写到了内存中,也就是0x12 34 56 78,这就是大端。
内存 低地址 -----------------> 高地址
0x12 | 0x34 | 0x56 | 0x78
高位子节 -----------------> 低位子节
但对于计算机来说,低地址对应低字节处理更加方便,所以计算机内部更多的是采用小端模式,即从第一页到最后一页写入的过程中将二进制序列从低字节到高字节写入。也就是0x78 56 24 12,这样就能保证在读取一个二进制序列的时候低地址总是对应低字节。
内存 低地址 -----------------> 高地址
0x78 | 0x56 | 0x34 | 0x12
低位子节 -----------------> 高位子节
最后我们可以看出UTF-8不用考虑大小端的原因是:某段比特流的首字节如果以10开头,那么只能是多字节模板的非第一字节,那么就对应小端模式。
但如果是连续的两个1,那么就是多字节模板的第一字节,对应大端模式。如果是0开头,那么说明是ASCII码,单字节编码没有大小端一说,或者说大小端都可。
4.5) Unicode 的实现方案检测
有了几种Unicode 的实现后,人们又产生疑惑,如何能知道到底采用什么编码方式?到底是大端还是小端?如果能检测就好了。
专家们也是这么想的,所以专家给每种格式和字节序规定了一些特殊的编码,用于区分编码方式和大小端,这些编码在unicode 中是没有使用的,所以不用担心会冲突,这个东西叫做BOM(Byte Order Mark)头,意思是字节序标志头。
通过它基本能确定编码格式和字节序。所以通过检测文件前面的BOM头,基本能确定编码格式和字节序。但是这个BOM头只是建议添加,不是强制的。关于Unicode的三种编码方式的BOM如下:
编码方式 | Byte Order Mark |
---|---|
UTF-8 | EF BB BF |
UTF-16LE | FF FE |
UTF-16BE | FE FF |
UTF-32LE | FF FE 00 00 |
UTF-32BE | 00 00 FE FF |
5、汉字编码字符集(GB2312、GBK、CJK)
5.1) GB2312 信息交换用汉字编码字符集
《信息交换用汉字编码字符集》是由中国国家标准总局1980年发布,1981年5月1日开始实施的一套国家标准,标准号是GB 2312—1980。
前面说,它是中国的ANSI编码,那么它显然要符合ANSI标准。中国人讲究做事留一线,于是每个字节都从0xA1开始编码,将0x7f-0xA0之前的位置空出来,并使用两字节编码,即从0xA1A1开始编码直到 0xFEFE,可以容纳两万多个字符。但实际上只收录6763个汉字,其中一级汉字3755个,二级汉字3008个;同时还收录了包括拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母在内的682个全角字符。
5.2) GBK (Chinese Internal Code Specification)汉字内码扩展规范
GBK全称《汉字内码扩展规范》(GBK即“国标”、“扩展”汉语拼音的第一个字母),由1995年12月1日制定。是在GB2312-80标准基础上的内码扩展规范,使用了双字节编码方案,其编码范围从0x8140至0xFEFE(其中剔除了0x**7F码位),共23940个码位,共收录了21003个汉字,完全兼容GB2312-80标准,支持国际标准ISO/IEC10646-1和国家标准GB13000-1中的全部中日韩汉字,并包含了BIG5编码中的所有汉字。
5.3) CJK (CJK Unified Ideographs)中日韩统一表意文字
中日韩统一表意文字(CJK Unified Ideographs),主要把分别来自中文、日文、韩文、越文、壮文中,本质、意义相同、形状一样或稍异的表意文字(主要为汉字,但也有仿汉字如日 本国字、韩国独有汉字、越南的喃字、古壮字,还包括汉字笔画、汉字偏旁)于ISO 10646及Unicode标准内赋予相同编码。
CJK 是中文(Chinese)、日文(Japanese)、韩文(Korean)三国文字的缩写。顾名思义,它能够支持这三种文字。实际上,CJK 能够支持在 LaTeX 中使用包括中文、日文、韩文在内的多种亚洲双字节文字。CJK包含了中国,日本,韩国,越南,香港,也就是CJKVH。CJK是《GB2312-80》、《BIG5》等字符集的超集。
三、字符编码与实际应用
1、字符系统
1.1) SBCS(single Byte Chactacter System) 单字节字符系统
在这种编码格式下,所有字符都只用一个字节表示,ASCII码就是单字节字符。
1.2) MBCS(Multi-Byte Chactacter System) 多字节字符系统
在前文,我们曾留下一个问题,那就是在基于ANSI编码的原理上,对一个字符的表示实际上是无法确定他需要占用几个字节的,只能从编码本身来区分和解释。
比如计算机在拿到一个字节的时候,发现字节编码超过了0x7f,那么就认定是非ASCII编码,于是连续读取下一字节,直到遇到ASCll编码。因此计算机在存储的时候,就采用多字节存储的形式。也就是你需要几个字节我给你放几个字节,比如字母‘A’我给你放一个字节,比如汉字"中“,我就给你放两个字节,这样的字符表示形式就是MBCS。
在基于 GBK的简体中文windows中,“多字节”不会超过2个字节,所以windows这种表示形式有叫做DBCS(Double- Byte Chactacter System),其实算是MBCS的一个特例。C语言的char类型默认存放字符串就是用的MBCS格式。从原理上来说,这样是非常经济的一种方式,下面以实例证明。
如下图所示,中文和英文相安无事的存储在内存中,彼此的编码范围互不干涉,一个英文/符号占一个字节,一个中文占两个字节。
1.2) wchar_t(wide character type) 宽字符
前文说过,Unicode字符集使用三字节编码,且对于他的几种实现方式都有可能超过两字节。那么windows的DBCS采用最多两字节编码的方式显然不够用,即不能再使用char去保存文字了。那么就需要新的字符存储方式,这种字符叫宽字符,C语言中的关键字是wchar_t
。
宽字符无论字符字符长短,都会采用两字节存储。其编码方式和编译器有关,并非由语言本身确定。比如下图实例文件保存格式为UTF-8带BOM,就是采用了UTF-16的双字节存储方式:
注:对于常量字符串,如果想要将其当做宽字符处理,则需要在开头添加’L’用作标识。
我们可以看到,即使对于一字节的字母I(ASCII码49)也占用了两个字节,且低位字节为0。所以相同长度的wchar_t所占用内存是char的两倍。
四、总结
-
其实慢慢了解了字符编码的历史时,你就会发现,许多东西的产生都是有原因,不是为了解决问题,就是为了预防问题。
-
美国人先发明了计算机,所以享有先定义字符编码的权利,ASCII码(单字节编码)在这个背景下产生了。
-
因为计算机的流行,使得非英语母语的国家需要自己国家语言的字符编码,于是ASNI码(多字节编码)产生了。
-
多字节编码产生后,与之适配的MBCS(多字节字符系统)也应运而生。
-
有了MBCS后,又必须要考虑大小端的问题,否则无法统一又会产生问题。
-
之后,随着时代的发展。ASNI码越来越多,人们意识到需要一个东西对其加以区分,于是CodePage(代码页)出现了。
-
再之后,随着全球化的流行,人们不满足只显示自己国家语言文字的要求。比如年轻人追求潮流想打个韩文[撒浪嘿]都打不出来,因为两种以上的ASNI码不能同时显示,所以又引入了Unicode来解决。
-
引入了Unicode后,不能直接进行编码,否则会造成大量的内存浪费,于是更加节省内存空间的UTF-16、UTF-8方案产生了。
-
但这些方案它们相互之间并不兼容,而且无法区分,于是我们又引入了BOM头来区分。
五、参考资料
GB2312简体中文编码表
彻底搞懂字符编码(unicode,mbcs,utf-8,utf-16,utf-32,big endian,little endian…)
UCS-2和UCS-4