字符编码一直是软件开发过程中棘手的难题,甚至,有人称它是Evil。特别地,如果期望开发World-Ready的应用的时候,尤其要特别处理好字符集和编码的问题。最近在一个项目里面也遇到了复杂的字符编码问题,重新看了一些doc之后,自己的理解和认识也深刻了很多。在这个entry里面,我把我的认识记录下来,希望能一次性地把这个复杂的问题说清楚。
首先,为了方便说明问题,暂且对字符下这么个定义:字符是人类文字表达的一个最小单位。'A'是一个字符,'汉'也是一个字符。由于计算机存储的是0和1,要把代表人类文明的文字字符存储到计算机里面,就需要有做一下转换:
- 先把字符对应一个特定的数字,或者说编号(code point)上面。我们把那个字符-数字的对应表称为Codepage,或者叫Character Set,即字符集。
- 再把这个编号转换成一个字节流(byte stream)从而能将它顺利的存储在内存、硬盘,或者通过网络传输出去。我们把这个过程称之为Encoding。
而当需要从byte stream还原出字符显示到界面上的时候,走一个相反的流程,即从byte stream还原出编号(称为Decoding),再把编号对应到字符就可以了。考虑字符编码的问题时,应该时刻记住Codepage和Encoding/Decoding是密切相关,但又是完全独立的概念,这是理解问题的关键。
最早的Codepage应该就是我们最为熟悉的ASCII表了,最初的ASCII只收入了128个字符,包括大小写的英文字母以及常见符号和控制字符,后来ASCII扩展到了256个,编号从0开始到0xFF。于是乎,对于ASCII来说,Encoding/Decoding几乎不存在任何问题,256个编号正好用一个byte来表示,直接存取就是了。
后来,技术继续发展,人们越来越觉得ASCII字符集不够用,特别是东亚国家,比如中日韩,使用的是象形文字,根本就无法将本国文字字符挤进那个小小的、只有256个格子的ASCII里面。于是,每个国家地区都开始设计他们自己的Codepage,比如我国大陆地区80年的时候制定了GB2312的汉字字符集,香港台湾地区也针对繁体字的设计的BIG5字符集。这两个Codepage都很大,所以,256个编号肯定是不够用的,于是必然要突破一个byte的限制,达到2个byte。这时候考虑到Encode/Decode问题的时候就有些麻烦了,因为,在现实的字符串中可能出现中英文混合的现象,也就是说既有一个字节表示的字符,又有两个字节表示的字符,如何找到字符的边界呢?为了解决这个棘手的问题,Codepage在设计的时候也照顾了Encode可能遇到的困难,对于字符标号也做了特殊的规定,比如以一个特定的数字(对应到编码就是一个特定的byte)开始,于是就是lead byte和trail byte这样的说法。
再往后,人们发现各个国家、地区各自为政,各搞各的,设计出来的Codepage互相冲突,互不兼容成了大问题,特别是在一篇文章里面,无法同时显示几种不同Codepage的字符(因为他们的编号可能冲突了)。于是80年代时候,一些公司开始行动起来,建立一个叫Unicode的标准化组织,企图对各个国家的不同文字统一进行编号,总共收录了2^16 = 65536个字符,以消除潜在的冲突问题。与此同时,ISO组织也在着手设计一个包容各个国家各个民族所有一切文字字符的超大的Codepage,称为UCS - Universal Character Set。在最大程度向前兼容已有字符集的基础上,UCS又保证了各个字符之间绝不出现编码冲突的情况,代价是UCS被设计成惊人的2^31那么大。当然,在这个世界上,我们并不需要两个统一字符集,幸运的是Unicode组织和ISO也明白了这一点,于是它们开始合并。从Unicode 1.1开始,Unicode已经成为了UCS的子集。就这样,Unicode/UCS成了当今通用的统一的标准字符集。
到此为止,我们讨论的Unicode、UCS都是Codepage,而如何把字符编号存储和传输,都还没涉及到。因此,如果问一个的Unicode字符,或者UCS字符占多大的空间,这样的问题是没有意义的。因为,这完全取决于编码方式,也就是我们下面要讨论的问题。
最直观的编码方式当然是把UCS用2个byte(刚好足够表示其子集Unicode)或者4个byte(可以完整表示整个UCS集合)表示。这就是UCS-2和UCS-4两种encoding schema。然而这样表示还带来一个麻烦,那就是字节顺序问题。于是又派生出了UCS-2LE, UCS-2BE, UCS-4LE, UCS-4BE四种编码方式分别对应于Big Endian和Little Endian的情况。对于没有特别指定字节顺序的编码方式,又引入了一个叫Byte Order Mark, BOM的特殊字符,解码的时候读到它就知道后面的字节是什么样的顺序存储的了。需要注意到是,在一个byte stream里面,BOM出现可能不止一次,于是,decode的时候就可能需要来来回回地把字节流颠来倒去……看来,原以为最简单直接的编码方式,也一点都不简单啊。
然而问题还没完。要知道,目前计算器存储的、网络上传输的绝大多数字符还是那最早的ANSI Codepage里面那128个,所以如果它们也统统用UCS-2或者UCS-4来表示,不能不说是很大的浪费。此外,有一些老式的应用程序,已经习惯了以\0作为字符串结尾的表达方式,如果把UCS-2/4的字节流直接传给它们的话,可能很容易就crash掉了。于是,一种新的encoding schema出现了,它就是UTF-8。可以把UTF-8看成是一种Huffman编码,它让最常用的字符编码最短,即原有的ANSI 128个字符,还是用1 byte表示,而不常用的字符,用2个,甚至更多的字节来表示。此外,UTF-8保证,字符仍然是以\0结尾,即在byte stream中保证不出现\0字符。这样的编码方式的好处是,既节省了存储的空间,而且仍兼容以前的应用程序。后来又出现的UTF-16也是类似的编码方式,只不过是2个2个byte一起编码而已。
由于UTF-8/16的众多优点,已经被大量使用在最复杂、最广泛的应用环境:World-Wide Web里面。比如W3C的XML标准,就要求XML Parser至少必须支持UTF-8/16两种编码方式。
说了这么多,还只是停留在理论和标准上面,但是到了实际的应用领域,就更复杂了。比如,在VC7里面,这样的两个字符串:
const char * pA = "博客园";
const wchar_t *pW = L"博客园";
它们是以什么样的编码方式存在于内存中呢?MBCS, DBCS, Unicode, BSTR是什么呢?mbxxx和wxxx又有什么区别呢?等等……这些问题,留待日后,有空再写一篇。
浙公网安备 33010602011771号