第2章U n i c o d e

随着M i c r o s o f t公司的Wi n d o w s操作系统在全世界日益广泛的流行,对于软件开发人员来说,将目标瞄准国际上的各个不同市场,已经成为一个越来越重要的问题。美国的软件版本比国际版本提前6个月推向市场,这曾经是个司空见惯的现象。但是,由于各国对Wi
n d o w s操作系统提供了越来越多的支持,因此就更加容易为国际市场生产各种应用软件,从而缩短了软件的美国版本与国际版本推出的时间间隔。

Wi n d o w s操作系统始终不逾地提供各种支持,以帮助软件开发人员进行应用程序的本地化工作。应用软件可以从各种不同的函数中获得特定国家的信息,并可观察控制面板的设置,以确定用户的首选项。Wi n d o w s甚至支持不同的字体,以适应应用的需要。

之所以将这一章放在本书的开头,是因为考虑到
U n i c o d e是开发任何应用程序时要采用的基本步骤。本书的每一章中几乎都要讲到关于U n i c o d e的问题,而且书中给出的所有示例应用程序都是“用U
n i c o d e实现的”。如果你为Microsoft Windows 2000或Microsoft
Windows CE开发应用程序,你应该使用U n i c o d e进行开发。如果你为Microsoft Windows 98开发应用程序,你必须对某些问题作出决定。本章也要讲述Windows
98的有关问题。

2.1
字符集

软件的本地化要解决的真正问题,实际上就是如何来处理不同的字符集。多年来,许多人一直将文本串作为一系列单字节字符来进行编码,并在结尾处放上一个零。对于我们来说,这已经成了习惯。当调用s t r l e n函数时,它在以0结尾的单字节字符数组中返回字符的数目。

问题是,有些文字和书写规则(比如日文中的汉字就是个典型的例子)的字符集中的符号太多了,因此单字节(它提供的符号最多不能超过2 5 6个)是根本不敷使用的。为此出现了双字节字符集(D B C S)
,以支持这些文字和书写规则。

2.1.1 单字节与双字节字符集

在双字节字符集中,字符串中的每个字符可以包含一个字节或包含两个字节。例如,日文中的汉字,如果第一个字符在0 x 8 1与0 x 9 F之间,或者在0
x E 0与0 x F C之间,那么就必须观察下一个字节,才能确定字符串中的这个完整的字符。使用双字节字符集,对于程序员来说简直是个很大的难题,因为有些字符只有一个字节宽,而有些字符则是两个字节宽。

如果只是调用s t r l e n函数,那么你无法真正了解字符串中究竟有多少字符,它只能告诉你到达结尾的0之前有多少个字节。A
N S I的C运行期库中没有配备相应的函数,使你能够对双字节字符集进行操作。但是,Microsoft
Visual C++的运行期库却包含许多函数,如_ m b s l e n ,它可以用来操作多字节(既包括单字节也包括双字节)字符串。

为了帮助你对D B C S字符串进行操作,Wi n d o w s提供了下面的一组帮助函数(见表2
- 1 )。

前两个函数CharNext和Char Prev允许前向或逆向遍历DBCS字符串,方法是每次一个字符。第三个函数
IsDBCSLeadByte, 在字节返回到一个两字字节符的第一个字节时将返回T R U E。

尽管这些函数使得我们对 D B C S的操作更容易,但还需要,一个更好的方法让我们来看看U n i c
o d e。

2.1.2 Unicode:宽字节字符集

U n i c o d e是A p p l e和X e r o x公司于1
9 8 8年建立的一个技术标准。 1 9 9 1年,成立了一个集团机构负责U n i c o d e的开发和推广应用。该集团由A
p p l e、C o m p a q、H P、I
B M、M i c r o s o f t、O r a c l e、Silicon
Graphics, Inc.、S y b a s e、U n i s y s和X
e r o x等公司组成(若要了解该集团的全部成员,请通过网址w w w. U n i c o d e . o rg查找) 。该集团负责维护U n i c o d e标准。U
n i c o d e的完整描述可以参阅A d d i s o n We s l e y出版的《Unicode Standard》一书(该书可以通过网址w
w w. U n i c o d e . o rg订购) 。

U n i c o d e提供了一种简单而又一致的表示字符串的方法。U n i c o d e字符串中的所有字符都是1 6位的(两个字节)
。它没有专门的字节来指明下一个字节是属于同一个字符的组成部分,还是一个新字符。这意味着你只需要对指针进行递增或递减,就可以遍历字符串中的各个字符,不再需要调用C h a r N e x t、C h a r P r e v和I
s D B C S L e a d B y t e之类的函数。由于U n i c o d e用一个1 6位的值来表示每个字符,因此总共可以得到65
000个字符,这样,它就能够对世界各国的书面文字中的所有字符进行编码,远远超过了单字节字符集的2 5 6个字符的数目。

目前,已经为阿拉伯文、中文拼音、西里尔字母(俄文) 、希腊文、西伯莱文、日文、韩文和拉丁文(英文)字母定义了U n i c o d e代码点 。这些字符集中还包含了大量的标点符号、数学符号、技术符号、箭头、装饰标志、区分标志和其他许多字符。如果将所有这些字母和符号加在一起,总计约达3
5 0 0 0个不同的代码点,这样,总计65 000多个代码点中,大约还有一半可供将来扩充时使用。

这65 536个字符可以分成不同的区域。表2-2
显示了这样的区域的一部分以及分配给这些区域的字符。

目前尚未分配的代码点大约还有29 000个,不过它们是保留供将来使用的。另外,大约有6 0 0 0个代码点是保留供个人使用的。

2.2
为什么使用U n i c o d e

