Unicode简介

Programming.Windows

Unicode简介

壹佰软件开发小组  整理编译  

第一章中,我已经预告,C语言中在Microsoft Windows程序设计中扮演着重要角色的任何部分都会讲述到,您也许在传统文字模式程序设计中还尚未遇到过这些问题。宽字符集和Unicode差不多就是这样的问题。

简单地说,Unicode扩展自ASCII字符集。在严格的ASCII中,每个字符用7位表示,或者计算机上普遍使用的每字符有8位宽;而Unicode使用全16位字符集。这使得Unicode能够表示世界上所有的书写语言中可能用于计算机通讯的字符、象形文字和其它符号。Unicode最初打算作为ASCII的补充,可能的话,最终将代替它。考虑到ASCII是计算机中最具支配地位的标准,所以这的确是一个很高的目标。

Unicode影响到了计算机工业的每个部分,但也许会对操作系统和程序设计语言的影响最大。从这方面来看,我们已经上路了。Windows NT从底层支持Unicode(不幸的是,Windows 98只是小部分支持Unicode)。先天即被ANSI束缚的C程序设计语言通过对宽字符集的支持来支持Unicode。下面将详细讨论这些内容。

自然,作为程序写作者,我们通常会面对许多繁重的工作。我已试图透过使本书中的所有程序「Unicode化」来减轻负担。其含义会随着本章对Unicode的讨论而清晰起来。

字符集简史

虽然不能确定人类开始讲话的时间,但书写已有大约6000年的历史了。实际上,早期书写的内容是象形文字。每个字符都对应于发声的字母表则出现于大约3000年前。虽然人们过去使用的多种书写语言都用得好好的,但19世纪的几个发明者还是看到了更多的需求。Samuel F. B. Morse在1838年到1854年间发明了电报,当时他还发明了一种电报上使用的代码。字母表中的每个字符对应于一系列短的和长的脉冲(点和破折号)。虽然其中大小写字母之间没有区别,但数字和标点符号都有了自己的代码。

Morse代码并不是以其它图画的或印刷的象形文字来代表书写语言的第一个例子。1821年到1824年之间,年轻的Louis Braille受到在夜间读写信息的军用系统的启发,发明了一种代码,它用纸上突起的点作为代码来帮助盲人阅读。Braille代码实际上是一种6位代码,它把字符、常用字母组合、常用单字和标点进行编码。一个特殊的escape代码表示后续的字符代码应解释为大写。一个特殊的shift代码允许后续代码被解释为数字。

Telex代码,包括Baudot (以一个法国工程师命名,该工程师死于1903年)以及一种被称为CCITT #2的代码(1931年被标准化),都是包括字符和数字的5位代码。

美国标准

早期计算机的字符码是从Hollerith卡片(号称不能被折迭、卷曲或毁伤)发展而来的,该卡片由Herman Hollerith发明并首次在1890年的美国人口普查中使用。6位字符码系统BCDIC(Binary-Coded Decimal Interchange Code:二进制编码十进制交换编码)源自Hollerith代码,在60年代逐步扩展为8位EBCDIC,并一直是IBM大型主机的标准,但没使用在其它地方。

美国信息交换标准码(ASCII:American Standard Code for Information Interchange)起始于50年代后期,最后完成于1967年。开发ASCII的过程中,在字符长度是6位、7位还是8位的问题上产生了很大的争议。从可靠性的观点来看不应使用替换字符,因此ASCII不能是6位编码,但由于费用的原因也排除了8位版本的方案(当时每位的储存空间成本仍很昂贵)。这样,最终的字符码就有26个小写字母、26个大写字母、10个数字、32个符号、33个句柄和一个空格,总共128个字符码。ASCII现在记录在ANSI X3.4-1986字符集-用于信息交换的7位美国国家标准码(7-Bit ASCII:7-Bit American National Standard Code for Information Interchange),由美国国家标准协会(American National Standards Institute)发布。图2-1中所示的ASCII字符码与ANSI文件中的格式相似。

ASCII有许多优点。例如,26个字母代码是连续的(在EBCDIC代码中就不是这样的);大写字母和小写字母可通过改变一位数据而相互转化;10个数字的代码可从数值本身方便地得到(在BCDIC代码中,字符「0」的编码在字符「9」的后面!)

最棒的是,ASCII是一个非常可靠的标准。在键盘、视讯显示卡、系统硬件、打印机、字体文件、操作系统和Internet上,其它标准都不如ASCII码流行而且根深蒂固。


 

图2-1 ASCII字符集

国际方面

ASCII的最大问题就是该缩写的第一个字母。ASCII是一个真正的美国标准,所以它不能良好满足其它讲英语国家的需要。例如英国的英镑符号(£)在哪里?

英语使用拉丁(或罗马)字母表。在使用拉丁语字母表的书写语言中,英语中的单词通常很少需要重音符号(或读音符号)。即使那些传统惯例加上读音符号也无不当的英语单字,例如c鰋perate或者résumé,拼写中没有读音符号也会被完全接受。

