Unicode 字符集学习笔记

字符集

软件的本地化要解决的真正问题,实际上就是如何来处理不同的字符集。多年来,许多人一直将文本串作为一系列单字节字符来进行编码,并在结尾处放上一个零。对于我们来说,这已经成了习惯。当调用s t r l e n函数时,它在以0结尾的单字节字符数组中返回字符的数目。
问题是,有些文字和书写规则(比如日文中的汉字就是个典型的例子)的字符集中的符号太多了,因此单字节(它提供的符号最多不能超过 2 5 6个)是根本不敷使用的。为此出现了双字节字符集( 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之前有多少个字节。 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 字符串,方法是每次一个字符。第三个函数sDBCSLeadByte, 在字节返回到一个两字字节符的第一个字节时将返回T R U E。

Unicode:宽字节字符集

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源代码

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),就可以修改然后重新编译该源文件。

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 *, wchar_t);

int strchr(const char *, int);
wchar_t wcschr(const wchar_t *, const wchar_t);

int strcmp(char , const char );
wchar_t wcscmp(wchar_t , const wchar_t );

size_t strlen(const char *);
size_t wcslen(const wchar_t *);
标准的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,
只需编写下面的代码即可:

if (szError[0] == _TEXT(“J”))
{
//first char is J
}
else
{
//first char is not J
}

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

                            表2-3 Uincode 数据类型 
数 据 类 型                                                       说 明 
W C H A R                                                   U n i c o d e字符 
P W S T R                                               指向U n i c o d e字符串的指针
P C W S T R                                     指向一个恒定的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通用数
据类型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头文件。当编译源代码模块时,通常必须同时定义这两个宏。

成为符合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用于将多字节字符串转换成宽字符串。下面显示了M u l t i B y t e To Wi d e C h a r函数。
int MultiByteToWideChar(
UINT CodePage,
DWORD dwFlags,
LPCSTR lpMultiByteStr,
int cchMultiByte,
LPWSTR lpWideCharStr,
int cchWideChar
);
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将宽字符串转换成等价的多字节字符串,如下所示:
int WideCharToMultiByte(
UINT CodePage, //指定执行转换的代码页
DWORD dwFlags, //允许你进行额外的控制,它会影响使用了读音符号(比如重音)的字符
LPCWSTR lpWideCharStr, //指定要转换为宽字节字符串的缓冲区
int cchWideChar, //指定由参数lpWideCharStr指向的缓冲区的字符个数
LPSTR lpMultiByteStr, //指向接收被转换字符串的缓冲区
int cchMultiByte, //指定由参数lpMultiByteStr指向的缓冲区最大值
LPCSTR lpDefaultChar, //遇到一个不能转换的宽字符,函数便会使用pDefaultChar参数指向的字符
LPBOOL pfUsedDefaultChar //至少有一个字符不能转换为其多字节形式,函数就会把这个变量设为TRUE
);
该函数与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参数,那么该函数将返回目标缓存需要的大小值。通常可以使用将多字节字符串转换成宽字节字符串时介绍的一系列类似的事件,将宽字节字符串转换成多字节字符串。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章