当开发应用程序时,当然应该考虑利用
U n i c o d e的优点。即使现在你不打算对应用程序进行本地化,开发时将U n i c o d e放在心上,肯定可以简化将来的代码转换工作。此外,U n i c o d e还

具备下列功能:

• 可以很容易地在不同语言之间进行数据交换。

• 使你能够分配支持所有语言的单个二进制. e x e文件或D L L文件。

• 提高应用程序的运行效率(本章后面还要详细介绍) 。

2.3 Windows 2000与U n i c o d e

Windows 2000是使用U n i c o d e从头进行开发的,用于创建窗口、显示文本、进行字符串操作等的所有核心函数都需要U
n i c o d e字符串。如果调用任何一个Wi n d o w s函数并给它传递一个A N S I字符串,那么系统首先要将字符串转换成U
n i c o d e,然后将U n i c o d e字符串传递给操作系统。如果希望函数返回A N S I字符串,系统就会首先将U
n i c o d e字符串转换成A N S I字符串,然后将结果返回给你的应用程序。所有这些转换操作都是在你看不见的情况下发生的。当然,进行这些字符串的转换需要占用系统的时间和内存。

例如,如果调用C r e a t e Wi n d o w E x函数,并传递类名字和窗口标题文本的非U n i c o d e字符串,那么C
r e a t e Wi n d o w E x必须分配内存块(在你的进程的默认堆中) ,将非U n i c o d e字符串转换成U n i c o d e字符串,并将结果存储在分配到的内存块中,然后调用U
n i c o d e版本的C r e a t e Wi n d o w E x函数。

对于用字符串填入缓存的函数来说,系统必须首先将
U n i c o d e字符串转换成非U n i c o d e字符串,然后你的应用程序才能处理该字符串。由于系统必须执行所有这些转换操作,因此你的应用程序需要更多的内存,并且运行的速度比较慢。通过从头开始用U
n i c o d e来开发应用程序,就能够使你的应用程序更加有效地运行。

2.4 Windows 98与U n i c o d e

Windows 98不是一种全新的操作系统。它继承了1 6位Wi n d o w s操作系统的特性,它不是用来处理U
n i c o d e的。如果要增加对U n i c o d e的支持,其工作量非常大,因此在该产品的特性列表中没有包括这个支持项目。由于这个原因,Windows 98像它的前任产品一样,几乎都是使用A
N S I字符串来进行所有的内部操作的。

仍然可以编写用于处理U n i c o d e字符和字符串的Wi n d o w s应用程序,不过,使用Wi
n d o w s函数要难得多。例如,如果想要调用 C r e a t e Wi n d o w E x函数并将A N S I字符串传递给它,这个调用的速度非常快,不需要从你进程的默认堆栈中分配缓存,也不需要进行字符串转换。但是,如果想要调用C
r e a t e Wi n d o w E x函数并将U n i c o d e字符串传递给它,就必须明确分配缓存,并调用函数,以便执行从U n i c o d e到A
N S I字符串的转换操作。然后可以调用 C r e a t e Wi n d o w E x,传递A N S I字符串。当C
r e a t e Wi n d o w E x函数返回时,就能释放临时缓存。这比使用Windows 2000上的U n i c o d e要麻烦得多。本章的后面要介绍如何在Windows
98下进行这些转换。

虽然大多数U n i c o d e函数在Windows 98中不起任何作用,但是仍有少数U
n i c o d e函数确实非常有用。这些函数是:

可惜的是,这些函数中有许多函数在Windows 98中会出现各种各样的错误。有些函数无法使用某些字体,有些函数会破坏内存堆栈,有些函数会使打印机驱动程序崩溃,等等。如果要使用这些函数,必须对它们进行大量的测试。即使这样,可能仍然无法解决问题。因此必须向用户说明这些情况。

2.5 Windows CE与U n i c o d e

Windows CE操作系统是为小型设备开发的,这些设备的内存很小,并且不带磁盘存储器。你可能认为,由于M i c r o s o f t公司的主要目标是建立一种尽可能小的操作系统,因此它会使用A
N S I作为自己的字符集。但是M i c r o s o f t公司并非鼠目寸光,他们懂得,Windows CE的设备要在世界各地销售,他们希望降低软件开发成本,这样就能更加容易地开发应用程序。为此,Windows
CE本身就是使用U n i c o d e的一种操作系统。

但是,为了使Windows CE尽量做得小一些,M i c r o s o f t公司决定完全不支持ANSI
Wi n d o w s函数。因此,如果要为Windows CE开发应用程序,必须懂得U n i c o d e,并且在整个应用程序中使用U
n i c o d e。

2.6
需要注意的问题

下面让我们进一步明确一下“M i c r o s o f t公司对U n i c o d e支持的情况”:

• Windows 2000既支持U n i c o d e,也支持A N S I,因此可以为任意一种开发应用程序。

• Windows 98只支持A N S I,只能为A N S I开发应用程序。

• Windows CE只支持U n i c o d e,只能为U n i c o d e开发应用程序。

虽然M i c r o s o f t公司试图让软件开发人员能够非常容易地开发在这3种平台上运行的软件,但是U
n i c o d e与A N S I之间的差异使得事情变得困难起来,并且这种差异通常是我遇到的最大的问题之一。请不要误解,M i c r o s o f t公司坚定地支持U
n i c o d e,并且我也坚决鼓励你使用它。不过你应该懂得,你可能遇到一些问题,需要一定的时间来解决这些问题。建议你尽可能使用U n i c o d e。如果运行Windows 98,那么只有在必要时才需转换到A
N S I。不过,还有另一个小问题你应该了解,那就是C O M。