但在美国以南、以北,以及大西洋地区的许多国家,在语言中使用读音符号很普遍。这些重音符号最初是为使拉丁字母表适合这些语言读音不同的需要。在远东或西欧的南部旅游,您会遇到根本不使用拉丁字母的语言,例如希腊语、希伯来语、阿拉伯语和俄语(使用斯拉夫字母表)。如果您向东走得更远,就会发现中国象形汉字,日本和朝鲜也采用汉字系统。

ASCII的历史开始于1967年,此后它主要致力于克服其自身限制以更适合于非美国英语的其它语言。例如,1967年,国际标准化组织(ISO:International Standards Organization)推荐一个ASCII的变种,代码0x40、0x5B、0x5C、0x5D、0x7B、0x7C和0x7D「为国家使用保留」,而代码0x5E、0x60和0x7E标为「当国内要求的特殊字符需要8、9或10个空间位置时,可用于其它图形符号」。这显然不是一个最佳的国际解决方案,因为这并不能保证一致性。但这却显示了人们如何想尽办法为不同的语言来编码的。

扩展ASCII

在小型计算机开发的初期,就已经严格地建立了8位字节。因此,如果使用一个字节来保存字符,则需要128个附加的字符来补充ASCII。1981年,当最初的IBM PC推出时,视讯卡的ROM中烧有一个提供256个字符的字符集,这也成为IBM标准的一个重要组成部分。

最初的IBM扩展字符集包括某些带重音的字符和一个小写希腊字母表(在数学符号中非常有用),还包括一些块型和线状图形字符。附加的字符也被添加到ASCII控制字符的编码位置,这是因为大多数控制字符都不是拿来显示用的。

该IBM扩展字符集被烧进无数显示卡和打印机的ROM中,并被许多应用程序用于修饰其文字模式的显示方式。不过,该字符集并没有为所有使用拉丁字母表的西欧语言提供足够多的带重音字符,而且也不适用于Windows。Windows不需要图形字符,因为它有一个完全图形化的系统。

在Windows 1.0(1985年11月发行)中,Microsoft没有完全放弃IBM扩展字符集,但它已退居第二重要位置。因为遵循了ANSI草案和ISO标准,纯Windows字符集被称作「ANSI字符集」。ANSI草案和ISO标准最终成为ANSI/ISO 8859-1-1987,即「American National Standard for Information Processing-8-Bit Single-Byte Coded Graphic Character Sets-Part 1: Latin Alphabet No 1」,通常也简写为「Latin 1」。

在Windows 1.0的《Programmer's Reference》中印出了ANSI字符集的最初版本,如图2-2所示。


 

图2-2 Windows ANSI字符集(基于ANSI/ISO 8859-1)

空方框表示该位置未定义字符。这与ANSI/ISO 8859-1的最终定义一致。ANSI/ISO 8859-1仅显示了图形字符,而没有控制字符,因此没有定义DEL。此外,代码0xA0定义为一个非断开的空格(这意味着在编排格式时,该字符不用于断开一行),代码0xAD是一个软连字符(表示除非在行尾断开单词时使用,否则不显示)。此外,ANSI/ISO 8859-1将代码0xD7定义为乘号(*),0xF7为除号(/)。Windows中的某些字体也定义了从0x80到0x9F的某些字符,但这些不是ANSI/ISO 8859-1标准的一部分。

MS-DOS 3.3(1987年4月发行)向IBM PC用户引进了代码页(code page)的概念,Windows也使用此概念。代码页定义了字符的映像代码。最初的IBM字符集被称作代码页437,或者「MS-DOS Latin US)。代码页850就是「MS-DOS Latin 1」,它用附加的带重音字母(但不是图2-2所示的Latin 1 ISO/ANSI标准)代替了一些线形字符。其它代码页被其它语言定义。最低的128个代码总是相同的;较高的128个代码取决于定义代码页的语言。

在MS-DOS中,如果用户为PC的键盘、显示卡和打印机指定了一个代码页,然后在PC上创建、编辑和打印文件,一切都很正常,每件事都会保持一致。然而,如果用户试图与使用不同代码页的用户交换文件,或者在机器上改变代码页,就会产生问题。字符码与错误的字符相关联。应用程序能够将代码页信息与文件一起保存来试图减少问题的产生,但该策略包括了某些在代码页间转换的工作。

虽然代码页最初仅提供了不包括带重音符号字母的附加拉丁字符集,但最终代码页的较高的128个字符还是包括了完整的非拉丁字母,例如希伯来语、希腊语和斯拉夫语。自然,如此多样会导致代码页变得混乱;如果少数带重音的字母未正确显示,那么整个文字便会混乱不堪而不可阅读。

代码页的扩展正是基于所有这些原因,但是还不够。斯拉夫语的MS-DOS代码页855与斯拉夫语的Windows代码页1251以及斯拉夫语的Macintosh代码页10007不同。每个环境下的代码页都是对该环境所作的标准字符集修正。IBM OS/2也支援多种EBCDIC代码页。

但等一下,你会发现事情变得更糟糕。

双字节字符集

迄今为止,我们已经看到了256个字符的字符集。但中国、日本和韩国的象形文字符号有大约21,000个。如何容纳这些语言而仍保持和ASCII的某种兼容性呢?

解决方案(如果这个说法正确的话)是双字节字符集(DBCS:double-byte character set)。DBCS从256代码开始,就像ASCII一样。与任何行为良好的代码页一样,最初的128个代码是ASCII。然而,较高的128个代码中的某些总是跟随着第二个字节。这两个字节一起(称作首字节和跟随字节)定义一个字符,通常是一个复杂的象形文字。

