
2.5 字符集
2.5.1 计算机上的三种字符集
在计算机中每个字符都要使用一个编码来表示,而每个字符究竟使用哪个编码来表示,要取决于使用哪个字符集(charset)。
计算机字符集可归类为三种,单字节字符集(SBCS)、多字节字符集(MBCS)和宽字符集(即Unicode字符集)。
1.单字节字符集(SBCS)
SBCS(Single-Byte Chactacter System)的中文意思是单字节字符系统,它的所有字符都只有一个字节的长度,SBCS是一个理论规范。具体实现时有两种字符集:ASCII字符集和扩展ASCII字符集。
ASCII字符集主要用于美国,它由美国国家标准局(ANSI)颁布,它的全称是美国国家标准信息交换码(American National Standard Code For Information Interchange),它使用7位来表示一个字符,总共可以表示128个字符(0~127),但一个字节有8位,有一位不需要用到,因此人们把最高一位永远设为0,用剩下的7位来表示这128个字符。ASCII字符集包括了英文字母、数字、标点符号等常用字符,如字符‘A’的ASCII码是65,字符‘a’的ASCII码是97,字符‘0’的ASCII码是48,字符‘1’的ASCII码是49,具体可以查看ASCII码表。
计算机刚刚在美国兴起的时候,ASCII字符集中的128个字符够用了,一切应用都是妥妥的。但后来计算机发展到欧洲,欧洲各个国家的字符就多了,128个就不够用了,怎么办?人们对ASCII码进行了扩展,因此就有了扩展ASCII字符集,它使用8位表示一个字符,这样表示256个字符,在前面0到127的范围内定义与ASCII字符集相同的字符,后面多出来的128个字符用来表示欧洲国家的一些字符,如拉丁字母、希腊字母等。有了扩展ASCII字符集,计算机在欧洲的发展也是妥妥的。
2.多字节字符集(MBCS)
随着计算机普及到更多国家和地区(比如东亚和中东),由于这些国家的字符更加多,8位的单字节字符系统(SBCS)也不能满足信息交流的需要了。因此,为了能够表示其他国家的文字(比如中文),人们对ASCII码继续扩展,就是在欧洲字符以及扩展的基础上再扩展,即英文字母和欧洲字符为了和扩展ASCII兼容,依然用1个字节表示,而对于其他各国自己的字符如中文字符则用2个字节表示,这就是多字节字符系统(Multi-Byte Chactacter System, MBCS),它也是一个理论规范,具体实现时各个国家和地区根据自己的语言字符分别各自实现了不同的字符集,比如中国大陆实现了GB-2312字符集(后来又扩展出GBK和GB18030),中国台湾地区实现了big5字符集,日本实现了jis字符集,等等。这些具体的字符集虽然不同,但实现依据都是MBCS,即256后面的字符用2个字节表示。
MBCS解决了欧美地区以外的字符表示,但缺点也是明显的。MBCS保留原有扩展ASCII码(前面256个)的同时,用2个字节来表示各国的语言字符,这样就导致占用一个字节和两个字节的混在一起,使用起来不方便。如字符串“你好abc”,字符数是5,而字节数是8(最后还有一个‘\0')。对于用++或--运算符来遍历字符串的程序员来说,这简直就是恶梦。另外,各个国家地区各自定义的字符集难免会有交集,因此使用简体中文的软件,就不能在日文环境下运行(显示乱码)。
这么多国家都定义了各自的多字节字符集,以此来为各自国家的文字编码。那操作系统如何区分这些字符集呢?操作系统通过代码页(CodePage)来为各个字符集定义一个编号,比如437(美国英语)、936(简体中文)、950(繁体中文)、932(日文)、949(朝鲜语_朝鲜)、1361(朝鲜语_韩国)等都是属于代码页。在Windows操作系统的控制面板可以设置当前系统所使用的字符集,打开Windows 7控制面板里的“区域和语言”对话框,然后切换到“管理”,可以看到当前非Unicode(也就是多字节字符集)程序使用的字符集,如图2-19所示。

图2-19
图2-19中选定的语言是“中文(简体,中国)”,那系统此时的代码页是936。我们可以编制一个控制台程序验证一下。要说明的是,控制台程序输出窗口默认使用的代码页(字符集)就是操作系统的代码页(字符集),也可以修改控制台窗口的代码页,使用函数是SetConsoleOutputCP。
【例2.7】 获取控制台窗口的代码页
(1)打开Visual C++ 2013,建立一个控制台工程。
(2)在Test.cpp中输入代码如下:
#include "stdafx.h" #include "windows.h" int _tmain(int argc, _TCHAR* argv[]) { UINT codepage = GetConsoleOutputCP(); //获得控制台输出窗口的代码页值 printf("当前系统使用的代码页是:%d\n", codepage); return 0; }
(3)保存并运行工程,运行结果如图2-20所示。
上面的程序说明当前系统所使用的代码页是936,即使用的是简体中文语言。printf里面的中文字符串也被正确地打印出来了,一切正常。也可以在命令行窗口下查看代码页,在图2-19的窗口左上方单击黑色图标,出现菜单,然后在菜单上选择“属性”,在属性对话框上可以看到当前操作系统的代码页,如图2-21所示。

图2-20

图2-21
如果我们在程序中把控制台窗口的代码页(字符集)修改成437(美国英语),则就不能正确输出中文了。
【例2.8】 设置控制台窗口的代码页
(1)打开Visual C++ 2013,建立一个控制台工程。
(2)在Test.cpp中输入代码如下:
#include "stdafx.h" #include "windows.h" int _tmain(int argc, _TCHAR* argv[]) { SetConsoleOutputCP(437); //设置控制台窗口代码页为437 UINT codepage = GetConsoleOutputCP(); printf("当前系统使用的代码页是:%d\n", codepage); return 0; }
(3)保存并运行工程,运行结果如图2-22所示。
设置控制台窗口的代码页为437(美国英语)后,控制台窗口中的中文就不能正确显示了,因为美国英语字符集中没有对中文文字的编码。
下面我们看看含有多字节字符的中文字符串程序是否能在使用日文字符集上正确显示呢?打开Wineows 7控制面板里的“区域和语言”对话框,然后切换到“管理”,单击“更改系统区域设置”按钮,出现“区域和语言设置”对话框,在“当前系统区域设置”下拉列表框中选择“日语(日本)”,如图2-23所示。

图2-22

图2-23
然后单击“确定”。此时会提示重启,单击“确定”重启电脑。重启后我们再编制一个程序。
【例2.9】 尝试在日语字符集下打印中文
(1)打开Visual C++ 2013,建立一个控制台工程。
(2)在Test.cpp中输入代码如下:
#include "stdafx.h" #include "windows.h" int _tmain(int argc, _TCHAR* argv[]) { SetConsoleOutputCP(936); //设置控制台窗口代码页为936,简体中文 UINT codepage = GetConsoleOutputCP(); printf("当前系统使用的代码页是:%d\n", codepage); return 0; }
(3)保存并运行工程,运行结果如图2-24所示。

图2-24
输出存在乱码。结果表明,即使设置了控制台的代码页为简体中文,中文字符串也不能被正确显示。这是因为系统中没有对中文编码的代码页(字符集)。这就说明了,含有多字节字符的中文信息软件不能在使用了其他字符集的系统上正确显示。那怎么解决这个问题呢?方法是使用宽字符字符集,即Unicode编码。
3. Unicode编码
Unicode编码是纯理论的东西,和具体计算机没关系。为了把全世界所有的文字符号都统一进行编码,标准化组织ISO提出了Unicode编码方案,它可以容纳世界上所有文字和符号的字符编码方案,这个方案规定任何语言中的任一字符都只对应一个唯一的数字,这个数字被称为代码点(Code Point),或称码点、码位,它用十六进制书写,并加上U+前缀,比如,‘田’的代码点是U+7530; ‘A’的代码点是U+0041。再强调一下,代码点是一个理论的概念,和具体的计算机无关。
所有字符及其Unicode编码构成的集合就叫Unicode字符集(Unicode Character Set, UCS)。早期的版本有UCS-2,它用两个字节编码,最多能表示65535个字符。在这个版本中,每个码点的长度有16位,这样可以用0至65535(2的16次方)之间的数字来表示世界上的字符(当初以为够用了),其中0至127这128个数字表示的字符依旧跟ASCII完全一样,比如Unicode和ASCII中的数字65,都表示字母‘A';数字97都表示字母‘a'。但反过来却是不同的,字符‘A’在Unicode中的编码是0x0041,在ASCII中的编码是0x41,虽然它们的值都是97,但编码的长度是不一样的,Unicode码是16位长度,ASCII码是8位长度。
但UCS-2后来不够用了,因此有了UCS-4这个版本,UCS-4用4个字节编码(实际上只用了31位,最高位必须为0),它根据最高字节分成2^7=128个组(最高字节的最高位恒为0,所以有128个)。每个组再根据次高字节分为256个平面(plane)。每个平面根据第3个字节分为256行(row),每行有256个码位(cell)。组0的平面0被称作基本多语言平面(Basic Multilingual Plane, BMP),即范围在U+00000000到U+0000FFFF的码点,若将UCS-4的BMP去掉前面的两个零字节就得到了UCS-2(U+0000~ U+FFFF)。每个平面有2^16=65536个码位。Unicode计划使用了17个平面,一共有17*65536=1114112个码位。在Unicode 5.0.0版本中,已定义的码位只有238605个,分布在平面0、平面1、平面2、平面14、平面15、平面16。其中平面15和平面16上只是定义了两个各占65534个码位的专用区(Private Use Area),分别是0xF0000-0xFFFFD和0x100000-0x10FFFD。所谓专用区,就是保留给大家放自定义字符的区域,可以简写为PUA。平面0也有一个专用区:0xE000-0xF8FF,有6400个码位。平面0的0xD800-0xDFFF,共2048个码位,是一个被称作代理区(Surrogate)的特殊区域。代理区的目的是用两个UTF-16字符表示BMP以外的字符。在介绍UTF-16编码时会介绍。
在Unicode 5.0.0版本中,238605-65534*2-6400-2408=99089,余下的99089个已定义码位分布在平面0、平面1、平面2和平面14上,它们对应着Unicode目前定义的99089个字符,其中包括71226个汉字。平面0、平面1、平面2和平面14上分别定义了52080、 3419、43253和337个字符。平面2的43253个字符都是汉字。平面0上定义了27973个汉字。
再归纳总结一下:
(1)在Unicode字符集中的某个字符对应的代码值,称作代码点(Code Point),简称码点,用十六进制书写,并加上U+前缀。比如,‘田’的代码点是U+7530; ‘A’的代码点是U+0041。
(2)后来字符越来越多,最初定义的16位(UC2版本)已经不够用了,后来用32位(UC4版本)表示某个字符的代码点,并且把所有CodePoint分成17个代码平面(Code Plane):其中,U+0000~ U+FFFF划入基本多语言平面(Basic MultilingualPlane,简记为BMP);其余划入16个辅助平面(Supplementary Plane),代码点范围U+10000~ U+10FFFF。
(3)并不是每个平面中的代码点都对应有字符,有些是保留的,还有些是有特殊用途的。
2.5.2 Unicode编码的实现
到目前为止,关于Unicode,我们都是在讲理论层面的东西,没有涉及Unicode码在计算机中的实现方式。Unicode的实现方式和编码方式不一定等价,一个字符的Unicode编码是确定的,但是在实际存储和传输过程中,由于不同系统平台的设计可能不一致,以及出于节省空间的目的,对Unicode编码的实现方式有所不同。Unicode编码的实现方式称为Unicode转换格式(Unicode Transformation Format,简称为UTF)。Unicode编码的实现方式主要有UTF-8、UTF16、UTF-32等,分别以字节(BYTE)、字(WORD,2个字节)、双字(DWORD,4个字节,实际上只用了31位,最高位恒为0)作为编码单位。根据字节序的不同,UTF-16可以被实现为UTF-16LE或UTF-16BE, UTF-32可以被实现为UTF-32LE或UTF-32BE。再次强调,这些实现方式是对Unicode码点进行编码,以适合计算机的存储和传输。
1. UTF-8
UTF-8以字节为单位对Unicode进行编码,这里的单位是程序在解析二进制流时的最小单元,UTF-8中,程序是一个字节一个字节地解析文本。从Unicode到UTF-8的编码方式(即对Unicode码点进行UTF-8编码),如下表所示。

从表2-1可以看出,UTF-8的特点是对不同范围的字符(也就是Unicode码点,一个码点对应一个字符)使用不同长度的编码。对于0x00-0x7F之间的字符,UTF-8编码与ASCII编码完全相同。UTF-8编码的最大长度是4个字节。4字节模板有21个x,即可以容纳21位二进制数字。Unicode的最大码点0x10FFFF也只有21位。
举个例子,‘汉’这个中文字符的Unicode编码是0x6C49。0x6C49在0x0800至0xFFFF之间,使用3字节模板:1110xxxx 10xxxxxx 10xxxxxx。将0x6C49写成二进制是:0110110001001001,用这个比特流从左到右依次代替模板中的x,得到:111001101011000110001001,即E6 B1 89。这样,‘汉’的UTF-8编码就是E6B189。
再看个例子,假设某字符的Unicode编码为0x20C30,0x20C30在0x010000至0x10FFFF之间,使用4字节模板:11110xxx 10xxxxxx 10xxxxxx 10xxxxxx。将0x20C30写成21位二进制数字(不足21位就在前面补0):000100000110000110000,用这个比特流依次代替模板中的x,得到:11110000101000001011000010110000,即F0 A0 B0 B0。
2. UTF-16
UTF-16编码以16位无符号整数为单位,即把Unicode码点转换为16比特长为一个单位的二进制串,以用于数据存储或传递。程序每次取16位二进制串为一个单位来解析。我们把Unicode编码记作U。具体编码规则如下:
(1)代理区
因为Unicode字符集的编码值范围为0-0x10FFFF,而大于等于0x10000的辅助平面区的编码值无法用一个16位来表示(16位最多能表示到码点为0xFFFF),所以Unicode标准规定:基本多语言平面(BMP)内,码点范围在U+D800到U+DFFF的值不对应于任何字符,称为代理区。这样,UTF-16利用保留下来的0xD800-0xDFFF区段的码点来对辅助平面内的字符的码点进行编码。
(2)在U+0000至U+D7FF以及从U+E000至U+FFFF的码点
第一个Unicode平面(BMP)的码点从U+0000至U+FFFF(除去代理区),包含了最常用的字符。这个范围内的码点的UTF-16编码数值等价于对应的码点,都是16位。
我们用U来表示码点,如果U<0x10000, U的UTF-16编码就是U对应的16位无符号整数(为书写简便,下文将16位无符号整数记作WORD)。
(3)从U+10000到U+10FFFF的码点
辅助平面(Supplementary Planes)中的码点,大于等于0x10000,在UTF-16中被编码为一对16比特长的码元(即32bit,4Bytes),称作理对(surrogate pair)。
如果码点U≥0x10000,先计算U'=U-0x10000,然后将U'(注意右上方有个撇)写成二进制形式:yyyy yyyy yyxx xxxx xxxx,然后在y前加上110110,在x前加上110111,则U的UTF-16编码(二进制)就是:110110yyyyyyyyyy 110111xxxxxxxxxx。
为什么U’可以被写成20个二进制位?Unicode的最大码点是0x10ffff,减去0x10000后,U'的最大值是0xfffff,所以肯定可以用20个二进制位表示。例如:Unicode编码0x20C30,减去0x10000后,得到0x10C30,写成二进制是:00010000110000110000。用前10位依次替代模板中的y,用后10位依次替代模板中的x,就得到:11011000010000111101110000110000,即0xD843 0xDC30。按照这个规则,如果Unicode编码在0x10000-0x10FFFF范围内的,则UTF-16编码有两个WORD,第一个WORD的高6位是110110,第二个WORD的高6位是110111。可见,第一个WORD的取值范围(二进制)是1101100000000000到1101101111111111,即0xD8000xDBFF。第二个WORD的取值范围(二进制)是1101110000000000到1101111111111111,即0xDC00-0xDFFF。它们和码点具体对应关系见下表。

通过代理区(Surrogate),我们很好地表示了U≥0x10000的码点,并且将一个WORD的UTF-16编码与两个WORD的UTF-16编码区分开了。
我们把D800-DB7F的范围称为高位代理(High Surrogates),意思就是代理区中的D800-DB7F是作为两个WORD的UTF-16编码的第一个WORD(高位部分的那个WORD);把DB80-DBFF的范围称为高位专用代理(High Private Use Surrogates);把DC00-DFFF的范围称为低位代理(Low Surrogates),意思就是代理区中的DC00-DFFF是作为两个WORD的UTF-16编码的第二个WORD(低位部分的那个WORD)。后来,由于高位代理比低位代理的值要小,为了避免混淆使用,Unicode标准现在称高位代理为前导代理(lead surrogates)。同样,由于低位代理比高位代理的值要大,所以为了避免混淆使用,Unicode标准现在称低位代理为后尾代理(trail surrogates)。
下面再说下高位专用代理。首先我们要来看下如何从UTF-16编码推导Unicode编码。
如果一个字符的UTF-16编码的第一个WORD在0xDB80到0xDBFF之间,那么它的Unicode编码在什么范围内?我们知道第二个WORD的取值范围是在pl=0xDC00-0xDFFF,所以这个字符的UTF-16编码范围应该是0xDB80 0xDC00到0xDBFF 0xDFFF。我们将这个范围写成二进制:
1101101110000000 1101110000000000-1101101111111111 1101111111111111
按照编码的相反步骤,取出高低WORD的后10位,然后拼在一起,得到
1110 0000 0000 0000 0000-1111 1111 1111 1111 1111
即0xE0000-0xFFFFF,按照编码的相反步骤再加上0x10000,得到0xF0000-0x10FFFF。这就是UTF-16编码的第一个WORD在0xDB80到0xDBFF之间的Unicode编码范围,即平面15和平面16。由于Unicode标准将平面15和平面16都作为专用区,所以0xDB80到0xDBFF之间的保留码点被称作高位专用代理。
下面讲述下UTF-16的字节序(字节存储次序)问题。
UTF-16的编码单元是16位,两个字节,这两个字节在传输和存储过程中,高低位位置不同,是不同的字符。比如,“田”的UTF-16编码是0x7530,但是如果存成0x3075,就变成了字符“ふ”,成了另外的字符。再比如‘奎’的UTF-16编码是594E, ‘乙’的UTF-16编码是4E59,如果我们收到UTF-16字节流594E,那么应该解释成“奎”还是“乙”?再如‘汉’字的Unicode编码是6C49。那么写到文件里时,究竟是将6C写在前面,还是将49写在前面?
UTF-8以字节为编码单元,没有字节序(字节存储次序)的问题。UTF-16以两个字节为编码单元,在解释一个UTF-16文本前,就要弄清楚每个编码单元的字节序(endian),字节序有两种:大端(Big-Endian)和小端(Little Endian),或称大尾和小尾。大端是指将一个数的高位字节存储在起始地址,数的其他部分再顺序存储;小端是指将一个数的低位字节存储在起始地址,数的其他部分再顺序存储。
例如,16位宽的数0x1234在小端模式CPU内存中的存放方式(假设从地址0x8000开始存放)为:

而在大端模式的CPU内存中的存放方式则为:

32位宽的数0x12345678在小端模式的CPU内存中的存放方式(假设从地址0x8000开始存放)为:

而在大端模式的CPU内存中的存放方式则为:

大端小端是由硬件决定的,和操作系统没关系。通常x86、ARM等硬件平台都是小端。
为了识别一个编码的字节序,Unicode标准建议用BOM(Byte Order Mark)来区分字节序,即在传输字节流前,先传输被作为BOM的字符,该字符的码点为U+FEFF,而它的相反FFFE在Unicode中是未定义的码位,所以两者结合起来可以分别表示字节序,即BOM字符在大端系统上的编码为FEFF;而在小端系统上的值则为FFFE。通常把BOM字符的编码放在文件开头,如果开头是FEFF,则说明该文件是以大端方式存储的UTF-16(UTF-16大端可以写成UTF-16BE)编码;如果文件开头是FFFE,则说明该文件是以小端方式存储的UTF-16(UTF-16小端可以写成UTF-16LE)编码。
数据传输过程也一样,如果接收者收到FEFF,就表明这个字节流是大端的;如果收到FFFE,就表明这个字节流是小端的。
UTF-8不需要BOM来表明字节顺序,但可以用BOM来表明编码方式,BOM的UTF-8编码为11101111 1011101110111111 (EFBBBF)。如果文件开头是EFBBBF,则说明该文件的编码是UTF-8;如果接收者收到以EFBBBF开头的字节流,也就知道这是UTF-8编码了。
在Windows的记事本上,选择“另存为”的时候,用户可以选择不同的编码选项,对应编码选项有 “ANSI”“Unicode”“Unicode big endian”“UTF-8”。其中,“Unicode”“Unicode big endian”对应的分别是UTF-16LE和UTF-16BE。可以做个试验,先选一个字,比如“海”,“海”的码点是U+6D77。在Windows下新建一个文本文档,输入“海”,然后选择菜单“另存为”,在另存为的时候选择编码方式为“Unicode big endian”,接着关闭文件。然后再用可以查看二进制码的文本工具(比如UltraEdit,但注意最好要用版本高一点的,比如版本21,版本太低会只会显示小端的情况,FF FE,比如UltraEdit版本11就是这样),打开后,选择二进制查看方式,然后可以看到内容为FE FF 6D 77,文件开头的两个字节是FE FF表示大端存储,6D 77就是“海”UTF16BE编码(因为码点小于等于0x10000,所以和码点一样,因为是大端,所以数据的高位字节6D存在低地址,即先存高位字节)。同样方式,我们再把文本文件改为小端方式(在记事本另存为时候选择编码为Unicode)存储,然后再用二进制查看,可以看到内容为:FF FE 77 6D,文件开头两个字节为FF FE,表示小端存储,77 6D为UTF-16LE编码,低位部分77存在低地址,即先存数据的低位字节;如果以UTF-8存放,则二进制查看的时候可以看到开头3个字节是EFBBBF。
3. UTF-32
UTF-32编码以32位无符号整数为单位。Unicode码点的UTF-32编码就是该码点值。UTF-32很简单,其编码和Unicode码点一一对应。根据字节序的不同,UTF-32也被实现为UTF-32LE或UTF32BE。BOM字符在UTF-32LE(UTF-32小端方式)的编码为FF FE 00 00, BOM字符在UTF-32BE(UTF-32大端方式)的编码为00 00 FE FF。
既然UTF-32最简单,那为什么很多系统不采用UTF-32呢?这是因为Unicode定义的范围太大了,实际使用中,99%的人使用的字符编码不会超过2个字节,如果统一用4个字节,数据冗余就非常大,会造成存储上的浪费和传输上的低效,因此16位是最好的。就算遇到超过16位能表示的字符,我们也可以通过上面讲到的代理技术,采用32位标识,这样的方案是最好的。现在主流操作系统实现Unicode方案还是采用的UTF-16或UTF-8的方案。比如Windows用的就是UTF16方案,而不少Linux用的就是UTF-8方案。
2.5.3 C运行时库对Unicode的支持
Unicode是后出来的东西,CRT库(C运行时库)为了支持Unicode,也定义了很多新的内容,现在的数据类型、API函数都分多字节字符版和宽字符版本。
(1)字符类型
C/C++新定义了一个名为“宽字符类型”的数据类型,以提供对Unicode的支持。这种数据类型为wchar_t,它的定义如下:
typdef unsigned short wchar_t;
从定义可以看出,wchar_t就是一个无符号短整型,用它来定义的字符,占用2个字节(16位),如:
wchar_t ch = ‘A'; //ch占用2个字节
原来的字符类型char仍然可以用,用char来定义的字符依旧占用一个字节(8位)。相对于wchar_t, char通常亦称为窄字符类型。
wchar_t可以用来定义字符,也可以用来定义字符串和字符数组:
wchar_t *psz = L"Unicode"; //定义字符串 wchar_t arr[] = L"Style"; //定义字符数组
其中,L要求编译器将其后的字符串按Unicode保存,即字符串中的每个字符占用2个字节,如果打印:
printf("%d, %d", sizeof(L"Unicode"), sizeof(arr));
可以发现结果是16,12。注意末尾的\0也要占用2个字节。
为了统一处理窄字符类型char和宽字符类型wchar_t,系统头文件tchar.h中定义了一个统一字符数据类型TCHAR,其定义大致如下:
#ifdef _UNICODE #define wchar_t TCHAR; #else typedef char TCHAR; #endif
意思就是如果定义了宏_UNICODE,则TCHAR就定义为wchar_t;如果没有定义_UNICODE,就定义TCHAR为char,定义一个通用形式的字符可以这样:
TCHAR ch = 'A'; //ch到底是窄字符还是宽字符,取决于前面有没有定义_UNICODE
(2)字符串处理
CRT库是C语言运行时库的简称,提供C语言函数的调用。有了宽字符类型wchar_t,字符串函数也有了相应的宽字符版本。比如求字符串长度函数strlen的宽字符版本是函数wcslen,计算方式两者相同,都是计算字符串中的字符个数。比如对上面两个字符串求长度,如果打印:
printf("%d, %d", wcslen(L"Unicode"), wcslen(arr));
结果是7,5。
除了求长度,其他字符串函数,都有相应的宽字符版本,为了让代码看起来统一,微软提供了一个系统文件tchar.h,在该函数中,对窄字符串函数和宽字符串函数进行统一处理,即提供一种函数形式来表示窄字符串函数或宽字符串函数,基本是将str换成了_tcs,最终代表哪一个,就要看是否定义了UNICODE宏。如果定义了UNICODE宏,则表示宽字符串函数,如果没有定义则表示窄字符串函数。比如求字符串长度的统一函数形式为_tcslen,在tchar.h中大致定义如下:
#ifdef _UNICODE #define _tcslen wcslen #else #define _tcslen strlen #endif
意思是如果定义了宏_UNICODE,则_tcslen就定义为wsclen,如果没有定义_UNICODE,就定义_tcslen为strlen。
此外,前面的宽字符串前有一个L,现在tchar.h中也对其进行统一处理,即用宏__T(x)来表示L,定义如下:
#ifdef _UNICODE #define __T(x) L##x #endif
意思就是,如果定义了宏_UNICODE, __T和L的功能相同,即后面的字符串是一个双字节字符串;如果没有定义,则后面字符串依旧是一个单字节字符串。另外,为了书写方便(2个下划线比较麻烦), __T(x)又被定义为:
#define _T(x) __T(x) #define _TEXT(x) __T(x)
__T(x)、_T(x)和_TEXT(x)三者功能相同,可以任选一。因此,定义一个通用形式的字符串可以这样:
TCHAR *p = _T("HELLO, boy");
2.5.4 C++标准库对Unicode的支持
C++标准库中的string,也有对应的宽字符版本wstring,但没有提供统一的函数形式,不过可以自己定义一个,如:
#ifdef _UNICODE #define tstr wstring #else #define tstr string #endif
然后在程序中使用tstr即可。类似的还有fstream/wfstream、ofstream/wofstream等,都有两个版本。
2.5.5 Windows API对Unicode的支持
Windows API函数也提供了两个版本,一个以A为结束的函数形式,针对多字节字符集;另外一个以W为结束的函数形式,针对Unicode字符集。比如显示信息框的API函数MessageBox,其实它是一个统一形式,如果没有定义UNICODE,则它就是MessageBoxA,里面的参数是单字节字符串;如果定义了UNICODE,则它就是MessageBoxW,里面的参数是双字节字符串。定义如下:
#ifdef UNICODE #define MessageBox MessageBoxW; #else typedef MessageBox MessageBoxA; #endif
其他Windows API函数都有如此形式的统一版本。
值得注意的是,从Windows 2000开始,Windows内部的核心函数都使用Uniocde字符串。如果调用一个Windows函数并传给它一个单字节字符串,那系统会先将字符串转换为Unicode字符串,然后再传给内核。这样转换会大大增加了系统开销,因此强烈建议在工程中使用Unicode字符集。
2.5.6 Visual C++ 2013开发环境对Unicode的支持
Visual C++开发环境支持两种字符集:多字节字符集和Unicode字符集。
新建一个Visual C++工程后,可以在工程属性里选择本工程所使用的字符集。但请一定要记住:此选项只控制代码里的数据类型和通用形式的Win32 API函数是用宽字符版的还是多字节字符版的,它控制不了代码里的字符是用Unicode编码还是多字节编码。如果选择了“使用Unicode字符集”,则代码里用到的API函数被解释为UNICODE版本的API(带标记W的API),如MessageBox被解释为MessageBoxW。如果选择了“使用多字节字符集”,则代码里用到的API函数被解释为多字节版本的API(带标记A的API),如MessageBox被解释为MessageBoxA。再比如对于代码中的宏_T,如果选择了Unicode字符集,则被解释成L,其后的字符串是双字节字符串;如果选择多字节字符集,则其后字符串是单字节字符串。
如果工程中使用了“多字节字符集”(就是系统预定义了宏_MBCS),则类型TCHAR将映射到char。如果工程中使用了“Unicode字符集”(就是系统预定义了宏_UNICODE),则类型TCHAR将映射到wchar_t。
到了Visual C++ 2013,会发现系统新建工程都是Unicode,工程向导中已经没有选择了。工程属性中如果没有多字节开发包的话,也是没法选择多字节字符集了。可见,微软建议大家直接用Unicode字符集开发软件。这是不是没有道理的,使用Unicode字符集开发热键的确好处颇多。比如:
(1)Unicode使程序的国际化变得更容易。
(2)Unicode提升了应用程序的效率,因为代码执行速度更快,占用内存更少。Windows内部的一切工作都是使用Unicode字符和字符串来进行的。所以,假如你非要传入ANSI字符或字符串,Windows就会被迫分配内存,并将ANSI字符或字符串转换为等价的Unicode形式。
(3)使用Unicode,你的应用程序能轻松调用所有的Windows函数,因为一些Windows函数提供了只能处理Unicode字符和字符串的版本。
(4)使用Unicode,你的代码很容易与COM集成(后者要求使用Unicode字符和字符串)。
(5)使用Unicode,你的代码很容易与.NET Framework集成(后者要求使用Unicode字符和字符串)。
这么多好处相信你以后决定使用Unicode字符集编程了,但如果你非要在Visual C++ 2013上使用多字节字符集编程,也是可以的。那就要安装多字节版本的MFC库Visual C++_mbcsmfc(这个可以网上免费下载)。下载下来直接双击安装即可,很傻瓜化,不再赘述。
安装完毕后,在工程的属性页的常规中就可以选择“使用多字节字符集”或“使用Unicode字符集”了,如图2-25所示。

图2-25
2.5.7 字符集相关范例
【例2.10】 求含中文字符的字符串的长度(字符数)
在Visual C++ 2013下新建一个控制台工程,使用Unicode字符集,求下列程序的运行结果。
#include "stdafx.h" #include "string" int _tmain(int argc, _TCHAR* argv[]) { char sz1[] = "aaa我"; TCHAR sz2[] = _T("aaa我"); printf("%s, ", sz1); setlocale(LC_ALL, "chs"); //设置区域语言为简体中文字符集 _tprintf(_T("%s\n"), sz2); printf("%d, %d\n", strlen(sz1), _tcslen(sz2)); return 0; }
其中,sz1是单字节字符数组,一个字节为一个字符单位,中文字符“我”占用2个字节,被当做两个字符,因此sz1的字符个数是5。
因为工程字符集是Unicode,所以TCHAR相当于wchar_t,因此sz2就是双字节字符数组,其中英文字符和中文字符都是占用2个字节空间,两个字节为一个字符单位,共有4个字符。
_tprintf相当于wprintf,但调用前需先调用setlocale来设置区域语言为简体中文字符集。
运行结果如图2-26所示。

图2-26
【例2.11】 Unicode下的wstring转string
主要利用API函数WideCharToMultiByte来转换。该函数声明如下:
int WideCharToMultiByte( UINT CodePage, DWORD dwFlags, LPWSTR lpWideCharStr, int cchWideChar, LPCSTR lpMultiByteStr, int cchMultiByte, LPCSTR lpDefaultChar, PBOOL pfUsedDefaultChar );
其中,参数CodePage指定执行转换的代码页,这个参数可以为系统已安装或有效的任何代码页所给定的值。也可以指定其为下面的任意一值:
CP_ACP:ANSI代码页 CP_MACCP:Macintosh代码页 CP_OEMCP:OEM代码页 CP_SYMBOL:符号代码页(42) CP_THREAD_ACP:当前线程ANSI代码页 CP_UTF7:使用UTF-7转换 CP_UTF8:使用UTF-8转换
dwFlags允许进行额外的控制,它会影响使用了读音符号(比如重音)的字符;lpWideCharStr指定要转换为宽字节字符串的缓冲区;cchWideChar指定由参数lpWideCharStr指向的缓冲区的字符个数;lpMultiByteStr指向接收被转换字符串的缓冲区;cchMultiByte指定由参数lpMultiByteStr指向的缓冲区最大值(用字节来计量)。若此值为零,函数返回lpMultiByteStr指向的目标缓冲区所必需的字节数,在这种情况下,lpMultiByteStr参数通常为NULL; lpDefaultChar的作用是如果遇到一个不能转换的宽字符,函数便会使用lpDefaultChar参数指向的字符;pfUsedDefaultChar的作用是当至少有一个字符不能转换为其多字节形式,函数就会把这个变量设为TRUE。如果函数运行成功,并且cchMultiByte不为零,返回值是由lpMultiByteStr指向的缓冲区中写入的字节数;如果函数运行成功,并且cchMultiByte为零,返回值是接收到待转换字符串的缓冲区所必需的字节数;如果函数运行失败,返回值为零。若想获得更多错误信息,可以调用GetLastError函数。注意:指针lpMultiByteStr和lpWideCharStr必须不一样。如果一样,函数将失败,GetLastError将返回ERROR_INVALID_PARAMETER的值。
步骤如下:
(1)打开Visual C++ 2013,新建一个控制台,设置字符集为Unicode。
(2)在Test.cpp中输入代码如下:
#include "stdafx.h" #include "windows.h" #include "string" using namespace std; //将宽字符串转为窄字符串 string WideCharToMultiChar(wstring str) { string strRes; //定义结果字符串 //获取缓冲区的大小,缓冲区大小是按字节计算的 int len = WideCharToMultiByte(CP_ACP, 0, str.c_str(), str.size(), NULL, 0, NULL, NULL); char *buffer = new char[len + 1]; //申请空间 WideCharToMultiByte(CP_ACP, 0, str.c_str(), str.size(), buffer, len, NULL, NULL); //转换 buffer[len] = '\0'; strRes.append(buffer); //加入string中 delete[] buffer; //删除缓冲区 return strRes; //返回结果 } int _tmain(int argc, _TCHAR* argv[]) { TCHAR sz2[] = _T("aaa我"); wstring wstr; wstr.append(sz2); string str = WideCharToMultiChar(wstr); printf("%s\n", str.c_str()); //显示结果 }
(3)保存工程并运行,运行结果如图2-27所示。

图2-27
【例2.12】 找出wchar_t类型的数组里的汉字
wchar_t类型的数组中的每个字符占用2个字节,其中非中文字符的高字节为0,以此可以判断是否为汉字。步骤如下:
(1)新建一个控制台工程。
(2)在Test.cpp中输入代码如下:
#include "stdafx.h" #include "windows.h" #include "string" //函数setlocale需要 #include "iostream" using namespace std; int _tmain(int argc, _TCHAR* argv[]) { wstring str; wchar_t arr[] = L"中国woaini江345苏"; int i; i = 0; while (arr[i]) { if (arr[i] >> 7) //判断高字节是否为0,如果不是,则为汉字 str.push_back(arr[i]); i++; } setlocale(LC_ALL, "chinese-simplified"); // 设置区域语言为简体中文 wcout << str<<endl; //输出找到的中文 return 0; }
在上面的代码中,wcout是cout的宽字符版本,在调用它之前,先要调用setlocale来设置区域语言为简体中文,也可以写成setlocale(LC_ALL, "chs");。
(3)保存工程并运行,运行结果如图2-28所示。

图2-28
【例2.13】 找出char类型的数组里的汉字
char类型的数组中的每个字符占用1个字节,其中非中文字符的高字节为0,以此可以判断是否为汉字。步骤如下:
(1)新建一个控制台工程,工程字符集为Unicode,当然也可以是多字节。
(2)在Test.cpp中输入代码如下:
#include "stdafx.h" #include "string" #include "iostream" using namespace std; int _tmain(int argc, _TCHAR* argv[]) { char sz1[] = "a世界a1a都去asdfad哪啦"; string str; int i, len = strlen(sz1); //得到字符数组长度 for (int i = 0; i < len; ) { if (sz1[i] < 0) //负数则前后两个字节存的是汉字 { str.push_back(sz1[i]); i++; str.push_back(sz1[i]); } i++; } cout << str << endl; //输出找到的汉字 return 0; }
(3)保存工程并运行,运行结果如图2-29所示。

图2-29