字符集
软件的本地化要解决的真正问题,实际上就是如何来处理不同的字符集。多年来,许多人
一直将文本串作为一系列单字节字符来进行编码,并在结尾处放上一个零。对于我们来说,这
已经成了习惯。当调用s t r l e n函数时,它在以0结尾的单字节字符数组中返回字符的数目。
问题是,有些文字和书写规则(比如日文中的汉字就是个典型的例子)的字符集中的符号
太多了,因此单字节(它提供的符号最多不能超过2 5 6个 2<*8)是根本不敷使用的。为此出现了双
字节字符集(D B C S),以支持这些文字和书写规则。
在双字节字符集中,字符串中的每个字符可以包含一个字节或包含两个字节。例如,日文
中的汉字,如果第一个字符在0 x 8 1与0 x 9 F之间,或者在0 x E 0与0 x F C之间,那么就必须观察下
一个字节,才能确定字符串中的这个完整的字符。使用双字节字符集,对于程序员来说简直是
个很大的难题,因为有些字符只有一个字节宽,而有些字符则是两个字节宽。
如果只是调用s t r l e n函数,那么你无法真正了解字符串中究竟有多少字符,它只能告诉你
到达结尾的0之前有多少个字节。
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 显示了这样的区域的一部分以及分配给这些
区域的字符。
表2-2 区域字符
1 6位代码字符 16 位代码字符
0 0 0 0 - 0 0 7 F A S C I I 0 3 0 0 - 0 3 6 F 通用区分标志
0 0 8 0 - 0 0 F F 拉丁文1字符 0 4 0 0 - 0 4 F F 西里尔字母
0 1 0 0 - 0 1 7 F 欧洲拉丁文 0 5 3 0 - 0 5 8 F 亚美尼亚文
0 1 8 0 - 0 1 F F 扩充拉丁文 0 5 9 0 - 0 5 F F 西伯莱文
0 2 5 0 - 0 2 A F 标准拼音 0 6 0 0 - 0 6 F F 阿拉伯文
0 2 B 0 - 0 2 F F 修改型字母 0 9 0 0 - 0 9 7 F 梵文
目前尚未分配的代码点大约还有29 000个,不过它们是保留供将来使用的。另外,大约有
6 0 0 0个代码点是保留供个人使用的。
当开发应用程序时,当然应该考虑利用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文件。
• 提高应用程序的运行效率为什么使用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文件。
• 提高应用程序的运行效率(本章后面还要详细介绍)。
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 wchar_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函数。
char *strcat(char *, const char *);
wchar_t * wcscat(wchar_t *, const wchar_t *);
注意大多数软件开发人员可能已经不记得这样一个非常重要的问题了,那就是
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
进行编译的源代码。
字符串(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文件中做了定义。
成为符合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 )。在上面所说的所有原则中,这是最难记住的一条原则,如果操作错误,编译器
将不发出任何警告。
在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用于将多字节字符串转换成宽字符串。
成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将宽字符串转换成等价的多字节字符串