虽然中文、日文和韩文共享一些相同的象形文字,但显然这三种语言是不同的,而且经常是同一个象形文字在三种不同的语言中代表三件不同的事。Windows支持四个不同的双字节字符集:代码页932(日文)、936(简体中文)、949(韩语)和950(繁体汉字)。只有为这些国家(地区)生产的Windows版本才支持DBCS。

双字符集问题并不是说字符由两个字节代表。问题在于一些字符(特别是ASCII字符)由1个字节表示。这会引起附加的程序设计问题。例如,字符串中的字符数不能由字符串的字节数决定。必须剖析字符串来决定其长度,而且必须检查每个字节以确定它是否为双字节字符的首字节。如果有一个指向DBCS字符串中间的指针,那么该字符串前一个字符的地址是什么呢?惯用的解决方案是从开始的指针分析该字符串!

Unicode解决方案

我们面临的基本问题是世界上的书写语言不能简单地用256个8位代码表示。以前的解决方案包括代码页和DBCS已被证明是不能满足需要的,而且也是笨拙的。那什么才是真正的解决方案呢?

身为程序写作者,我们经历过这类问题。如果事情太多,用8位数值已经不能表示,那么我们就试更宽的值,例如16位值。而且这很有趣的,正是Unicode被制定的原因。与混乱的256个字符代码映像,以及含有一些1字节代码和一些2字节代码的双字节字符集不同,Unicode是统一的16位系统,这样就允许表示65,536个字符。这对表示所有字符及世界上使用象形文字的语言,包括一系列的数学、符号和货币单位符号的集合来说是充裕的。

明白Unicode和DBCS之间的区别很重要。Unicode使用(特别在C程序设计语言环境里)「宽字符集」。「Unicode中的每个字符都是16位宽而不是8位宽。」在Unicode中,没有单单使用8位数值的意义存在。相比之下,在双字节字符集中我们仍然处理8位数值。有些字节自身定义字符,而某些字节则显示需要和另一个字节共同定义一个字符。

处理DBCS字符串非常杂乱,但是处理Unicode文字则像处理有秩序的文字。您也许会高兴地知道前128个Unicode字符(16位代码从0x0000到0x007F)就是ASCII字符,而接下来的128个Unicode字符(代码从0x0080到0x00FF)是ISO 8859-1对ASCII的扩展。Unicode中不同部分的字符都同样基于现有的标准。这是为了便于转换。希腊字母表使用从0x0370到0x03FF的代码,斯拉夫语使用从0x0400到0x04FF的代码,美国使用从0x0530到0x058F的代码,希伯来语使用从0x0590到0x05FF的代码。中国、日本和韩国的象形文字(总称为CJK)占用了从0x3000到0x9FFF的代码。

Unicode的最大好处是这里只有一个字符集,没有一点含糊。Unicode实际上是个人计算机行业中几乎每个重要公司共同合作的结果,并且它与ISO 10646-1标准中的代码是一一对应的。Unicode的重要参考文献是《The Unicode Standard,Version 2.0》(Addison-Wesley出版社,1996年)。这是一本特别的书,它以其它文件少有的方式显示了世界上书写语言的丰富性和多样性。此外,该书还提供了开发Unicode的基本原理和细节。

Unicode有缺点吗?当然有。Unicode字符串占用的内存是ASCII字符串的两倍。(然而压缩文件有助于极大地减少文件所占的磁盘空间。)但也许最糟的缺点是:人们相对来说还不习惯使用Unicode。身为程序写作者,这就是我们的工作。

宽字符和 C

对C程序写作者来说,16位字符的想法的确让人扫兴。一个char和一个字节同宽是最不能确定的事情之一。没几个程序写作者清楚ANSI/ISO 9899-1990,这是「美国国家标准程序设计语言-C」(也称作「ANSI C」)通过一个称作「宽字符」的概念来支持用多个字节代表一字符的字符集。这些宽字符与常用的字符完美地共存。

ANSI C也支持多字节字符集,例如中文、日文和韩文版本Windows支持的字符集。然而,这些多字节字符集被当成单字节构成的字符串看待,只不过其中一些字符改变了后续字符的含义而已。多字节字符集主要影响C语言程序执行时期链接库函数。相比之下,宽字符比正常字符宽,而且会引起一些编译问题。

宽字符不需要是Unicode。Unicode是一种可能的宽字符集。然而,因为本书的焦点是Windows而不是C执行的理论,所以我将把宽字符和Unicode作为同义语。

Char数据型态

假定我们都非常熟悉在C程序中使用char数据型态来定义和储存字符跟字符串。但为了便于理解C如何处理宽字符,让我们先回顾一下可能在Win32程序中出现的标准字符定义。

下面的语句定义并初始化了一个只包含一个字符的变量:

char c = 'A' ;
        

变量c需要1个字节来保存,并将用十六进制数0x41初始化,这是字母A的ASCII代码。

您可以像这样定义一个指向字符串的指针:

char * p ;
        

因为Windows是一个32位操作系统,所以指针变量p需要用4个字节保存。您还可初始化一个指向字符串的指针:

