Home | 2012-12-23

字符编码

这篇文章是我两年多前写给同事看的,当时不少同事对编码了解甚少,直到现在发现还是很多人对编码了解甚少,所以我就把这篇文章发出来让大家参考一下,希望对一些人有帮助,不过这篇文章是当时花了3个小时左右写的,错误在所难免。

字符编码历史

计算机,发明在20世纪中期西方国家。计算机内部使用二进制作为表示任何东西的基础,为了能够在计算机中使用整数、浮点数等都要对其进行编码,只是这个编码是在硬件层的(CPU指令),而计算机要与人进行交互就要对人所能识别的文字进行编码,ASCII就在那个时候诞生。

ASCII(American Standard Code for Information Interchange,美国信息互换标准代码)

美国标准编码,用于编码英文字母的编码方式。它用了0-127的数字之间来表示a-z等可见字符和一些控制字符,而这个字符集的编码就确定了ASCII字符集,而这个字符集要想要在计算机的二进制方式下使用就必须用计算机能够理解的方式来处理,ASCII使用了一个计算机字节来表示这种编码方式,在C/C++语言中一个字节通常我们使用char来表示。

GBK

GBK,大家都知道,汉字内码扩展规范。GBK是怎么来的呢?GBK由GB2312扩展来的,GB2312是最早的中文编码方式。GB2312又是怎么来的呢?

计算机发展到80年代在中国开始慢慢的兴起。为了能够让中国人能够更好的使用计算机,自然要引入中文编码到计算机中。但是引入中文编码遇到了一个问题,就是ASCII使用一个字节(char)来编码字符,但是中国的汉字是肯定不能够在一个字节中表示完全,怎么办?聪明的中国人发现一个字节(char)可表示的最大区间是0-255,而ASCII只使用了0-127,128-255并没有使用,那么我们就可以用多个字节来表示中文编码,比如:

一个字节(char)的值是在0-127之间,我们就还是用这个字节来表示ASCII里面的字符,也就是说兼容ASCII。

一个字节(char)的值是在128-255之间(此处假设是130),则说明它不是在ASCII字符集里面的,那它表示什么字符呢?此种情况下,则要在读取下一个字节(char)的值(此处假设是10),那么就将两个字节的值:130和10按照某种计算规则来计算等到一个值,此处计算得到3290,那么这个值可以在一个字符编码表中去查,它可能得到某个汉字,此处假设为‘字’字,然后就可以在屏幕上显示出来了。

(此处的计算都是假设,我没有详细查,以后有时间再更正,理解原理即可。也可以查看维基百科了解详细。)

GBK中有哪些字符?

GBK是微软利用GB2312未使用的编码空间,收入GB13000.1的全部字符制定而来。GBK是用来编码中文汉字的,并且兼容ASCII字符集。GBK中是不是只有简体中文呢?

因为简体汉字和繁体汉字有很大一部分是相同,把常见的几千个繁体中文里面的汉字编码进来也不会多多少,因此,GBK里面也是有繁体中文汉字的。

除此之外还有什么,因为现在简体和繁体的常见字都编码了,那日文里面的大多数文字也都在其中,只需要将日文的平假名和片假名(也就几十个字符)编码进来。

因此,GBK里面是有常见的简体中文、繁体中文、日文等字符在一起的字符集。

Big5等编码

中国人发明了多字节编码的方式,但是只能编码中国汉字(包括简繁和日文)。其他国家和中国台湾想要编码文字自然就会采用相似的方式来编码,Big5等编码方式就产生了,它们同样按照某种计算方式计算出值,此处假设还是3290,但是他们的编码表和GBK不一样,所以他们代表的文字就不在是‘字’了。

Windows ANSI编码

GBK、Big5等编码就是ANSI编码,也叫本地码。ANSI编码就是本地码的统称,就是在什么国家或地区就是什么编码。比如在中国的大陆地区就是GBK,在中国台湾就是Big5。

