2字符和字符串处理.docx
《2字符和字符串处理.docx》由会员分享,可在线阅读,更多相关《2字符和字符串处理.docx(28页珍藏版)》请在冰豆网上搜索。
2字符和字符串处理
第2章Unicode
随着Microsoft公司的Windows操作系统在全世界日益广泛的流行,对于软件开发人员来说,将目标瞄准国际上的各个不同市场,已经成为一个越来越重要的问题。
美国的软件版本比国际版本提前6个月推向市场,这曾经是个司空见惯的现象。
但是,由于各国对Windows操作系统提供了越来越多的支持,因此就更加容易为国际市场生产各种应用软件,从而缩短了软件的美国版本与国际版本推出的时间间隔。
Windows操作系统始终不逾地提供各种支持,以帮助软件开发人员进行应用程序的本地化工作。
应用软件可以从各种不同的函数中获得特定国家的信息,并可观察控制面板的设置,以确定用户的首选项。
Windows甚至支持不同的字体,以适应应用的需要。
之所以将这一章放在本书的开头,是因为考虑到Unicode是开发任何应用程序时要采用的基本步骤。
本书的每一章中几乎都要讲到关于Unicode的问题,而且书中给出的所有示例应用程序都是“用Unicode实现的”。
如果你为MicrosoftWindows2000或MicrosoftWindowsCE开发应用程序,你应该使用Unicode进行开发。
如果你为MicrosoftWindows98开发应用程序,你必须对某些问题作出决定。
本章也要讲述Windows98的有关问题。
2.1字符集
软件的本地化要解决的真正问题,实际上就是如何来处理不同的字符集。
多年来,许多人一直将文本串作为一系列单字节字符来进行编码,并在结尾处放上一个零。
对于我们来说,这已经成了习惯。
当调用strlen函数时,它在以0结尾的单字节字符数组中返回字符的数目。
问题是,有些文字和书写规则(比如日文中的汉字就是个典型的例子)的字符集中的符号太多了,因此单字节(它提供的符号最多不能超过256个)是根本不敷使用的。
为此出现了双字节字符集(DBCS),以支持这些文字和书写规则。
2.1.1单字节与双字节字符集
在双字节字符集中,字符串中的每个字符可以包含一个字节或包含两个字节。
例如,日文中的汉字,如果第一个字符在0x81与0x9F之间,或者在0xE0与0xFC之间,那么就必须观察下一个字节,才能确定字符串中的这个完整的字符。
使用双字节字符集,对于程序员来说简直是个很大的难题,因为有些字符只有一个字节宽,而有些字符则是两个字节宽。
如果只是调用strlen函数,那么你无法真正了解字符串中究竟有多少字符,它只能告诉你到达结尾的0之前有多少个字节。
ANSI的C运行期库中没有配备相应的函数,使你能够对双字节字符集进行操作。
但是,MicrosoftVisualC++的运行期库却包含许多函数,如_mbslen,它可以用来操作多字节(既包括单字节也包括双字节)字符串。
为了帮助你对DBCS字符串进行操作,Windows提供了下面的一组帮助函数(见表2-1)。
前两个函数CharNext和CharPrev允许前向或逆向遍历DBCS字符串,方法是每次一个字符。
第三个函数IsDBCSLeadByte,在字节返回到一个两字字节符的第一个字节时将返回TRUE。
表2-1对DBCS字符串进行操作的帮助函数
函数
描述
PTSTRCharNext(PCTSTRpszCurrentChar);
返回字符串中的下一个字符的地址
PTSTRCharPrev(PCTSTRpszStart,PCTSTRpszCurrentChar);
返回字符串中的上一个字符的地址
BOOLIsDBCSLeadByteTRUE(BYTEbTestChar);
如果该字节是DBCS字符的第一个字节,则返回
尽管这些函数使得我们对DBCS的操作更容易,但还需要,一个更好的方法让我们来看看Unicode。
2.1.2Unicode:
宽字节字符集
Unicode是Apple和Xerox公司于1988年建立的一个技术标准。
1991年,成立了一个集团机构负责Unicode的开发和推广应用。
该集团由Apple、Compaq、HP、IBM、Microsoft、Oracle、SiliconGraphics,Inc.、Sybase、Unisys和Xerox等公司组成(若要了解该集团的全部成员,请通过网址www.Unicode.org查找)。
该集团负责维护Unicode标准。
Unicode的完整描述可以参阅AddisonWesley出版的《UnicodeStandard》一书(该书可以通过网址www.Unicode.org订购)。
Unicode提供了一种简单而又一致的表示字符串的方法。
Unicode字符串中的所有字符都是16位的(两个字节)。
它没有专门的字节来指明下一个字节是属于同一个字符的组成部分,还是一个新字符。
这意味着你只需要对指针进行递增或递减,就可以遍历字符串中的各个字符,不再需要调用CharNext、CharPrev和IsDBCSLeadByte之类的函数。
由于Unicode用一个16位的值来表示每个字符,因此总共可以得到65000个字符,这样,它就能够对世界各国的书面文字中的所有字符进行编码,远远超过了单字节字符集的256个字符的数目。
目前,已经为阿拉伯文、中文拼音、西里尔字母(俄文)、希腊文、西伯莱文、日文、韩文和拉丁文(英文)字母定义了Unicode代码点。
(代码点是字符集中符号的位置。
)这些字符集中还包含了大量的标点符号、数学符号、技术符号、箭头、装饰标志、区分标志和其他许多字符。
如果将所有这些字母和符号加在一起,总计约达35000个不同的代码点,这样,总计65000多个代码点中,大约还有一半可供将来扩充时使用。
这65536个字符可以分成不同的区域。
表2-2显示了这样的区域的一部分以及分配给这些区域的字符。
表2-2区域字符
16位代码
字符
16位代码
字符
0000-007F
ASCII
0300-036F
通用区分标志
0080-00FF
拉丁文1字符
0400-04FF
西里尔字母
0100-017F
欧洲拉丁文
0530-058F
亚美尼亚文
0180-01FF
扩充拉丁文
0590-05FF
西伯莱文
0250-02AF
标准拼音
0600-06FF
阿拉伯文
02B0-02FF
修改型字母
0900-097F
梵文
目前尚未分配的代码点大约还有29000个,不过它们是保留供将来使用的。
另外,大约有6000个代码点是保留供个人使用的。
2.2为什么使用Unicode
当开发应用程序时,当然应该考虑利用Unicode的优点。
即使现在你不打算对应用程序进行本地化,开发时将Unicode放在心上,肯定可以简化将来的代码转换工作。
此外,Unicode还具备下列功能:
•可以很容易地在不同语言之间进行数据交换。
•使你能够分配支持所有语言的单个二进制.exe文件或DLL文件。
•提高应用程序的运行效率(本章后面还要详细介绍)。
2.3Windows2000与Unicode
Windows2000是使用Unicode从头进行开发的,用于创建窗口、显示文本、进行字符串操作等的所有核心函数都需要Unicode字符串。
如果调用任何一个Windows函数并给它传递一个ANSI字符串,那么系统首先要将字符串转换成Unicode,然后将Unicode字符串传递给操作系统。
如果希望函数返回ANSI字符串,系统就会首先将Unicode字符串转换成ANSI字符串,然后将结果返回给你的应用程序。
所有这些转换操作都是在你看不见的情况下发生的。
当然,进行这些字符串的转换需要占用系统的时间和内存。
例如,如果调用CreateWindowEx函数,并传递类名字和窗口标题文本的非Unicode字符串,那么CreateWindowEx必须分配内存块(在你的进程的默认堆中),将非Unicode字符串转换成Unicode字符串,并将结果存储在分配到的内存块中,然后调用Unicode版本的CreateWindowEx函数。
对于用字符串填入缓存的函数来说,系统必须首先将Unicode字符串转换成非Unicode字符串,然后你的应用程序才能处理该字符串。
由于系统必须执行所有这些转换操作,因此你的应用程序需要更多的内存,并且运行的速度比较慢。
通过从头开始用Unicode来开发应用程序,就能够使你的应用程序更加有效地运行。
2.4Windows98与Unicode
Windows98不是一种全新的操作系统。
它继承了16位Windows操作系统的特性,它不是用来处理Unicode的。
如果要增加对Unicode的支持,其工作量非常大,因此在该产品的特性列表中没有包括这个支持项目。
由于这个原因,Windows98像它的前任产品一样,几乎都是使用ANSI字符串来进行所有的内部操作的。
仍然可以编写用于处理Unicode字符和字符串的Windows应用程序,不过,使用Windows函数要难得多。
例如,如果想要调用CreateWindowEx函数并将ANSI字符串传递给它,这个调用的速度非常快,不需要从你进程的默认堆栈中分配缓存,也不需要进行字符串转换。
但是,如果想要调用CreateWindowEx函数并将Unicode字符串传递给它,就必须明确分配缓存,并调用函数,以便执行从Unicode到ANSI字符串的转换操作。
然后可以调用CreateWindowEx,传递ANSI字符串。
当CreateWindowEx函数返回时,就能释放临时缓存。
这比使用Windows2000上的Unicode要麻烦得多。
本章的后面要介绍如何在Windows98下进行这些转换。
虽然大多数Unicode函数在Windows98中不起任何作用,但是仍有少数Unicode函数确实非常有用。
这些函数是:
■EnumResourceLanguagesW
■GetTextExtentPoint32W
■EnumResourceNamesW
■GetTextExtentPointW
■EnumResourceTypesW
■LstrlenW
■ExtTextOutW
■MessageBoxExW
■FindResourceW
■MessageBoxW
■FindResourceExW
■TextOutW
■GetCharWidthW
■WideCharToMultiByte
■GetCommandLineW
■MultiByteToWideChar
可惜的是,这些函数中有许多函数在Windows98中会出现各种各样的错误。
有些函数无法使用某些字体,有些函数会破坏内存堆栈,有些函数会使打印机驱动程序崩溃,等等。
如果要使用这些函数,必须对它们进行大量的测试。
即使这样,可能仍然无法解决问题。
因此必须向用户说明这些情况。
2.5WindowsCE与Unicode
WindowsCE操作系统是为小型设备开发的,这些设备的内存很小,并且不带磁盘存储器。
你可能认为,由于Microsoft公司的主要目标是建立一种尽可能小的操作系统,因此它会使用ANSI作为自己的字符集。
但是Microsoft公司并非鼠目寸光,他们懂得,WindowsCE的设备要在世界各地销售,他们希望降低软件开发成本,这样就能更加容易地开发应用程序。
为此,WindowsCE本身就是使用Unicode的一种操作系统。
但是,为了使WindowsCE尽量做得小一些,Microsoft公司决定完全不支持ANSIWindows函数。
因此,如果要为WindowsCE开发应用程序,必须懂得Unicode,并且在整个应用程序中使用Unicode。
2.6需要注意的问题
下面让我们进一步明确一下“Microsoft公司对Unicode支持的情况”:
•Windows2000既支持Unicode,也支持ANSI,因此可以为任意一种开发应用程序。
•Windows98只支持ANSI,只能为ANSI开发应用程序。
•WindowsCE只支持Unicode,只能为Unicode开发应用程序。
虽然Microsoft公司试图让软件开发人员能够非常容易地开发在这3种平台上运行的软件,但是Unicode与ANSI之间的差异使得事情变得困难起来,并且这种差异通常是我遇到的最大的问题之一。
请不要误解,Microsoft公司坚定地支持Unicode,并且我也坚决鼓励你使用它。
不过你应该懂得,你可能遇到一些问题,需要一定的时间来解决这些问题。
建议你尽可能使用Unicode。
如果运行Windows98,那么只有在必要时才需转换到ANSI。
不过,还有另一个小问题你应该了解,那就是COM。
2.7对COM的简单说明
当Microsoft公司将COM从16位Windows转换成Win32时,公司作出了一个决定,即需要字符串的所有COM接口方法都只能接受Unicode字符串。
这是个了不起的决定,因为COM通常用于使不同的组件能够互相进行通信,而Unicode则是传递字符串的最佳手段。
如果你为Windows2000或WindowsCE开发应用程序,并且也使用COM,那么你将会如虎添翼。
在你的整个源代码中使用Unicode,将使与操作系统进行通信和与COM对象进行通信的操作变成一件轻而易举的事情。
如果你为Windows98开发应用程序,并且也使用COM,那么将会遇到一些问题。
COM要求使用Unicode字符串,而操作系统的大多数函数要求使用ANSI字符串。
那是多么难办的事情啊!
我曾经从事过若干个项目的开发,在这些项目中,我编写了许多代码,仅仅是为了来回进行字符串的转换。
2.8如何编写Unicode源代码
Microsoft公司为Unicode设计了WindowsAPI,这样,可以尽量减少对你的代码的影响。
实际上,你可以编写单个源代码文件,以便使用或者不使用Unicode来对它进行编译。
只需要定义两个宏(UNICODE和_UNICODE),就可以修改然后重新编译该源文件。
2.8.1C运行期库对Unicode的支持
为了利用Unicode字符串,定义了一些数据类型。
标准的C头文件String.h已经作了修改,以便定义一个名字为wchar_t的数据类型,它是一个Unicode字符的数据类型:
typedefunsignedshortwchar_t;
例如,如果想要创建一个缓存,用于存放最多为99个字符的Unicode字符串和一个结尾为零的字符,可以使用下面这个语句:
wchar_tszBuffer[100];
该语句创建了一个由100个16位值组成的数组。
当然,标准的C运行期字符串函数,如strcpy、strchr和strcat等,只能对ANSI字符串进行操作,不能正确地处理Unicode字符串。
因此,ANSIC也拥有一组补充函数。
清单2-1显示了一些标准的ANSIC字符串函数,后面是它们的等价Unicode函数。
char*strcat(char*,constchar*);
wchar_t*wcscat(wchar_t*,constwchar_t*);
清单2-1标准的ANSIC字符串函数和它们的等价Unicode函数
char*strchr(constchar*,int);
wchar_t*wcschr(constwchar_t*,wchar_t);
intstrcmp(constchar*,constchar*);
intwcscmp(constwchar_t*,constwchar_t*);
char*strcpy(char*,constchar*);
wchar_t*wcscpy(wchar_t*,constwchar_t*);
size_tstrlen(constchar*);
size_twcslen(constwchar_t*);
请注意,所有的Unicode函数均以wcs开头,wcs是宽字符串的英文缩写。
若要调用Unicode函数,只需用前缀wcs来取代ANSI字符串函数的前缀str即可。
注意大多数软件开发人员可能已经不记得这样一个非常重要的问题了,那就是Microsoft公司提供的C运行期库与ANSI的标准C运行期库是一致的。
ANSIC规定,C运行期库支持Unicode字符和字符串。
这意味着始终都可以调用C运行期函数,以便对Unicode字符和字符串进行操作,即使是在Windows98上运行,也可以调用这些函数。
换句话说,wcscat、wcslen和wcstok等函数都能够在Windows98上很好地运行,这些都是必须关心的操作系统函数。
对于包含了对str函数或wcs函数进行显式调用的代码来说,无法非常容易地同时为ANSI和Unicode对这些代码进行编译。
本章前面说过,可以创建同时为ANSI和Unicode进行编译的单个源代码文件。
若要建立双重功能,必须包含TChar.h文件,而不是包含String.h文件。
TChar.h文件的唯一作用是帮助创建ANSI/Unicode通用源代码文件。
它包含你应该用在源代码中的一组宏,而不应该直接调用str函数或者wcs函数。
如果在编译源代码文件时定义了UNICODE,这些宏就会引用wcs这组函数。
如果没有定义_UNICODE,那么这些宏将引用str这组宏。
例如,在TChar.h中有一个宏称为_tcscpy。
如果在包含该头文件时没有定义_UNICODE,那么_tcscpy就会扩展为ANSI的strcpy函数。
但是如果定义了_UNICODE,_tcscpy将扩展为Unicode的wcscpy函数。
拥有字符串参数的所有C运行期函数都在TChar.h文件中定义了一个通用宏。
如果使用通用宏,而不是ANSI/Unicode的特定函数名,就能够顺利地创建可以为ANSI或Unicode进行编译的源代码。
但是,除了使用这些宏之外,还有一些操作是必须进行的。
TChar.h文件包含了另外一些宏.若要定义一个ANSI/Unicode通用的字符串数组,请使用下面的TCHAR数据类型。
如果定义了_UNICODE,TCHAR将声明为下面的形式:
typedefwchar_tTCHAR;
如果没有定义_UNICODE,则TCHAR将声明为下面的形式:
typedefcharTCHAR;
使用该数据类型,可以像下面这样分配一个字符串:
TCHARszString[100];
也可以创建对字符串的指针:
TCHAR*szError="Error";
不过上面这行代码存在一个问题。
按照默认设置,Microsoft公司的C++编译器能够编译所有的字符串,就像它们是ANSI字符串,而不是Unicode字符串。
因此,如果没有定义_UNICODE,该编译器将能正确地编译这一行代码。
但是,如果定义了_UNICODE,就会产生一个错误。
若要生成一个Unicode字符串而不是ANSI字符串,必须将该代码行改写为下面的样子:
TCHAR*szError=L"Error";
字符串(literalstring)前面的大写字母L,用于告诉编译器该字符串应该作为Unicode字符串来编译。
当编译器将字符串置于程序的数据部分中时,它在每个字符之间分散插入零字节。
这种变更带来的问题是,现在只有当定义了_UNICODE时,程序才能成功地进行编译。
我们需要另一个宏,以便有选择地在字符串