char * p = "Hello!" ;
        

像前面一样,变量p也需要用4个字节保存。该字符串保存在静态内存中并占用7个字节-6个字节保存字符串,另1个字节保存终止符号0。

您还可以像这样定义字符数组:

char a[10] ;
        

在这种情况下,编译器为该数组保留了10个字节的储存空间。表达式sizeof(a)将返回10。如果数组是整体变量(即在所有函数外定义),您可使用像下面的语句来初始化一个字符数组:

char a[] = "Hello!" ;
        

如果您将该数组定义为一个函数的区域变量,则必须将它定义为一个static变量,如下:

static char a[] = "Hello!" ;
        

无论哪种情况,字符串都储存在静态程序内存中,并在末尾添加0,这样就需要7个字节的储存空间。

宽字符

Unicode或者宽字符都没有改变char数据型态在C中的含义。char继续表示1个字节的储存空间,sizeof (char)继续返回1。理论上,C中1个字节可比8位长,但对我们大多数人来说,1个字节(也就是1个char)是8位宽。

C中的宽字符基于wchar_t数据型态,它在几个表头文件包括WCHAR.H中都有定义,像这样:

typedef unsigned short wchar_t ;
        

因此,wchar_t数据型态与无符号短整数型态相同,都是16位宽。

要定义包含一个宽字符的变量,可使用下面的语句:

wchar_t c = 'A' ;
        

变量c是一个双字节值0x0041,是Unicode表示的字母A。(然而,因为Intel微处理器从最小的字节开始储存多字节数值,该字节实际上是以0x41、0x00的顺序保存在内存中。如果检查Unicode文字的计算机储存应注意这一点。)

您还可定义指向宽字符串的指针:

wchar_t * p = L"Hello!" ;
        

注意紧接在第一个引号前面的大写字母L(代表「long」)。这将告诉编译器该字符串按宽字符保存-即每个字符占用2个字节。通常,指针变量p要占用4个字节,而字符串变量需要14个字节-每个字符需要2个字节,末尾的0还需要2个字节。

同样,您还可以用下面的语句定义宽字符数组:

static wchar_t a[] = L"Hello!" ;
        

该字符串也需要14个字节的储存空间,sizeof (a) 将返回14。索引数组a可得到单独的字符。a[1] 的值是宽字符「e」,或者0x0065。

虽然看上去更像一个印刷符号,但第一个引号前面的L非常重要,并且在两个符号之间必须没有空格。只有带有L,编译器才知道您需要将字符串存为每个字符2字节。稍后,当我们看到使用宽字符串而不是变量定义时,您还会遇到第一个引号前面的L。幸运的是,如果忘记了包含L,C编译器通常会给提出警告或错误信息。

您还可在单个字符文字前面使用L前缀,来表示它们应解释为宽字符。如下所示:

wchar_t c = L'A' ;
        

但通常这是不必要的,C编译器会对该字符进行扩充,使它成为宽字符。

宽字符链接库函数

我们都知道如何获得字符串的长度。例如,如果我们已经像下面这样定义了一个字符串指针:

char * pc = "Hello!" ;
        

我们可以呼叫

iLength = strlen (pc) ;
        

这时变量iLength将等于6,也就是字符串中的字符数。

太好了!现在让我们试着定义一个指向宽字符的指针:

wchar_t * pw = L"Hello!" ;
        

再次呼叫strlen :

iLength = strlen (pw) ;
        

现在麻烦来了。首先,C编译器会显示一条警告消息,可能是这样的内容:

'function' : incompatible types - from 'unsigned short *' to 'const char *'

这条消息的意思是:声明strlen函数时,该函数应接收char类型的指标,但它现在却接收了一个unsigned short类型的指标。您仍然可编译并执行该程序,但您会发现iLength等于1。为什么?

字符串「Hello!」中的6个字符占用16位:

0x0048 0x0065 0x006C 0x006C 0x006F 0x0021

Intel处理器在内存中将其存为:

48 00 65 00 6C 00 6C 00 6F 00 21 00

假定strlen函数正试图得到一个字符串的长度,并把第1个字节作为字符开始计数,但接着假定如果下一个字节是0,则表示字符串结束。

这个小练习清楚地说明了C语言本身和执行时期链接库函数之间的区别。编译器将字符串L"Hello!" 解释为一组16位短整数型态数据,并将其保存在wchar_t数组中。编译器还处理数组索引和sizeof操作符,因此这些都能正常工作,但在连结时才添加执行时期链接库函数,例如strlen。这些函数认为字符串由单字节字符组成。遇到宽字符串时,函数就不像我们所希望那样执行了。

您可能要说:「噢,太麻烦了!」现在每个C语言链接库函数都必须重写以接受宽字符。但事实上并不是每个C语言链接库函数都需要重写,只是那些有字符串参数的函数才需要重写,而且也不用由您来完成。它们已经重写完了。

strlen函数的宽字符版是wcslen(wide-character string length:宽字符串长度),并且在STRING.H(其中也说明了strlen)和WCHAR.H中均有说明。strlen函数说明如下:

size_t __cdecl strlen (const char *) ;        

而wcslen函数则说明如下:

size_t __cdecl wcslen (const wchar_t *) ;        

