宽字元和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 ;
posted on 2009-04-30 15:37
游子 阅读(380)
评论(0) 编辑 收藏 引用