2.7
对C O M的简单说明

当M i c r o s o f t公司将C O M从1
6位Wi n d o w s转换成Wi n 3 2时,公司作出了一个决定,即需要字符串的所有C
O M接口方法都只能接受U n i c o d e字符串。这是个了不起的决定,因为C O M通常用于使不同的组件能够互相进行通信,而U
n i c o d e则是传递字符串的最佳手段。如果你为Windows 2000或Windows CE开发应用程序,并且也使用C
O M,那么你将会如虎添翼。在你的整个源代码中使用U n i c o d e,将使与操作系统进行通信和与C O M对象进行通信的操作变成一件轻而易举的事情。

如果你为Windows 98开发应用程序,并且也使用C O M,那么将会遇到一些问题。C
O M要求使用U n i c o d e字符串,而操作系统的大多数函数要求使用A N S I字符串。那是多么难办的事情啊!我曾经从事过若干个项目的开发,在这些项目中,我编写了许多代码,仅仅是为了来回进行字符串的转换。

2.8
如何编写U n i c o d e源代码

M i c r o s o f t公司为U n i c o d e设计了Windows API,这样,可以尽量减少对你的代码的影响。实际上,你可以编写单个源代码文件,以便使用或者不使用U
n i c o d e来对它进行编译。只需要定义两个宏(U N I C O D E和_ U N I C O D E)
,就可以修改然后重新编译该源文件。

2.8.1 C运行期库对U n i c o d e的支持

为了利用U n i c o d e字符串,定义了一些数据类型。标准的C头文件S
t r i n g . h已经作了修改,以便定义一个名字为w c h a r _ t的数据类型,它是一个U n i c o d e字符的数据类型:

typedef unsigned short wchart_t;

例如,如果想要创建一个缓存,用于存放最多为
9 9个字符的U n i c o d e字符串和一个结尾为零的字符,可以使用下面这个语句:

wchar_t szBuffer[100];

该语句创建了一个由 1 0 0个1 6位值组成的数组。当然,标准的C运行期字符串函数,如s
t r c p y、s t r c h r和s t r c a t等,只能对A
N S I字符串进行操作,不能正确地处理U n i c o d e字符串。因此,ANSI C也拥有一组补充函数。清单2
- 1显示了一些标准的ANSI C字符串函数,后面是它们的等价U n i c o d e函数。

请注意,所有的U n i c o d e函数均以w c s开头,w
c s是宽字符串的英文缩写。若要调用U n i c o d e函数,只需用前缀w c s来取代A
N S I字符串函数的前缀s t r即可。

注意 大多数软件开发人员可能已经不记得这样一个非常重要的问题了,那就是

M i c r o s o f t公司提供的C运行期库与A N S I的标准C运行期库是一致的。ANSI
C规定,C运行期库支持U n i c o d e字符和字符串。这意味着始终都可以调用C运行期函数,以便对U
n i c o d e字符和字符串进行操作,即使是在Windows 98上运行,也可以调用这些函数。换句话说,w c s c a t、w
c s l e n和w c s t o k等函数都能够在Windows 98上很好地运行,这些都是必须关心的操作系统函数。

对于包含了对s t r函数或w c s函数进行显式调用的代码来说,无法非常容易地同时为A
N S I和U n i c o d e对这些代码进行编译。本章前面说过,可以创建同时为A N S I和U
n i c o d e进行编译的单个源代码文件。若要建立双重功能,必须包含T C h a r. h文件,而不是包含S t r i n g . h文件。T
C h a r. h文件的唯一作用是帮助创建A N S I / U n i c o d e通用源代码文件。它包含你应该用在源代码中的一组宏,而不应该直接调用s t r函数或者w
c s函数。如果在编译源代码文件时定义了_ U N I C O D E,这些宏就会引用w c s这组函数。如果没有定义_
U N I C O D E,那么这些宏将引用s t r这组宏。

例如,在T C h a r. h中有一个宏称为_ t c s c p y。如果在包含该头文件时没有定义_
U N I C O D E ,那么_ t c s c p y就会扩展为A N S I的s
t r c p y函数。但是如果定义了_UNICODE, _tcscpy将扩展为U n i c o d e的w
c s c p y函数。拥有字符串参数的所有C运行期函数都在T C h a r. h文件中定义了一个通用宏。如果使用通用宏,而不是A
N S I / U n i c o d e的特定函数名,就能够顺利地创建可以为
A N S I或U n i c o d e进行编译的源代码。

但是,除了使用这些宏之外,还有一些操作是必须进行的。
T C h a r. h文件包含了另外一些宏。若要定义一个A N S I / U n i c o d e通用的字符串数组,请使用下面的T C H A R数据类型。如果定义了_
U N I C O D E,T C H A R将声明为下面的形式:

typedef wchar_t TCHAR;

如果没有定义_ U N I C O D E,则T C H A R将声明为下面的形式:

typedef char TCHAR;

使用该数据类型,可以像下面这样分配一个字符串:

TCHAR szString[100];

也可以创建对字符串的指针:

TCHAR *szError = “Error”;

不过上面这行代码存在一个问题。按照默认设置,
M i c r o s o f t公司的C + +编译器能够编译所有的字符串,就像它们是A N S I字符串,而不是U
n i c o d e字符串。因此,如果没有定义_ U N I C O D E,该编译器将能正确地编译这一行代码。但是,如果定义了_ U N I C O D E,就会产生一个错误。若要生成一个U
n i c o d e字符串而不是A N S I字符串,必须将该代码行改写为下面的样子:

TCHAR *szError = L”Error”;