这时我们知道,要得到宽字符串的长度可以呼叫

iLength = wcslen (pw) ; 

函数将返回字符串中的字符数6。请记住,改成宽字节后,字符串的字符长度不改变,只是位组长度改变了。

您熟悉的所有带有字符串参数的C执行时期链接库函数都有宽字符版。例如,wprintf是printf的宽字符版。这些函数在WCHAR.H和含有标准函数说明的表头文件中说明。

维护单一原始码

当然,使用Unicode也有缺点。第一点也是最主要的一点是,程序中的每个字符串都将占用两倍的储存空间。此外,您将发现宽字符执行时期链接库中的函数比常规的函数大。出于这个原因,您也许想建立两个版本的程序-一个处理ASCII字符串,另一个处理Unicode字符串。最好的解决办法是维护既能按ASCII编译又能按Unicode编译的单一原始码文件。

虽然只是一小段程序,但由于执行时期链接库函数有不同的名称,您也要定义不同的字符,这将在处理前面有L的字符串文字时遇到麻烦。

一个办法是使用Microsoft Visual C++包含的TCHAR.H表头文件。该表头文件不是ANSI C标准的一部分,因此那里定义的每个函数和宏定义的前面都有一条底线。TCHAR.H为需要字符串参数的标准执行时期链接库函数提供了一系列的替代名称(例如,_tprintf和_tcslen)。有时这些名称也称为「通用」函数名称,因为它们既可以指向函数的Unicode版也可以指向非Unicode版。

如果定义了名为_UNICODE的标识符,并且程序中包含了TCHAR.H表头文件,那么_tcslen就定义为wcslen:

#define _tcslen wcslen        

如果没有定义UNICODE,则_tcslen定义为strlen:

#define _tcslen strlen        

等等。TCHAR.H还用一个新的数据型态TCHAR来解决两种字符数据型态的问题。如果定义了_UNICODE标识符,那么TCHAR就是wchar_t:

typedef wchar_t TCHAR ;        

否则,TCHAR就是Char:

typedef char TCHAR ;        

现在开始讨论字符串文字中的L问题。如果定义了_UNICODE标识符,那么一个称作__T的宏就定义如下:

#define __T(x) L##x        

这是相当晦涩的语法,但合乎ANSI C标准的前置处理器规范。那一对井字号称为「粘贴符号(token paste)」,它将字母L添加到宏参数上。因此,如果宏参数是"Hello!",则L##x就是L"Hello!"。

如果没有定义_UNICODE标识符,则__T宏只简单地定义如下:

#define __T(x) x        

此外,还有两个宏与__T定义相同:

#define _T(x)__T(x)        
#define _TEXT(x)__T(x)        

在Win32 console程序中使用哪个宏,取决于您喜欢简洁还是详细。基本地,必须按下述方法在_T或_TEXT宏内定义字符串文字:

_TEXT ("Hello!")        

这样做的话,如果定义了_UNICODE,那么该串将解释为宽字符的组合,否则解释为8位的字符字符串。

宽字符和 Windows

Windows NT从底层支援Unicode。这意味着Windows NT内部使用由16位字符组成的字符串。因为世界上其它许多地方还不使用16位字符串,所以Windows NT必须经常将字符串在操作系统内转换。Windows NT可执行为ASCII、Unicode或者ASCII和Unicode混合编写的程序。即,Windows NT支持不同的API函数呼叫,这些函数接受8位或16位的字符串(我们将马上看到这是如何动作的。)

相对于Windows NT,Windows 98对Unicode的支持要少得多。只有很少的Windows 98函数呼叫支持宽字符串(这些函数列在《Microsoft Knowledge Base article Q125671》中;它们包括MessageBox)。如果要发行的程序中只有一个.EXE文件要求在Windows NT和Windows 98下都能执行,那么就不应该使用Unicode,否则就不能在Windows 98下执行;尤其程序不能呼叫Unicode版的Windows函数。这样,将来发行Unicode版的程序时会处于更有利的位置,您应试着编写既为ASCII又为Unicode编译的原始码。这就是本书中所有程序的编写方式。

Windows表头文件类型

正如您在第一章所看到的那样,一个Windows程序包括表头文件WINDOWS.H。该文件包括许多其它表头文件,包括WINDEF.H,该文件中有许多在Windows中使用的基本型态定义,而且它本身也包括WINNT.H。WINNT.H处理基本的Unicode支持。

WINNT.H的前面包含C的表头文件CTYPE.H,这是C的众多表头文件之一,包括wchar_t的定义。WINNT.H定义了新的数据型态,称作CHAR和WCHAR:

typedef char CHAR ;        
typedef wchar_t WCHAR ;    // wc        

当您需要定义8位字符或者16位字符时,推荐您在Windows程序中使用的数据型态是CHAR和WCHAR。WCHAR定义后面的注释是匈牙利标记法的建议:一个基于WCHAR数据型态的变量可在前面附加上字母wc以说明一个宽字符。

WINNT.H表头文件进而定义了可用做8位字符串指针的六种数据型态和四个可用做const 8位字符串指针的数据型态。这里精选了表头文件中一些实用的说明数据型态语句:

typedef CHAR * PCHAR, * LPCH, * PCH, * NPSTR, * LPSTR, * PSTR ;        
typedef CONST CHAR * LPCCH, * PCCH, * LPCSTR, * PCSTR ;        

前缀N和L表示「near」和「long」,指的是16位Windows中两种大小不同的指标。在Win32中near和long指标没有区别。

类似地,WINNT.H定义了六种可作为16位字符串指针的数据型态和四种可作为const 16位字符串指针的数据型态:

typedef WCHAR * PWCHAR, * LPWCH, * PWCH, * NWPSTR, * LPWSTR, * PWSTR ;        
typedef CONST WCHAR * LPCWCH, * PCWCH, * LPCWSTR, * PCWSTR ;        

至此,我们有了数据型态CHAR(一个8位的char)和WCHAR(一个16位的wchar_t),以及指向CHAR和WCHAR的指标。与TCHAR.H一样,WINNT.H将TCHAR定义为一般的字符类型。如果定义了标识符UNICODE(没有底线),则TCHAR和指向TCHAR的指标就分别定义为WCHAR和指向WCHAR的指标;如果没有定义标识符UNICODE,则TCHAR和指向TCHAR的指标就分别定义为char和指向char的指标:

#ifdef  UNICODE 
typedef WCHAR TCHAR, * PTCHAR ;
typedef LPWSTR LPTCH, PTCH, PTSTR, LPTSTR ; 
typedef LPCWSTR LPCTSTR ;      
#else
typedef char TCHAR, * PTCHAR ;  
typedef LPSTR LPTCH, PTCH, PTSTR, LPTSTR ; 
typedef LPCSTR LPCTSTR ;   
#endif
        

如果已经在某个表头文件或者其它表头文件中定义了TCHAR数据型态,那么WINNT.H和WCHAR.H表头文件都能防止其重复定义。不过,无论何时在程序中使用其它表头文件时,都应在所有其它表头文件之前包含WINDOWS.H。

WINNT.H表头文件还定义了一个宏,该宏将L添加到字符串的第一个引号前。如果定义了UNICODE标识符,则一个称作 __TEXT的宏定义如下:

#define __TEXT(quote) L##quote        

如果没有定义标识符UNICODE,则像这样定义__TEXT宏:

#define __TEXT(quote) quote        

此外, TEXT宏可这样定义:

#define TEXT(quote) __TEXT(quote)        

这与TCHAR.H中定义_TEXT宏的方法一样,只是不必操心底线。我将在本书中使用这个宏的TEXT版本。

这些定义可使您在同一程序中混合使用ASCII和Unicode字符串,或者编写一个可被ASCII或Unicode编译的程序。如果您希望明确定义8位字符变量和字符串,请使用CHAR、PCHAR(或者其它),以及带引号的字符串。为明确地使用16位字符变量和字符串,请使用WCHAR、PWCHAR,并将L添加到引号前面。对于是8位还是16位取决于UNICODE标识符的定义的变量或字符串,要使用TCHAR、PTCHAR和TEXT宏。

Windows函数呼叫

从Windows 1.0到Windows 3.1的16位Windows中,MessageBox函数位于动态链接库USER.EXE。在Windows 3.1软件开发套件的WINDOWS.H中,MessageBox函数定义如下:

int WINAPI MessageBox (HWND, LPCSTR, LPCSTR, UINT) ;        

注意,函数的第二个、第三个参数是指向常数字符串的指针。当编译连结一个Win16程序时,Windows并不处理MessageBox呼叫。程序.EXE文件中的表格,允许Windows将该程序的呼叫与USER中的MessageBox函数动态链接起来。

32位的Windows(即所有版本的Windows NT,以及Windows 95和Windows 98)除了含有与16位兼容的USER.EXE以外,还含有一个称为USER32.DLL的动态链接库,该动态链接库含有32位使用者接口函数的进入点,包括32位的MessageBox。

这就是Windows支持Unicode的关键:在USER32.DLL中,没有32位MessageBox函数的进入点。实际上,有两个进入点,一个名为MessageBoxA(ASCII版),另一个名为MessageBoxW(宽字符版)。用字符串作参数的每个Win32函数都在操作系统中有两个进入点!幸运的是,您通常不必关心这个问题,程序中只需使用MessageBox。与TCHAR表头文件一样,每个Windows表头文件都有我们需要的技巧。

下面是MessageBoxA在WINUSER.H中定义的方法。这与MessageBox早期的定义很相似:

WINUSERAPI int WINAPI MessageBoxA (HWND hWnd, LPCSTR lpText, 
                           LPCSTR lpCaption, UINT uType) ;
        

下面是MessageBoxW:

WINUSERAPI int WINAPI MessageBoxW (HWND hWnd, LPCWSTR lpText,
        
                           LPCWSTR lpCaption, UINT uType) ;
        

注意,MessageBoxW函数的第二个和第三个参数是指向宽字符的指针。

如果需要同时使用并分别匹配ASCII和宽字符函数呼叫,那么您可在Windows程序中明确地使用MessageBoxA和MessageBoxW函数。但大多数程序写作者将继续使用MessageBox。根据是否定义了UNICODE,MessageBox将与MessageBoxA或MessageBoxW一样。在WINUSER.H中完成这一技巧时,程序相当琐碎:

#ifdef UNICODE
        