乱码

乱码,很常见也很烦的一个Bug,编码Bug。

假设我们在简体中文的机器上,那么本地码是GBK,我们用它编码了一些文字保存在txt文件里面,然后把它拷贝到繁体中文的机器上,此处的本地码是Big5,然后打开这个文件,假设我们使用记事本打开,它打开文件使用的默认编码就是本地码,即Big5,按照Big5的方式计算值,并依次查字符编码表,然后显示出来。乱码来了,本来在GBK中编码的有意义的字句,在Big5下计算查表出来得到的是一些文字字符,而这些文字字符连接在一起没有任何意思,因此乱码。

乱码和字体

乱码是编码引起的,而在文字显示中是要去字体中查询到某个字符的形状,然后显示,有可能出现字体里面没有这个文字,那么就会用一个默认的字符显示,此时给人的感觉也像乱码,通常表现出很多个字符都是一个样子(比如一个白框),但是它不是乱码,只是字体中没有文字的信息,换个字体就能显示。

UNICODE

随着互联网的发展,各个国家基本上都有自己的本地编码ANSI编码。为此,系统要支持多过的本地编码,怎么办?引入代码页code page,根据代码页号去查相应的字符集,GBK的代码页就是CP936(有细微差别,详细可以查看维基百科)。

但是代码页还是不能将不同字符集中的字符在同一系统中显示,比如:汉字和阿拉伯文不能同时显示。UNICODE诞生了。

UNICODE就是要将所有的字符全部编码在一个字符集里面,比如1-10000编码简体中文,10001-20000编码繁体中文,依次类推,这样就构成了UNICODE字符集。但是UNICODE字符集并没说要怎么编码,只是说某个数字代表某个字符,即之规定了数字到字符的的字典,但是没有规定在计算机中怎么编码。

UCS(Universal Character Set)/UTF(Unicode transformation format)

为了在计算机中编码字符,就出现了UCS/UTF编码,常见的有UTF-8,UTF-16(UCS-2),UTF-32(UCS-4)编码。

UTF-8是类似GBK编码的一种编码,就是用多个字节编码计算出值然后查表,它可以是一个字节(也就是兼容ASCII)表示一个字符,可以是两个、三个、四个或者更多个字节根据计算得到某个值,然后去查UNICODE表得到某个字符,这样就将所有字符进行了编码。

UTF-16则至少是需要两个字节来表示,也就是说,可以由两个字节计算得到某个值,也可以是四个字节、六个字节、八个字节计算出值然后查表得到字符。

UTF-32则至少是需要四个字节表示,以此类推。

C/C++中的编码

char在C/C++中表示一个字节,通常也用它来表示编码字符,如果它编码的字符是ASCII编码,则是每个字节都表示一个字符,也就是说每个char表示一个字符。如果编码是ANSI,此处假设是GBK编码,那么可以是一个char表示的字符,也可以是两个char表示一个字符。如果是UTF-8编码,那么可以是一个char、两个char、三个char或者更多来表示一个字符。

wchar_t是C/C++中的宽字符,标准没有规定它占几个字节,只是规定用来编码unicode字符集,一个wchar_t在windows(wchar_t是UTF-16编码)下面占2个字节,在linux(wchar_t是UTF-32编码)下面占4个字节,用wchar_t来编码unicode的话,常见字符都可以用一个wchar_t来表示。但是unicode字符集一直在扩充引入更多的字符,所以很有可能一个wchar_t(windows)不能表示出80001号字符,那么也就出现两个wchar_t表示一个字符,这也就正好符合UTF-16编码的规则。

C/C++中的乱码解决方法

乱码其实是无解的。大多数软件的软件的处理方式就是只处理UNICODE、UTF-8和ANSI编码,因此一段文字的ANSI编码和打开机器的本地码不一样那么就必然出现乱码,当然若人为的告诉软件说这段文字是某个code page编码的,那么还是可以正确显示,但是这是依靠人为操作了。