字符串(literal string)前面的大写字母L,用于告诉编译器该字符串应该作为U
n i c o d e字符串来编译。当编译器将字符串置于程序的数据部分中时,它在每个字符之间分散插入零字节。这种变更带来的问题是,现在只有当定义了_ U N I C O D E时,程序才能成功地进行编译。我们需要另一个宏,以便有选择地在字符串的前面加上大写字母L。这项工作由_
T E X T宏来完成,_ T E X T宏也在T C h a r. h文件中做了定义。如果定义了_
U N I C O D E,那么_ T E X T定义为下面的形式:

#define_TEXT(x) L ## x

如果没有定义_ U N I C O D E,_ T E X T将定义为

#define _TEXT(x) x

使用该宏,可以改写上面这行代码,这样,无论是否定义了
_ U N I C O D E宏,它都能够正确地进行编译。如下所示:

TCHAR *szError = _TEXT(“Error”);

_ T E X T宏也可以用于字符。例如,若要检查一个字符串的第一个字符是否是大写字母J,只需编写下面的代码即可:

2.8.2 Wi n d o w s定义的U n i c o d e数据类型

Wi n d o w s头文件定义了表2 - 3列出的数据类型。

这些数据类型是指U n i c o d e字符和字符串。Wi n d o w s头文件也定义了A
N S I / U n i c o d e通用数据类型P T S T R和P C T S T R。这些数据类型既可以指A
N S I字符串,也可以指U n i c o d e字符串,这取决于当编译程序模块时是否定义了U N I C O D E宏。

请注意,这里的U N I C O D E宏没有前置的下划线。_ U N I C O D E宏用于C运行期头文件,而U
N I C O D E宏则用于Wi n d o w s头文件。当编译源代码模块时,通常必须同时定义这两个宏。

2.8.3 Wi n d o w s中的U n i c o d e函数和A N S I函数

前面已经讲过,有两个函数称为C r e a t e Wi n d o w E x,一个C r e a t e Wi n d o w E x接受U
n i c o d e字符串,另一个C r e a t e Wi n d o w E x接受A N S I字符串。情况确实如此,不过,这两个函数的原型实际上是下面的样子:

C r e a t e Wi n d o w E x W是接受U n i c o d e字符串的函数版本。函数名结尾处的大写字母W是英文w
i d e(宽)的缩写。每个U n i c o d e字符的长度是1
6位,因此,它们常常称为宽字符。C r e a t e Wi n d o w E x A的结尾处的大写字母A表示该函数可以接受A
N S I字符串。但是,在我们的代码中,通常只包含了对 C r e a t e Wi n d o w E x的调用,而不是直接调用C r e a t e Wi n d o w E x W或者C
r e a t e Wi n d o w E x A。在Wi n U s e r. h文件中,C r e a t e Wi n d o w E x实际上是定义为下面这种形式的一个宏:

当编译源代码模块时,
U N I C O D E 是否已经作了定义,将决定你调用的是哪个C r e a t e Wi n d o w E x版本。当转用一个1 6位的Wi
n d o w s应用程序时,你在编译期间可能没有定义U N I C O D E。对C r e a t e Wi n d o w E x函数的任何调用都会将该宏扩展为对C
r e a t e Wi n d o w E x A的调用,即对C r e a t e Wi n d o w E x的A N S I版本的调用。由于1
6位Wi n d o w s只提供了C r e a t e Wi n d o w s E x的A
N S I版本,因此可以比较容易地转用它的应用程序。在Windows 2000下,M i c r o s o f t的C
r e a t e Wi n d o w E x A源代码只不过是一个形实替换程序层或翻译层,用于分配内存,以便将A N S I字符串转换成U n i c o d e字符串。该代码然后调用C
r e a t eWi n d o w E x W,并传递转换后的字符串。当C r e a t e Wi n d o w E x W返回时,C r e a t e Wi n d o w E
x A便释放它的内存缓存,并将窗口句柄返回给你。

如果要创建其他软件开发人员将要使用的动态链接库(
D L L) ,请考虑使用下面的方法。在D L L中提供两个输出函数。一个是A N S I版本,另一个是U
n i c o d e版本。在A N S I版本中,只需要分配内存,执行必要的字符串转换,并调用该函数的U n i c o d e版本(本章后面部分介绍这个进程)

在Windows 98下,M i c r o s o f t的C
r e a t e Wi n d o w E x A源代码是执行操作的函数。Windows 98提供了接受U n i c o d e参数的所有Wi
n d o w s函数的进入点,但是这些函数并不将 U n i c o d e字符串转换成A N S I字符串,它们只返回运行失败的消息。调用G
e t L a s t E r r o r将返回E R R O R _C A L L _ N O T _ I M P L E M E N T E D。这些函数中只有A N S I版本的函数才能正确地运行。如果编译的代码调用了任何宽字符函数,应用程序将无法在Windows
98下运行。Windows API中的某些函数,比如Wi n E x e c和O
p e n F i l e等,只是为了实现与1 6位Wi n d o w s程序的向后兼容而存在,因此,应该避免使用。应该使用对C
r e a t e P r o c e s s和C r e a t e F i l e函数的调用来取代对Wi n E x e c和O
p e n F i l e函数的调用。从系统内部来讲,老的函数完全可以调用新的函数。

老的函数存在的一个大问题是,它们不接受
U n i c o d e字符串。当调用这些函数时,必须传递A N S I字符串。另一方面,所有新的和未过时的函数在Windows 2000中都同时拥有A
N S I和U n i c o d e两个版本。

2.8.4 Wi n d o w s字符串函数