#define MessageBox  MessageBoxW
        
#else
        
#define MessageBox  MessageBoxA
        
#endif
        

这样,如果定义了UNICODE标识符,那么程序中所有的MessageBox函数呼叫实际上就是MessageBoxW函数;否则,就是MessageBoxA函数。

执行该程序时,Windows将程序中不同的函数呼叫与不同的Windows动态链接库的进入点连结。虽然只有少数例外,但是,在Windows 98中不能执行Unicode版的Windows函数。虽然这些函数有进入点,但通常返回错误代码。应用程序注意这些返回的错误并采取一些合理的动作。

Windows的字符串函数

正如前面谈到的,Microsoft C包括宽字符和需要字符串参数的C语言执行时期链接库函数的所有普通版本。不过,Windows复制了其中一部分。例如,下面是Windows定义的一组字符串函数,这些函数用来计算字符串长度、复制字符串、连接字符串和比较字符串:

ILength = lstrlen (pString) ;
        
pString = lstrcpy (pString1, pString2) ;
        
pString = lstrcpyn (pString1, pString2, iCount) ;
        
pString = lstrcat (pString1, pString2) ;
        
iComp = lstrcmp (pString1, pString2) ;
        
iComp = lstrcmpi (pString1, pString2) ;
        

这些函数与C链接库中对应的函数功能相同。如果定义了UNICODE标识符,那么这些函数将接受宽字符串,否则只接受常规字符串。宽字符串版的lstrlenW函数可在Windows 98中执行。

在Windows中使用printf

有文字模式、命令列C语言程序写作历史的程序写作者往往特别喜欢printf函数。即使可以使用更简单的命令(例如puts),但printf出现在Kernighan和Ritchie的「hello, world」程序中一点也不会令人惊奇。我们知道,增强后的「hello, world」最终还是需要printf的格式化输出,因此我们最好从头开始就使用它。

但有个坏消息:在Windows程序中不能使用printf。虽然Windows程序中可以使用大多数C的执行时期链接库-实际上,许多程序写作者更愿意使用C内存管理和文件I/O函数而不是Windows中等效的函数-Windows对标准输入和标准输出没有概念。在Windows程序中可使用fprintf,而不是printf。

还有一个好消息,那就是仍然可以使用sprintf及sprintf系列中的其它函数来显示文字。这些函数除了将内容格式化输出到函数第一个参数所提供的字符串缓冲区以外,其功能与printfI相同。然后便可对该字符串进行操作(例如将其传给MessageBox)。

如果您从未使用过sprintf (我第一次开始写Windows程序时也没用过此函数),这里有一个简短的执行实体,printf函数说明如下:

int printf (const char * szFormat, ...) ;
        

第一个参数是一个格式字符串,后面是与格式字符串中的代码相对应的不同类型多个参数。

sprintf函数定义如下:

int sprintf (char * szBuffer, const char * szFormat, ...) ;
        

第一个参数是字符缓冲区;后面是一个格式字符串。Sprintf不是将格式化结果标准输出,而是将其存入szBuffer。该函数返回该字符串的长度。在文字模式程序设计中,

printf ("The sum of %i and %i is %i", 5, 3, 5+3) ;
        

的功能相同于

char szBuffer [100] ;
        
sprintf (szBuffer, "The sum of %i and %i is %i", 5, 3, 5+3) ;
        
puts (szBuffer) ;
        

在Windows中,使用MessageBox显示结果优于puts。

几乎每个人都经历过,当格式字符串与被格式化的变量不合时,可能使printf执行错误并可能造成程序当掉。使用sprintf时,您不但要担心这些,而且还有一个新的负担:您定义的字符串缓冲区必须足够大以存放结果。Microsoft专用函数_snprintf解决了这一问题,此函数引进了另一个参数,表示以字符计算的缓冲区大小。

vsprintf是sprintf的一个变形,它只有三个参数。vsprintf用于执行有多个参数的自订函数,类似printf格式。vsprintf的前两个参数与sprintf相同:一个用于保存结果的字符缓冲区和一个格式字符串。第三个参数是指向格式化参数数组的指针。实际上,该指针指向在堆栈中供函数呼叫的变量。va_list、va_start和va_end宏(在STDARG.H中定义)帮助我们处理堆栈指针。本章最后的SCRNSIZE程序展示了使用这些宏的方法。使用vsprintf函数,sprintf函数可以这样编写:

int sprintf (char * szBuffer, const char * szFormat, ...)
        
{
        
    int     iReturn ;
        
    va_list pArgs ;
        
    va_start (pArgs, szFormat) ;
        
    iReturn = vsprintf (szBuffer, szFormat, pArgs) ;
        
    va_end (pArgs) ;
        
    return iReturn ;
        
}
        

va_start宏将pArg设置为指向一个堆栈变量,该变量地址在堆栈参数szFormat的上面。

由于许多Windows早期程序使用了sprintf和vsprintf,最终导致Microsoft向Windows API中增添了两个相似的函数。Windows的wsprintf和wvsprintf函数在功能上与sprintf和vsprintf相同,但它们不能处理浮点格式。

当然,随着宽字符的发表,sprintf类型的函数增加许多,使得函数名称变得极为混乱。表2-1列出了Microsoft的C执行时期链接库和Windows支持的所有sprintf函数。

