Unicode的C/C++编程
Unicode的最大好处是这里只有一个字符集,通俗一点讲就是说使用Unicode字符编码的程序可以在任何国语言的编译环境下编译通过,而不会被认为是乱码,也可以使任何语言的编辑环境下正常显示字符,而不是乱码。Unicode有缺点吗?当然有。Unicode字符串占用的内存是ASCII字符串的两倍,然而压缩文件有助于极大地减少文件所占的磁盘空间。
对于C编程,在处理有关字符数据操作时,可以用宽字符数据类型来增加对Unicode编程的支持,从而实现多国语言版本的程序。
假定我们都非常熟悉在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 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.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宏。