Wi n d o w s还提供了一组范围很广的字符串操作函数。这些函数与C运行期字符串函数(如s t r c p y和w
c s c p y)很相似。但是该操作系统函数是操作系统的一个组成部分,操作系统的许多组件都使用这些函数,而不使用C运行期库。建议最好使用操作系统函数,而不要使用C运行期字符串函数。这将有助于稍稍提高你的应用程序的运行性能,因为操作系统字符串函数常常被大型应用程序比如操作系统的外壳进程E
x p l o r e r. e x e所使用。由于这些函数使用得很多,因此,在你的应用程序运行时,它们可能已经被装入R A M。

若要使用这些函数,系统必须运行
Windows 2000或Windows 98。如果安装了I n t e r n e t Explorer 4.0或更新的版本,也可以在较早的Wi
n d o w s版本中获得这些函数。

在经典的操作系统函数样式中,操作系统字符串函数名既包含大写字母,也包含小写字母,它的形式类似这个样子:S t r C a t、S t r C h r、S
t r C m p和S t r C p y等。若要使用这些函数,必须加上S h l WA p i . h头文件。另外,如前所述,这些字符串函数既有A
N S I版本,也有U n i c o d e版本,例如S t r C a t A和S
t r C a t W。由于这些函数属于操作系统函数,因此,当创建应用程序时,如果定义了U N I C O D E(不带前置下划线) ,那么它们的符号将扩展为宽字符版本。

2.9
成为符合A N S I和U n i c o d e的应用程序

即使你不打算立即使用U n i c o d e,最好也应该着手将你的应用程序转换成符合U n i c o d e的应用程序。下面是应该遵循的一些基本原则:

• 将文本串视为字符数组,而不是c h a r s数组或字节数组。

• 将通用数据类型(如T C H A R和P T S T R)用于文本字符和字符串。

• 将显式数据类型(如B Y T E和P B Y T E)用于字节、字节指针和数据缓存。

• 将T E X T宏用于原义字符和字符串。

• 执行全局性替换(例如用P T S T R替换P S T R) 。

• 修改字符串运算问题。例如函数通常希望你在字符中传递一个缓存的大小,而不是字节。

