详谈字符编码[一]字符编码中的坑
说起字符编码首先可能想到的词汇有Unicode,UTF,UCS,内码,区位码,跨平台等词汇,甚至还有输入法,乱码,emoji,微软雅黑,URL encode等相关的词汇也会冒出来,足以说明字符编码在计算机中重要的地位。程序员关心的是在自己的代码中如何处理好字符编码的问题,特别是像C/C++这样历史悠久而又偏向底层的语言中如何处理好这一问题。实际上这个问题在今天已经得到了很好的解决,前辈们通过努力使得今天的字符编码基本上不再困扰程序的使用者和编程人员,但其留下的诸多历史痕迹却还一直在困扰着一代又一代的编程初学者。本系列博客共有五篇,尽量详细但不失广度,通俗不失深度地介绍关于字符编码的内容,但求抛砖引玉。
详谈字符编码[一]将介绍编码,输入码,机内码,字形码,字形库等概念,主要说一下这些概念给我们挖的坑。
接下来的几篇还会介绍“什么是代码页”,“字符输出与消除乱码”,“宽窄字符--字符相关的数据结构”,“常用IO函数的效率分析”等。
一.编码
编码(encode)的概念并不仅仅出现在计算机科学中,字符编码(character encoding)就是要找到一种能够用表示字符的方案,以方便存储与传输。
电影《无间道》中梁朝伟和黄秋生通过传输莫斯电码的方式传输信息,再或者小说里两情相悦的男女自己发明一套符号交流感情的桥段,这都是一种编码。但我们要谈的主要还是大家公认的字符的数字编码。这些编码像GBK,UTF-8,UCS-2,ASCII等等。还有Unicode?这就要说说这几个概念的不同了。
ASCII:信息技术萌发在西方社会,最早的编码也只针对英语的习惯(ASCII只定义了英语字母和数字,符号等)。那时候总共也不超过128个字符,编码方式一张表就全说明白了,表的内容正是字符与数字(0到127)的一一对应。在计算机上的具体实现方式就是用数字对应的二进制无符号数作为字符的机器表示。
因为那时候要编码的字符太少,人们甚至都没怎么感觉到“规定字符和数字的对应方式”与“规定数字的二进制表示方式”是两个相对独立的过程。多年后,面对着浩如烟海的待编码字符,这种分治成为必要。
Unicode:Unicode一开始定位的问题就是给全世界的所有字符(包括古代字符)一个统一的身份证号。所谓统一就是谁跟谁也不会冲突,每个字符都有自己的位子。相比给128个字符编码,这个问题的复杂程度前无古人。分离“制定字符集”和“制定二进制编码”这两个过程势在必行。
制定统一字符集就是给全世界的字符一个编号,称为码点(code point ),一般用这样“U+7231”(表示汉字 爱 )的方式表示。码点是给人看的,机器只认识二进制,不认识什么叫”U+7241“。所以字符集定义完成也只完成了一半工作,但已经完成了主要的工作。另一部分的工作就是把数字定义为二进制编码,这时出现了多种方案,著名的就有UTF-8,UTF-16,UTF-32,UCS-2,UCS-4。
字符集说明这种表示方式支持多少字符,比如Unicode能表示很多字符(而且仍在不断扩充),ASCII就只有128个。而编码方式则用来说明怎么用二进制表示字符集里字符对应的数字。到这里应该明白,Unicode是指一个字符集,而UTF-8,UCS-2都是具体的编码方式。ASCII呢,我想应该理解为它既说明了字符集又说明了编码方式。
分开”定义字符集“和”定义编码“这两部分的工作,好处有很多,我大概想到三点:
1.这两部分工作用到的领域知识并不相同,因此天然需要分开。定义字符集的专家需要相当的社会学,语言学的人文知识,而编码工作需要由有计算机底层知识的工程师去完成。
2.定义字符集的工作可以不受编码工作的影响,有利于字符集今后的扩充。Unicode的扩充一直在进行,但编码方式基本只有UTF-8和UTF-16使用的最多,今后也基本不会再变。
3.不同的系统可以选择不同的二进制实现,哪个方便选哪个。不同的用途也可以选择不同的编码,比如网络传输用UTF-8,内部存储用UTF-16。
二.UTF-8,UTF-16,UTF-32,UCS-2,UCS-4
网上已经有不少介绍具体编码方式的文章,这里不再赘述,只画一下重点。
Ⅰ。UTF-16与UCS-2是不同的编码方式(UCS-4也不同与UTF-32)。简单的来说,UCS是较旧的概念。早期Unicode认为32bit足以表示全世界的字符,但现实是后来发现不够便经过了一次扩充(U+XXXX变成U+XXXXXX,比如U+1F60A是emoji中的笑脸)。UCS-2只能表示扩充前的字符(请自行百度”Basic Multilingual Plane“)。UTF-16完全兼容UCS-2,但支持所有Unicode字符,代价就是UTF-16是变长的,一个UTF-16字符可能是16bit也可能用32bit(请自行百度”surrogate pair“)。
Ⅱ。UTF-16,UTF-32存在大小端的问题,UTF-8不存在。简单来说因为UTF-16的码元的2字节,UTF-32的码元的4字节都是多字节,而UTF-8的码元是一个字节,单字节不存在大小端问题(无所谓字节序)。
但真正要解释清除还要说一下什么是Unicode的码元,码元(是Unicode的码元,不是通讯学里说的那个码元)是机器一次处理的数据单位,是字节(8bit)的整数倍。要知道机器可不是一个字符一个字符处理的,而是一个码元一个码元的处理,具体来说C++中UTF-8不是用char数组存储吗,所以机器一次处理一个char类型(8bit)。如果码元只包含一个字节,一次处理一个字节当然不会有大小端的问题。但如果是多字节码元(UTF-16,UTF-32)就有区别了,x86是小端模式(x64不过是x86_64),他表示的UTF-16的’爱‘当然也应该等于7231但是在内存中就是按照0x3172这样存储的,不过,也没关系毕竟机器自己认得在wchar_t的上下文中0x3172表示的值就是0x7231。问题是一旦换一台新机器,鬼知道你的0x3172表示的是3172还是7231?新机器也是小端那还好,默认认为0x3172就是7231。但如果是大端的机器,默认理解3172就是3172,爱就变成另一个字了。再看单字节码元的UTF-8,不论在大端还是小端的机器中都存储为E788B1,而且都理解为E788B1,因此也就不存在大小端问题了。
Ⅲ。BOM(Byte Order Mark)是为了区分大小端而存在的。用U+FEFF作为第一个字符(选择它是因为U+EFFF在是非法的码点),U+FEFF这个码点的名称就是”零长不折行空格“,意思就是什么都不显示,零长度嘛,所以即便在字符中任意插入U+FEFF也不会影响显示出来的结果。一台机器读取字符时发现前两个字节是FFEF,就知道之后的文本是大端表示,如果前两个字节是EFFF,就知道之后的文本是小端表示。UTF-8没有大小端问题,BOM也就可有可无,但如果有的话应该是什么呢?就是U+FEFF采用UTF-8编码得到的:EFBBBF。
Ⅳ。知乎上有这样的问题:”Windows为什么用GBK而不是UTF-8?“,“为什么Windows简体中文默认编码是GBK?”这可真冤枉微软了,Windows会放着Unicode这么好的东西不用吗?其实Windows2000之后的操作系统内码都是UTF-16了。如果你还有有这样的误会,请关注后续博客"什么是代码页"。
三.机内码,输入码,字形码
这三个概念是应该放在一起的,因为分别代表了一个字符从输入到处理存储再到输出计算机的过程。输入阶段用到的是输入码,比如拼音码,五笔字型码,区位码,他们基本上都对应了一类输入法,用来完成字符的输入。当我们使用了五笔输入并在按键上按下EP这两个按键时,一系列按键消息传到输入法程序中,输入法在把这一系列按键消息翻译成我们想输入的汉字:爱。机内码则是字符在计算机内的表示方式,一般说的字符编码也是指机内码。字形码就有意思了。计算机知道内存中的一个字符是“爱”,要把它显示到屏幕或打印到纸上,这就需要字形码。他包含了一个字符形状的点阵信息或者矢量信息,总之是和“字符的样子”有关的信息。
图1.1 汉字“爱”的16*16点阵
字形的信息存储在字库中(win10中C:/windows/Fonts/目录下的就是关于字形的文件了)。比如上边的“爱”字是一个可以查询字形信息的程序输出的,这个程序就需要读取附带的字库信息。单片机想要在一个LED显示屏上显示一个字符时用到的底层驱动程序也是类似原理,针式打印机要在纸上打印同样需要字形信息。一个字当然可以有很多种字体,每一种字体都需要对应字形库的支持。多说一句字形设计可是有著作权保护的,方正楷体可以免费商用,但微软雅黑商用就需要付费了(但Windows用户可以在屏幕上显示微软雅黑,因为这部分版权费微软已经给方正付过了)。
任何刚学完控制台输出的新手都有写一个字符画程序的冲动,这里有一个以前写的可以将图片转换为bmp位图字符画的程序例子,涉及字库的使用,bmp位图的文件格式,生成灰度图等知识。(自己吐槽:这个程序很鸡肋,要么看得清图片全貌看不清每个汉字,要么看得清汉字但图片却超出了屏幕范围,要想写一个更好的字符画程序不得不研究一下图像处理呢)这里抖胆把效果展示一下。
原图 缩小后的字符画 字符画局部,可以看到由汉字组成
这篇的全部内容就是这样了,下一篇会介绍”宽窄字符--字符相关的数据结构“,包括C++11中新加入的char16_t和char32_t的内容。喜欢就推荐一下吧。
祝大家国庆,中秋快乐!