字符编码研究
Updated:
1. 一些概念
字符集charset
字符编码characteren coding
常见字符集名称:ASCII字符集、GB2312字符集、BIG5字符集、GB18030字符集、Unicode字符集,OEM字符集(将ASCII从7bit扩展成8bit,有很多方案)等。
字符编码:ASCII字符编码
注:最初是没有字符编码与字符集的区别的,ASCII既是字符编码又是字符集;后来由于字符应用环境的变化,字符集开始与字符编码概念分开。典型的是Unicode,它有很多种字符编码。而某种字符编码在最终存储的时候可能又有不同。
内码:在计算机科学及相关领域当中,内码指的是“将资讯编码后,透过某种方式储存在特定记忆装置时,装置内部的编码形式”。在不同的系统中,会有不同的内码。
交换码:解决方法则为,在交换文件前,文件提供者先将由内码形式储存的文件转换成交换码形式再做交换。在接收文件后,文件接收者再由交换码转成内码。
为了方便起见,许多系统的内码则直接使用交换码,如ASCII广为各种系统所使用。
Unicode编码系统为表达任意语言的任意字符而设计。它使用4字节的数字来表达每个字母、符号,或者表意文字(ideograph)。每个数字代表唯一的至少在某种语言中使用的符号。(并不是所有的数字都用上了,但是总数已经超过了65535,所以2个字节的数字是不够用的。)被几种语言共用的字符通常使用相同的数字来编码,除非存在一个在理的语源学(etymological)理由使不这样做。不考虑这种情况的话,每个字符对应一个数字,每个数字对应一个字符。即不存在二义性。不再需要记录”模式”了。U+0041总是代表’A’,即使这种语言没有’A’这个字符。
UTF-8是ASCII(这里实际是指编码)的一个超集。因为一个纯ASCII字符串也是一个合法的UTF-8字符串,所以现存的ASCII文本不需要转换。为传统的扩展ASCII字符集设计的软件通常可以不经修改或很少修改就能与UTF-8一起使用。
2. 支持多字节字符集(MBCS)
多字节字符集(MBCS)是一种替代Unicode以支持无法用单字节表示的字符集(如日文和中文)的方法。为国际市场编程时应考虑使用Unicode或MBCS,或使程序能够通过更改开关来生成支持两种字符集之一的程序。
最常见的MBCS实现是双字节字符集(DBCS)。一般来说,VisualC++(尤其是MFC)完全支持DBCS。
现在熟知的256个字符的版本其实叫做EASCII编码方案,你可能会以为EASCII是一种编码方案,其实不然,他是一大堆基于ASCII编码方案的扩展,详细资料可以参见IEC_8859。
VC++2008中的字符使用
1.char *p=”abcdf”; thensizeof(p)=4 // (the length of a pointer)
Char p[10]=”abcdf”; thensizeof(p)=15 // the size of the array
String a=”somestring”; function sizeof(a) will show the fact memory of a
2.char指针也可以指向中文字符,不过此时它是两个首位为1的字符组合而成的,占两个字节,所以用指针偏移只取一个字符的话,值为负值
目前的总结如下:——万变不离其宗
vc++中可以在项目属性中设置使用的代码页:未设置、Unicode宽字符、多字节字符集(MBCS)
与此无关的是,c++中有两种字符,一种是ANSI字符(即ASCII),另外一种为Unicode字符。
在VC中的ASCII字符猜测已经被扩展过了,不然是无法识别中文的。
ASCII用数据类型char表示,Unicode用数据类型wchar_t表示,其中wchar_t通过重定义得到,重定义的代码为:type unsigned short wchar_t。
这两种数据类型分别有对应的字符串处理函数。
char类型的字符串为”string”,Unicode类型的字符串为L”string”;L是宏,告诉编译器这是一个Unicode字符。
如果仅仅使用单纯的char与wchar_t,是涉及不到(未设置、Unicode宽字符、多字节字符集)的。宽字节就是Unicode编码(应该是UTC-16编码),多字节字符则相当,按照每一个字节读取,如果首位为0,则是char字符;如果首位为1,则继续读,然后组合成一个字符。在这种情况下,组合之后显示的字符可能因地区而异(不同地区使用的编码方式可能不同),但是这种方式是比较节省空间的,并且可以使用char的函数(这时候如果测量字符的数目,可能测不准)。
对于多字节的编码的显示,涉及到了代码页的内容。ANSI标识对世界上的国家分别设定了相应的代码页。
那么,对于一以那种方式进行编码呢。为此,引入了上述选项进行选择。选择的标识用TCHAR。对于一个TCHAR类型,使用TEXT(“string”)进行声明。如果使用了Unicode选项,则编译成Unicode宽字节,即每一个字符占两位,相当于L”string”;如果使用了多字节编码,则相当于使用了ANSI进行编码,字符占一位。
UTF-8是UNICODE的一种变长字符编码又称万国码,由KenThompson于1992年创建。现在已经标准化为RFC3629。UTF-8用1到6个字节编码UNICODE字符。用在网页上可以同一页面显示中文简体繁体及其它语言(如日文,韩文)
3. 字符集分类:
3.1. 单字节字符集SBCS
ASCII;
EASCII(对ASCII的扩展,不是一种编码方案,而是一大堆基于ASCII编码方案的扩展)
3.2. 多字节字符集MBCS
一般是一个国家的扩展,所以基本兼容ASCII,按照我的理解,多字节字符集,其中的一个字符长度是固定的,比如在DBCS中,对于’A’,也是用的两位。
之所以有多字节字符集与Unicode字符集之分,是因为多字节字符集的字符集不是通用的。
l 双字节字符集DBCS
GB2312,日本的**字符集…
l 三字节字符集
3.3. Unicode字符集
Unicode的字符集与字符编码是严格区分的,目前有UTF-8,UTF-16,UTF-32等编码格式。
Unicode中每一个字符与一个无符整数对应,这个整数可能很大,甚至超过4个、5个字节。但是在存储的时候不用理他,因为要存储Unicode必须编码后放在文件中。
UTF-16是双字节编码,UTF-32是四字节编码,(很有可能,它们都不能表示未来所有的字符),但是这样的编码效率比较低。于是常用的是utf-8编码。
UTF-8编码是可变长编码,用1~6个字节来表示一个字符,有点像霍夫曼编码,我想这是字符编码的一个进化。
4. 在编程中的样子
*我打开了IDE,在源码中定义了一个字符变量,它的内容为’a’。那么编译之后运行的时候,操作系统需要在内存中分配多大的单元存储’a’呢?所以我需要用char或者wchar——t来说明’a’所要获得的空间。char很明显,是ASCII字符,分配一个字节,在内存中存储’a’对应的值。但是如果是wchar_t,虽然type unsigned short wchar_t,但是一个short类型的值并不是占用两个字节,而取决于操作系统,因为在标准c++中是这么说明的。那好,我知道了’a’在内存中的样子。
*但是如果是中文’我’呢,如果声明为char,在编译之后的exe文件中,肯定是有’我’这个字符的(以ASCII字符存储,不过在exe中怎么存就不知道了),但是,在运行的时候,系统只会分配一个字节来存放’我’,所以在内存中这个字节是一个负值。如果声明为wchar_t就可以了,声明为wchar_t之后(也就告诉了系统这是一个Unicode字符),在exe文件中会以’我’对应的Unicode整数存储在exe中,运行的时候有系统决定分配的内存空间并把这个整型值放到内存中。
注意,在这一步的时候,可能会出现乱码。如果用户使用的操作系统不认识Unicode,那么这个内存中的整型值显示到屏幕的时候则是乱码。
Windows在2000以上的版本中好像都认识Unicode。
决定了运行时候的样子之后,我要保存源文件。这时候涉及到字符编码。
当有其他字符出现的时候,操作系统就需要认识其他字符了。在没有Unicode字符集的时候,若想Windows显示中文,则需要Windows含有中文的字符集,并在程序编译的时候告诉编译器这是中文的字符集。为此微软引入了代码页。
我想一个操作系统应该有一个默认的代码页,然后编译器由一个代码页。代码页即指定了字符集。这时候还没有wchar_t,一切都还是char。编译器将char字符或者字符串编译成当期编译器环境所设置成的代码页,比如GB2312的中文的字符’a’,对应的整型值是XX,那么IDE则将XX弄进exe里,在运行的时候,内存中存放的是XX,如果Windows认识GB2312,那么则能正确显示,如果不认识(即Windows系统或者文本编辑器没有这个代码页呢),显然就无法正确显示了。
从键盘上输入了一个字符’a’,输入到了IDE中。那么操作系统就需要在内存中分配一个单元存储’a’,分配多大的空间呢。
对于文本文件的流程:输入一个字符->操作系统识别这个键盘字符,并按照内定方式映射到内存中(并且在文本编辑器中显示)->保存文件(要求选择编码格式)->按照此编码编码成二进制文件保存在存储器中->请求该文件、获取该文件、读取该文件->内存中存放的是二进制->文本编辑器识别内存数据并将其显示。
对于exe文件的编辑:定义数据类型->IDE按照指定方式(比如多字节还是宽字节)编译->运行时根据操作系统以及数据类型分配空间->认识当前的字符集并理解字符含义->处理这些字符。
似乎进入内存之后,就不存在字符编码的问题了,而只涉及到字符集。
所以原来的strcpy()等函数也许只是对内存进行复制罢了。
5. 关于汉字编码
字符必须编码后才能被计算机处理。计算机使用的缺省编码方式就是计算机的内码。早期的计算机使用7位的ASCII编码,为了处理汉字,程序员设计了用于简体中文的GB2312和用于繁体中文的big5。
GB2312(1980年)一共收录了7445个字符,包括6763个汉字和682个其它符号。汉字区的内码范围高字节从B0-F7,低字节从A1-FE,占用的码位是72*94=6768。其中有5个空位是D7FA-D7FE。
GB2312支持的汉字太少。1995年的汉字扩展规范GBK1.0收录了21886个符号,它分为汉字区和图形符号区。汉字区包括21003个字符。
从ASCII、GB2312到GBK,这些编码方法是向下兼容的,即同一个字符在这些方案中总是有相同的编码,后面的标准支持更多的字符。在这些编码中,英文和中文可以统一地处理。区分中文编码的方法是高字节的最高位不为0。按照程序员的称呼,GB2312、GBK都属于双字节字符集(DBCS)。
2000年的GB18030是取代GBK1.0的正式国家标准。该标准收录了27484个汉字,同时还收录了藏文、蒙文、维吾尔文等主要的少数民族文字。从汉字字汇上说,GB18030在GB13000.1的20902个汉字的基础上增加了CJK扩展A的6582个汉字(Unicode码0x3400-0x4db5),一共收录了27484个汉字。
CJK就是中日韩的意思。Unicode为了节省码位,将中日韩三国语言中的文字统一编码。GB13000.1就是ISO/IEC10646-1的中文版,相当于Unicode1.1。
GB18030的编码采用单字节、双字节和4字节方案。其中单字节、双字节和GBK是完全兼容的。4字节编码的码位就是收录了CJK扩展A的6582个汉字。例如:UCS的0x3400在GB18030中的编码应该是8139EF30,UCS的0x3401在GB18030中的编码应该是8139EF31。
微软提供了GB18030的升级包,但这个升级包只是提供了一套支持CJK扩展A的6582个汉字的新字体:新宋体-18030,并不改变内码。Windows的内码仍然是GBK。
6. Windows下的编码
目前Windows的内核已经支持Unicode字符集,这样在内核上可以支持全世界所有的语言文字。但是由于现有的大量程序和文档都采用了某种特定语言的编码,例如GBK,Windows不可能不支持现有的编码,而全部改用Unicode。
Windows使用代码页(codepage)来适应各个国家和地区。codepage可以被理解为前面提到的内码。GBK对应的codepage是CP936。
微软也为GB18030定义了codepage:CP54936。但是由于GB18030有一部分4字节编码,而Windows的代码页只支持单字节和双字节编码,所以这个codepage是无法真正使用的。
7. 其他
3.1. big endian和little endian
bigendian和littleendian是CPU处理多字节数的不同方式。例如“汉”字的Unicode编码是6C49。那么写到文件里时,究竟是将6C写在前面,还是将49写在前面?如果将6C写在前面,就是big endian。如果将49写在前面,就是little endian。
3.2. 关于换行符
注意换行键与回车键其实并不相同。
在Windows的记事本中,输入一个回车符,然后使用二进制查看,代码是0D0A,即一次回车实际上输入了两个字符CRLF;如果输入两个回车,则是0D0A0D0A。在Linux下,不太一样。虽然很多程序不在乎DOS/Windows格式的CR/LF文本文件,但是有几个程序却在乎——最著名的是bash,只要一遇到回车,它就会出问题。