这意味着你不应该传递
s i z e o f ( s z B u ff e r ) ,而应该传递(s i z e o f ( s z B u ff e r ) / s i z e o f ( T C H A R )。另外,如果需要为字符串分配一个内存块,并且拥有该字符串中的字符数目,那么请记住要按字节来分配内存。这就是说,应该调用malloc(nCharacters
*sizeof(TCHAR)),而不是调用m a l l o c( n C h a r a c t e r s )。在上面所说的所有原则中,这是最难记住的一条原则,如果操作错误,编译器将不发出任何警告。

当我为本书的第一版编写示例程序时,我编写的原始程序只能编译为
A N S I程序。后来,当我开始撰写本章的内容时,我想我应该鼓励使用
U n i c o d e,并且打算创建一些示例程序,以便展示你可以非常容易地编写既可以用U n i c o d e也可以用A
N S I来编译的程序。这时我发现最好的办法是将本书的所有示例程序进行转换,使它们都能够用U n i c o d e和A N S I进行编译。我用了大约4个小时将所有程序进行了转换。考虑到我以前从来没有这方面的转换经验,这个速度是相当不错了。

2.9.1 Wi n d o w s字符串函数

Wi n d o w s也提供了一组用于对U n i c o d e字符串进行操作的函数,表2 - 4对它们进行了描述。

这些函数是作为宏来实现的,这些宏既可以调用函数的
U n i c o d e版本,也可以调用函数的A N S I版本,这要根据编译源代码模块时是否已经定义了U N I C O D E而定。例如,如果没有定义U
N I C O D E,l s t r c a t函数将扩展为l s t r c a t A。如果定义了U
N I C O D E,l s t r c a t将扩展为l s t r c a t W。

有两个字符串函数,即l s t r c m p和l s t r c m p i,它们的行为特性与等价的C运行期函数是不同的。C运行期函数s
t r c m p、s t r c m p i、w c s c m p和w
c s c m p i只是对字符串中的代码点的值进行比较,这就是说,这些函数将忽略实际字符的含义,只是将第一个字符串中的每个字符的数值与第二个字符串中的字符的数值进行比较。而Wi n d o w s函数l
s t r c m p和l s t r c m p i是作为对Wi n d o w s函数C
o m p a r e S t r i n g的调用来实现的。

该函数对两个 U n i c o d e字符串进行比较。C o m p a r e S t r i n g的第一个参数用于设定语言I
D(L C I D) ,这是个3 2位值,用于标识一种特定的语言。C
o m p a r e S t r i n g使用这个L C I D来比较这两个字符串,方法是对照一种特定的语言来查看它们的字符的含义。这种操作方法比C运行期函数简单地进行数值比较更有意义。

当l s t r c m p函数系列中的任何一个函数调用C o m p a r e S t r i n g时,该函数便将调用Wi
n d o w s的G e t T h r e a d S t r i n g函数的结果作为第一个参数来传递:

LCID GetThreadLocale();

每次创建一个线程时,它就被赋予一种语言。函数将返回该线程的当前语言设置。

C o m p a r e S t r i n g的第二个参数用于标识一些标志,这些标志用来修改该函数比较两个字符串时所用的方法。表2 - 5显示了可以使用的标志。

当l s t r c m p调用C o m p a r e S t r i n g时,它传递0作为f
d w S t y l e的参数。但是,当l s t r c m p i调用C o m p a r e S t r i n g时,它就传递N
O R M _ I G N O R E C A S E。C o m p a r e S t r i n g的其余4个参数用于设定两个字符串和它们各自的长度。如果为c
c h 1参数传递- 1,那么该函数将认为p S t r i n g 1字符串是以0结尾,并计算该字符串的长度。对于p
S t r i n g 2字符串来说,参数c c h 2的作用也是一样。

其他C运行期函数没有为U n i c o d e字符串的操作提供很好的支持。例如,t
o l o w e r和t o u p p e r函数无法正确地转换带有重音符号的字符。为了弥补C运行期库中的这些不足,必须调用下面这些Wi
n d o w s函数,以便转换U n i c o d e字符串的大小写字母。这些函数也可以正确地用于A N S I字符串。

头两个函数:

PTSTR CharLower(PTSTR pzsString);

PTSTR CharUpper(PTSTR pszString);

既可以转换单个字符,也可以转换以
0结尾的整个字符串。若要转换整个字符串,只需要传递字符串的地址即可。若要转换单个字符,必须像下面这样传递各个字符:

TCHAR cLowerCaseChar = CharLower((PTSTR) szString[0]);

将单个字符转换成一个P T S T R,便可调用该函数,将一个值传递给它,在这个值中,较低的1 6位包含了该字符,较高的1
6位包含0。当该函数看到较高位是0时,该函数就知道你想要转换单个字符,而不是整个字符串。返回的值是个3
2位值,较低的1 6位中是已经转换的字符。下面两个函数与前面两个函数很相似,差别在于它们用于转换缓存中包含的字符(该缓存不必以0结尾)

其他的C运行期函数,如i s a l p h a、i
s l o w e r和i s u p p e r,返回一个值,指明某个字符是字母字符、小写字母还是大写字母。Windows API提供了一些函数,也能返回这些信息,但是Wi
n d o w s函数也要考虑用户在控制面板中指定的语言:

p r i n t f函数家族是要介绍的最后一组C运行期函数。如果在定义了_ U N I C O D E的情况下编译你的源代码模块,那么p
r i n t f函数家族便希望所有字符和字符串参数代表U n i c o d e字符和字符串。但是,如果在没有定义_ U N I C O D E的情况下编译你的源代码模块,p
r i n t f函数家族便希望传递给它的所有字符和字符串都是A N S I字符和字符串。

M i c r o s o f t公司已经给C运行期的p r i n t f函数家族增加了一些特殊的域类型。其中有些域类型尚未被ANSI
C采用。 新类型使你能够很容易地对A N S I和U n i c o d e字符和字符串进行混合和匹配。操作系统的w
s p r i n t f函数也得到了增强。下面是一些例子(请注意大写S和小写s的使用) :

2.9.2 资源

当资源编译器对你的所有资源进行编译时,输出文件是资源的二进制文件。资源(字符串表、对话框模板和菜单等)中的字符串值总是写作U n i c o d e字符串。在Windows 98和Wi
n d o w s2 0 0 0下,如果应用程序没有定义U N I C O D E宏,那么系统就会进行内部转换。

例如,如果在编译源代码模块时没有定义
U N I C O D E,调用L o a d S t r i n g实际上就是调用L o a d S t r i n g A函数。这时L
o a d S t r i n g A就从你的资源中读取字符串,并将该字符串转换成A N S I字符串。A N S I形式的字符串将从该函数返回给你的应用程序。

2.9.3 确定文本是A N S I文本还是U n i c o d e文本

到现在为止,U n i c o d e文本文件仍然非常少。实际上,M i c r o s o f t公司自己的大多数产品并没有配备任何U
n i c o d e文本文件。但是预计将来这种情况是会改变的(尽管这需要一个很长的过程) 。当然,Windows 2000的N o t e p a d (记事本)应用程序允许你既能打开U
n i c o d e文件,也能打开A N S I文件,并且可以创建这些文件。图2 - 1显示了N
o t e p a d的Save As(文件另存为)对话框。请注意可以用不同的方法来保存文本文件。

对于许多用来打开文本文件和处理这些文件的应用程序(如编译器)来说,打开一个文件后,应用程序就能方便地确定该文本文件是包含A N S I字符还是U n i c o d e字符。I
s Te x t U n i c o d e函数能够帮助进行这种区分:

DWORD IsTextUnicode(CONST PVOID pvBuffer ,int cb ,PINT pResult);

文本文件存在的问题是,它们的内容没有严格和明确的规则,因此很难确定该文件是包含A N S I字符还是U n i c o d e字符。I
s Te x t U n i c o d e使用一系列统计方法和定性方法,以便猜测缓存的

内容。由于这不是一种确切的科学方法,因此I s Te x t U n i c o d e有可能返回不正确的结果。

第一个参数p v B u ff e r用于标识要测试的缓存的地址。该数据是个无效指针,因为你不知道你拥有的是A
N S I字符数组还是U n i c o d e字符数组。

第二个参数c b用于设定p v B u ff e r指向的字节数。同样,由于你不知道缓存中放的是什么,因此c
b是个字节数,而不是字符数。请注意,不必设定缓存的整个长度。当然,I s Te x t U n i c o d e

能够测试的字节越多,得到的结果越准确。

第三个参数p R e s u l t是个整数的地址,必须在调用I s Te x t U n i c o d e之前对它进行初始化。对该

整数进行初始化后,就可以指明你要I s Te x t U n i c o d e执行哪些测试。也可以为该参数传递N U L L,

在这种情况下,I s Te x t U n i c o d e将执行它能够进行的所有测试(详细说明请参见Platform SDK文档) 。

如果I s Te x t U n i c o d e认为缓存包含U n i c o d e文本,便返回T
R U E,否则返回FA L S E。确实是这样,尽管M i c r o s o f t将该函数的原型规定为返回D
W O R D,但是它实际上返回一个布尔值。如果在p R e s u l t参数指向的整数中必须进行特定的测试,该函数就会在返回之前设定整数中的信息位,以反映每个测试的结果。

Wi n d o w s 9 8
在Windows 98下,I s Te x t U n i c o d e函数没有有用的实现代码,它只是返回FA
L S E。调用G e t L a s t E r r o r函数将返回E R R O R _ C A L L _ N O T _ I M P L E M E N T D。

第1 7章中的Flie Rev示例应用程序演示了I s TextUnicode函数的使用。

2.9.4 在U n i c o d e与A N S I之间转换字符串

Wi n d o w s函数M u l t i B y t e To Wi d e C h a r用于将多字节字符串转换成宽字符串。下面显示了M
u l t i B y t e To Wi d e C h a r函数。

u C o d e P a g e参数用于标识一个与多字节字符串相关的代码页号。d w F l a g s参数用于设定另一个控件,它可以用重音符号之类的区分标记来影响字符。这些标志通常并不使用,在d
w F l a g s参数中传递0。p M u l t i B y t e S t r参数用于设定要转换的字符串,c
c h M u l t i B y t e参数用于指明该字符串的长度(按字节计算) 。如果为c c h M u l t i B y t e参数传递- 1,那么该函数用于确定源字符串的长度。

转换后产生的U n i c o d e版本字符串将被写入内存中的缓存,其地址由p Wi d e C h a r S t r参数指定。必须在c
c h Wi d e C h a r参数中设定该缓存的最大值(以字符为计量单位) 。如果调用M u l t i B y t e To Wi d e C h a r,给c c h Wi d
e C h a r参数传递0,那么该参数将不执行字符串的转换,而是返回为使转换取得成功所需要的缓存的值。一般来说,可以通过下列步骤将多字节字符串转换成U n i c o d e等价字符串:

1) 调用M u l t i B y t e To Wi d e C h a r函数,为p Wi d e C h a r S t r参数传递N
U L L,为c c h Wi d e C h a r参数传递0。