表2-1

ASCII

宽字符

常规

参数的变数个数

标准版

sprintf

swprintf

_stprintf

最大长度版

_snprintf

_snwprintf

_sntprintf

Windows版

wsprintfA

wsprintfW

wsprintf

参数数组的指针

标准版

vsprintf

vswprintf

_vstprintf

最大长度版

_vsnprintf

_vsnwprintf

_vsntprintf

Windows版

wvsprintfA

wvsprintfW

wvsprintf

在宽字符版的sprintf函数中,将字符串缓冲区定义为宽字符串。在宽字符版的所有这些函数中,格式字符串必须是宽字符串。不过,您必须确保传递给这些函数的其它字符串也必须由宽字符组成。

格式化消息框

程序2-1所示的SCRNSIZE程序展示了如何实作MessageBoxPrintf函数,该函数有许多参数并能像printf那样编排它们的格式。

程序2-1  SCRNSIZE
        
SCRNSIZE.C
        
/*---------------------------------------------------------------------------
        
    SCRNSIZE.C -- Displays screen size in a message box
        
                 (c) Charles Petzold, 1998
        
----------------------------------------------------------------------------*/
        
#include <windows.h>
        
#include <tchar.h>   
        
#include <stdio.h>   
        
int CDECL MessageBoxPrintf (TCHAR * szCaption, TCHAR * szFormat, ...)
        
{
        
    TCHAR   szBuffer [1024] ;
        
    va_list pArgList ;
        

    // The va_start macro (defined in STDARG.H) is usually equivalent to:
        
    // pArgList = (char *) &szFormat + sizeof (szFormat) ;
        

    va_start (pArgList, szFormat) ;
        

    // The last argument to wvsprintf points to the arguments
        

    _vsntprintf ( szBuffer, sizeof (szBuffer) / sizeof (TCHAR),
        
                   szFormat, pArgList) ;
        

    // The va_end macro just zeroes out pArgList for no good reason
        
    va_end (pArgList) ;
        
    return MessageBox (NULL, szBuffer, szCaption, 0) ;
        
}
        
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
        
                   PSTR szCmdLine, int iCmdShow)
        
{
        
    int cxScreen, cyScreen ;
        
    cxScreen = GetSystemMetrics (SM_CXSCREEN) ;
        
    cyScreen = GetSystemMetrics (SM_CYSCREEN) ;
        

    MessageBoxPrintf (    TEXT ("ScrnSize"),
        
                   TEXT ("The screen is %i pixels wide by %i pixels high."),
        
                   cxScreen, cyScreen) ;
        
    return 0 ;
        
}
        

经由从GetSystemMetrics函数得到的信息,该程序以图素为单位显示了视讯显示的宽度和高度。GetSystemMetrics是一个能用来获得Windows中不同对象的尺寸信息的函数。事实上,我将在第四章用GetSystemMetrics函数向您展示如何在一个Windows窗口中显示和滚动多行文字。

本书与国际化

为国际市场准备的Windows程序不光要使用Unicode。国际化超出了本书的范围,但在Nadine Kano所写的《Developing International Software for Windows 95 and Windows NT》(Microsoft Press,1995年)一书中涉猎了许多。

本书中的程序写作时被限制成既可使用也可不使用定义的UNICODE标识符来编译。这包括对所有字符和字符串定义使用TCHAR,对字符串文字使用TEXT宏,以及注意不要混淆字节和字符。例如,注意SCRNSIZE中的 _vsntprintf呼叫。第二个参数是缓冲区的字符大小。通常,您使用sizeof (szBuffer)。但如果缓冲区中有宽字符,则返回的不是缓冲区的字符长度,而是缓冲区的字节大小。您必须用sizeof(TCHAR)将其分开。

通常,在Visual C++ Developer Studio中,可使用两种不同的设定来编译程序:Debug和Release。为简便起见,对本书的范例程序,我已修改了Debug设定,以便于定义UNICODE标识符。如果程序使用了需要字符串作参数的C链接库函数,那么_UNICODE标识符也在Debug设定中定义(要了解这是在哪里完成的,请从「Project」菜单中选择「Settings」,然后单击「C/C++」标签)。使用这种方式,这些程序就可以方便地被重新编译和连结以供测试。

本书中所有程序-无论是否为Unicode编译-都可以在Windows NT下执行。只有极少数情况例外。本书中按Unicode编译的程序不能在Windows 98中执行,而非Unicode版则可以。本章和第一章的程序就是两个特例。MessageBoxW是Windows 98支持的少数宽字符Windows函数之一。在SCRNSIZE.C中,如果用Windows函数wprintf代替了_vsntprintf(您还必须删除该函数的第二个参数),那么SCRNSIZE.C的Unicode版将不能在Windows 98下执行,这是因为Windows 98不支持wprintfW。

在本书的后面(特别在第六章,介绍键盘的使用时),我们将看到,编写能处理远东版Windows双字符集的Windows程序不是一件容易的事情。本书没有说明如何去做,并且基于这个原因,本书中的某些非Unicode版本的程序在远东版的Windows下不能正常执行。这也是Unicode对将来的程序设计如此重要的一条理由。Unicode允许程序更容易地跨越国界。