软件里面处理这个问题处理的最好的有一类软件,就是浏览器。浏览器检测文本编码的方式通常就是猜,猜它是哪种编码,猜完是哪种编码之后就用相应的code page去查字符,然后显示。那么这个猜是不是乱猜呢?不是,是通过逐个字节扫描进行统计,看看这段文本最可能是哪种编码。当然这样做也会有错误,那么也一样会出现乱码,但是已经出现乱码的几率很低了。(想详细了解可以查看firefox和chrome的源码)

看不懂比乱码好

假设一个程序是用的是GBK编码的字符串,那么在一个日文操作系统(Windows)上,软件的字符那将是乱码,这给人一个很不好的感受。即使此软件没有日文版,但是如果能够将简体中文正确的显示出来那还是要好上许多的,说不定使用软件的人还是个懂中文的人。

C++里怎么做?Windows从NT开始就支持宽字符版本的API,对于所有使用API的地方都是用宽字符版本就能够正常的显示出文字了,在C++中就是使用wchar_t。比如:

wchar_t *wstr = L"中文";

然后用对应的宽字符版本的API来显示出来就可以了。那是不是程序内部全部都应该使用wchar_t来表示字符?我个人推荐只在Windows下运行的程序这么做,也就是跟字符显示不相关的东西也使用wchar_t来表示。当然,也可以根据情况在只在显示字符的时候通过调用MultiByteToWideChar将其它编码的字符转换成宽字符来显示,这样显示不相关的字符就可以使用多字节字符集(ANSI、UTF-8)了。

对于跨平台的软件(Windows、Linux),我个人推荐使用UTF-8编码的字符来作为内部处理的字符,这样在只需要在字符显示的地方转换成相应的编码就可以了。当然主要还是在Windows上面做处理,调用MultiByteToWideChar将UTF-8转换成宽字符然后显示。

UTF-8在C++ 98中的表示

在目前的C++标准中,我们通常不能直接在代码里面写出UTF-8编码的字符串常量。

char *str = "中文";      // 对于VS,只有源文件是不带BOM的UTF-8编码时才是UTF-8字符串,
                         // 对于带BOM的UTF-8编码或者GBK编码的文件都是GBK的字符串;
                         // 对于GCC,源文件编码是什么那么这个字符串的编码就是什么。
wchar_t *wstr = L"中文"; // 这里使用的是unicode编码,但是,因为ANSI编码和UTF-8编码
                         // 都是兼容ASCII编码的,所以我们可以在代码里面这样写:
char *str = "abc";       // 此处的编码是ASCII、ANSI、UTF-8

也就是上面这段字符是可以当成ANSI编码也可以当成是UTF-8编码的,那么我们就可以将它当成UTF-8编码来使用。所以在代码里面最好不要出现字母以外的字符。(当然,不考虑多语言版的话除外)

那我们要与用户交互的时候不能是英文字母啊。我们可以从资源文件中读取,即我们可以将要显示的字符放置到ini、XML以及其它文本文件中,这些文件以UTF-8编码。这样我们程序就从资源文件中读取这些UTF-8编码的字符就可以了。这也就可以很好的做多语言版本了,只要将资源文件中的字符改成其它语言的字符就可以了,当然编码还是UTF-8。(Windows下窗口相关的资源.rc也使用UNICODE编码就行)

这样做值得么?值得不值得就看我们的程序是不是需要做多语言版,或者将来要不要做多语言版,如果要,这就是值得的,不要当然就无所谓了。

UTF-8在C++ 11中的表示

C++ 98中不能写出UTF-8、UTF-16、UTF-32的字符串常量,C++ 11加入了新字符类型char16_tchar32_t,其相应的常量表示如下:

u8"中文"; // 表示用UTF-8编码的字符串常量
u"中文";  // 表示用UTF-16编码的字符串常量
U"中文";  // 表示用UTF-32编码的字符串常量