2) 分配足够的内存块,用于存放转换后的U n i c o d e字符串。该内存块的大小由前面对M u l t B y t e To
Wi d e C h a r的调用返回。

3) 再次调用M u l t i B y t e To Wi d e C h a r,这次将缓存的地址作为p Wi d e C h
a r S t r参数来传递,并传递第一次调用M u l t i B y t e To Wi d e C h a r时返回的缓存大小,作为c c h Wi d e c h a r参数。

4) 使用转换后的字符串。

5) 释放U n i c o d e字符串占用的内存块。函数Wi d e C h a r To M u l t i B y t e将宽字符串转换成等价的多字节字符串,如下所示:

该函数与M u l t i B i t e To Wi d e C h a r函数相似。同样,u C o d e P a g e参数用于标识与新转换的字符串相关的代码页。d
w F l a g s则设定用于转换的其他控件。这些标志能够作用于带有区分符号的字符和系统不能转换的字符。通常不需要为字符串的转换而拥有这种程度的控制手段,你将为d w F l a g s参数传递0。

p Wi d e C h a r S t r参数用于设定要转换的字符串的内存地址,c c h Wi d e C h a r参数用于指明该字符串的长度(用字符数来计量) 。如果你为c
c h Wi d e C h a r参数传递- 1,那么该函数用于确定源字符串的长度。

转换产生的多字节版本的字符串被写入由p M u l t i B y t e S t r参数指明的缓存。必须在c c h M u l t i B y t e参数中设定该缓存的最大值(用字节来计量)
。如果传递0作为Wi d e C h a r To M u l t i B y t e函数的c c h M
u l t i B y t e参数,那么该函数将返回目标缓存需要的大小值。通常可以使用将多字节字符串转换成宽字节字符串时介绍的一系列类似的事件,将宽字节字符串转换成多字节字符串。

你会发现,Wi d e C h a r To M u l t i B y t e函数接受的参数比M u l t i B y t e To Wi d e C h a r函数要多2个,即p
D e f a u l t C h a r和p f U s e d D e f a u l t C h a r。只有当Wi d e C h a r To M u l t i B y t e函数遇到一个宽字节字符,而该字符在u
C o d e P a g e参数标识的代码页中并没有它的表示法时,Wi d e C h a r To M u l t i B y t e函数才使用这两个参数。如果宽字节字符不能被转换,该函数便使用p
D e f a u l t C h a r参数指向的字符。如果该参数是N U L L(这是大多数情况下的参数值) ,那么该函数使用系统的默认字符。该默认字符通常是个问号。这对于文件名来说是危险的,因为问号是个通配符。

p f U s e d D e f a u l t C h a r参数指向一个布尔变量,如果宽字符串中至少有一个字符不能转换成等价多字节字符,那么函数就将该变量置为T R U E。如果所有字符均被成功地转换,那么该函数就将该变量置为FA
L S E。当函数返回以便检查宽字节字符串是否被成功地转换后,可以测试该变量。同样,通常为该测试传递N U L L。

关于如何使用这些函数的详细说明,请参见Platform SDK文档。

如果使用这两个函数,就可以很容易创建这些函数的
U n i c o d e版本和A N S I版本。例如,你可能有一个动态链接库,它包含一个函数,能够转换字符串中的所有字符。可以像下面这样编写该函数的U n i c o d e版本:

ASCII版本

然后是提供一个对外接口:

windows核心编程-第二章 Unicode的更多相关文章

  1. windows核心编程---第二章 字符和字符串处理

        使用vc编程时项目-->属性-->常规栏下我们可以设置项目字符集合,它可以是ANSI(多字节)字符集,也可以是unicode字符集.一般情况下说Unicode都是指UTF-16.也 ...

  2. Windows核心编程第二章,字符串的表示以及宽窄字符的转换

    目录 Windows核心编程,字符串的表示以及宽窄字符的转换 1.字符集 1.1.双字节字符集DBCS 1.2 Unicode字符集 1.3 UTF-8编码 1.4 UTF - 32编码. 1.5 U ...

  3. Windows核心编程第一章.错误处理

    Windows核心编程第一章,错误处理. 一丶错误处理 1.核心编程学习总结 不管是做逆向,开始做开发.在Windows下.你都需要看一下核心编程这本书.这本书确实写得很好.所以自己在学习这本书的同时 ...

  4. windows核心编程---第九章 同步设备IO与异步设备IO之同步IO

    同步设备IO 所谓同步IO是指线程在发起IO请求后会被挂起,IO完成后继续执行. 异步IO是指:线程发起IO请求后并不会挂起而是继续执行.IO完毕后会得到设备的通知.而IO完成端口就是实现这种通知的很 ...

  5. [转]Windows Shell 编程 第二章 【来源:http://blog.csdn.net/wangqiulin123456/article/details/7987893】

    第二章Shell的结构  “Shell 编程”的大伞之下有大量的API函数和COM接口.这个种类繁多的‘命令’集允许你用不同的方法对Windows Shell进行编程.函数和接口并不是两种提供相同功能 ...

  6. Windows核心编程 第二十章 DLL的高级操作技术

    第2 0章 D L L的高级操作技术 看了下这章的内容,谈不上高级,都是些常用相关,但是还是有一些细节需要注意. 20.1 DLL模块的显式加载和符号链接 如果线程需要调用D L L模块中的函数,那么 ...

  7. Windows核心编程 第九章 线程与内核对象的同步(下)

    9.4 等待定时器内核对象 等待定时器是在某个时间或按规定的间隔时间发出自己的信号通知的内核对象.它们通常用来在某个时间执行某个操作. 若要创建等待定时器,只需要调用C r e a t e Wa i ...

  8. windows核心编程-第一章 对程序错误的处理

    第一章-对程序错误的处理 在开始介绍Microsoft Windows 的特性之前,必须首先了解 Wi n d o w s的各个函数是如何进行错误处理的. 当调用一个Wi n d o w s函数时,它 ...

  9. windows核心编程---第一章 谈谈windows中的错误处理机制

        我们写的函数会用返回值表示程序执行的正确与否,使用void,就意味着程序一定不会出错.Bool类型标识true时为真,false时为假.其他类型根据需要可以定义成不同意义.       Win ...

随机推荐

  1. JVM笔记 -- JVM经历了什么?

    Sun Classic VM 世界上第一款商用 Java 虚拟机,JDK1.4 已经淘汰. 内部只有解释器,可以自己外挂JIT编译器,但是二者只能使用其一,不能配合工作. hotspot 内置了该虚拟 ...

  2. Python3+pygame实现的flappy bird游戏,代码完整,还有音乐

    之前一直在手机上玩flappy bird游戏,闲暇时间就编写了一个 是采用python3+pygame模块制作而成的,运行效果非常流畅,会让你大吃一惊哦哈哈 一.运行效果展示 下载游戏之后,注意在自己 ...

  3. 详解 ZooKeeper 数据持久化

    本文作者:HelloGitHub-老荀 Hi,这里是 HelloGitHub 推出的 HelloZooKeeper 系列,免费开源.有趣.入门级的 ZooKeeper 教程,面向有编程基础的新手. 项 ...

  4. AutoPy开发文档

    AutoPy 简介 AutoPy是为python开发者提供的一个安卓插件,由路飞大佬开发维护,主要功能为了实现使用python在安卓端完成一些操作,例如点击,滑动,返回 准备 安装AutoPy.apk ...

  5. ElasticSearch实战系列十: ElasticSearch冷热分离架构

    前言 本文主要介绍ElasticSearch冷热分离架构以及实现. 冷热分离架构介绍 冷热分离是目前ES非常火的一个架构,它充分的利用的集群机器的优劣来实现资源的调度分配.ES集群的索引写入及查询速度 ...

  6. Topshelf一个用于使用.NET构建Windows服务框架

    1 Topshelf是什么? Topshelf是用于托管使用.NET框架编写的Windows服务的框架.服务的创建得到简化,从而使开发人员可以创建一个简单的控制台应用程序,可以使用Topshelf将其 ...

  7. Kubernetes 常见问题总结

    Kubernetes 常见问题总结 如何删除不一致状态下的 rc,deployment,service 在某些情况下,经常发现 kubectl 进程挂起现象,然后在 get 时候发现删了一半,而另外的 ...

  8. 经常问到的 BFC 和 IFC 是什么?

    什么是BFC?什么作用? Block Formatting Context 块盒子布局发生的区域,浮动元素和其他元素交互的区域 浮动定位和清除浮动的时候只会应用于同一个BFC内的元素.浮动不会影响其他 ...

  9. [Fundamental of Power Electronics]-PART I-1.引言-1.2 1.3 电力电子技术的几个应用、本书内容

    1.2 电力电子技术的几个应用 高效开关变换器面临的功率范围从 (1)小于1瓦(电池供电的便携式设备内的DC-DC转换器)到(2)计算机及办公设备中的几十,几百,数千瓦到(3)变速电机驱动器中上千瓦及 ...

  10. java面试-synchronized与lock有什么区别?

    1.原始构成: synchronized是关键字,属于JVM层面,底层是由一对monitorenter和monitorexit指令实现的. ReentrantLock是一个具体类,是API层面的锁. ...