Unicode化
作者:斑鸠更新时间:2009/08/21
为了程序编写方便,根除乱码问题等等需求,很多新项目都采用了Unicode编码。同时,不少使用MBCS多字节编码的旧项目为了升级,也有了转向Unicode编码的意向。不过,从MBCS升级到Unicode并不是无缝的,该问题的复杂程度,取决于代码总量和代码的编写质量。本文是作者在一个C/C++项目中的一些经验之谈,希望对有此需求的读者带来帮助。
1. 工程属性切换在VC6.0中,切换到Unicode没有直接的选项可以选,需要在宏定义中添加 UNICODE和 _UNICODE, 同时需要 去除MBCS宏定义。另外,如果生成的是exe的程序的话,还需要定义入口函数名 wWinMainCRTStartup。 在VC2003以及之后的IDE环境中,有直接选者使用UNICODE还是MBCS的选项,无需添加宏定义。 【设定场所】 VC6.0: 主菜单 - 工程 - 设定 - C/C++标签(共通) - 宏定义 主菜单 - 工程 - 设定 - 链接标签(输出) - 入口符号 VC2003: 主菜单 - 工程 - 属性 - 共通 - 字符集
2. 字符串定义 MBCSUnicode兼容MBCS和UnicodecharWCHARTCHARchar*LPWSTRLPTSTRLPSTRLPWSTRLPTSTRconst char*LPCWSTRLPCTSTRLPCSTRLPCWSTRLPCTSTR ※兼容MBCS和Unicode: 根据第一步的工程属性切换来决定采用MBCS还是Unicode 是一种比较好的,切换后无需修改代码的类型定义。不过也会带来一定的麻烦(稍后 17点会针对这点进行讨论)。
3. 字符串常量定义需要在字符串两端加上L...,兼容模式的话则是_T(...)或者TEXT(...),比如: MBCSUnicode兼容MBCS和Unicode"Clannad"L"Clannad"_T("Clannad")或者TEXT("Clannad") ※对于宏定义 __FILE__,如果要使得这个宏定义变成Unicode的话,必须使用TEXT(...) 采用_T(...)会产生编译错误。
4. 字符串文字数计算在MBCS中,我们采用strlen来计算一个字符串的长度,其实结果是字节数,而非文字数(纯英文数字除外)。 而在Unicode中,我们可以采用lstrlen来计算一个字符串的长度,其结果是文字数,恰好是字节数的一半。 但有时候我们需要获得变量可以容纳文字数的长度,比如LoadString(),其中最后一个参数需要我们传入 可容纳最大的文字数长度,在MBCS中,我们常常这么写sizeof(buffer),不过在Unicode中, 这样写的话就有可能会导致内存溢出,所以更好的写法是 sizeof(buffer) / sizeof(TCHAR)或者 sizeof(buffer) / sizeof(buffer[0]) 个人推荐后者,理由是仍然在Unicode环境下,如果buffer因为某些原因从TCHAR变回了char, 那么后者能正常工作,前者会因为错误的字符串变量文字数而导致字符串截断。 ※另外需要注意的是,在Windows API中如果是针对内存操作的函数,比如memcpy, memset等等, 那么sizeof(buffer)是正确的,因为函数需要传入字节数,而不是文字数 只有在API函数需要的文字数的时候才需要作此更改,对于参数的详细信息,可以参考MSDN后再做判断
5. 字符串函数的替换最早的一些字符串处理函数,比如: strcpy, strlen等等因为都是针对ANSI的( strstr这类搜索函数在处理MBCS字符串时可能会产生错误,所以这些函数本身并不是MBCS向的),在更换成Unicode后, 这些常用函数也多了许多新版本,不单单是针对Unicode,而且增加安全性等方面也作了改进。 在这里列出来的话可能会占用不少篇幅,而且也很难整理全,所以在此直接提供MSDN的地址。 String Manipulation (CRT)
6. Windows API函数对于Windows API函数来说,通常涉及字符串的函数都有A和W两个版本,比如: CreateFileA和CreateFileW。 这两个虽然对我们来说是可引用的,但由于Windows头文件的屏蔽,我们经常使用CreateFile来进行编程。 而根据第一步的工程属性切换,代码中会自动替换成A版本或者W版本。因此对于这点我们无须操心太多, 唯一需要操心的就是,无法对应的Unicode的地方,我们必须采用A版本来处理某些操作,这就需要我们 显式指定A版本了,因为工程属性的关系,CreateFile总是被映射到CreateFileW上。
7. 推荐使用 wsprintf对于格式化字符串,这个函数提供了很好的Unicode和MBCS的兼容性。 此函数在Unicode和MBCS下都能正常工作,因为它的两个参数为LPTSTR和LPCTSTR。 其次,在MBCS下 %s表示MBCS,%S表示Unicode, 在Unicode下 %S表示MBCS,%s表示Unicode。 因此,采用这个函数进行字符串格式化的话,基本上是不需要修正就能使用的。 相关的资料请参见MSDN: wsprintf
8. WideCharToMultiByte和MultiByteToWideChar为了在MBCS和Unicode之间转换,Windows API提供了这两个函数。 基本上工程一大,总会遇到不能彻底Unicode化的情况,这个时候就用借用这两个函数的力量了。 MultiByteToWideChar WideCharToMultiByte 使用例: WideCharToMultiByte(CP_OEMCP,NULL,szSrc,-1,szDest,dwLen,NULL,NULL); ※建议:可以先以dwLen = WideCharToMultiByte(CP_OEMCP,NULL,szSrc,-1,NULL,0,NULL,NULL) 的形式获得szSrc转换后的字节数(包含/0),然后分配内存后再做字符集转换。 MultiByteToWideChar(CP_ACP,0,szSrc,-1,szDest,dwLen); 同样建议先获得szDest所需内存大小(包含/0)后分配内存再做转换。
9. MBCS专用函数在MBCS的程序中,因为str*系的函数只对应ANSI,对MBCS使用后往往会产生错误的结果, 所以往往采用几个MBCS专用函数来进行纠正。不过由于这些函数的引入,往往导致Unicode化繁琐化。 IsDBCSLeadByte和IsDBCSLeadByteEx这两个函数用来判断当前字节是不是MBCS的前导字节, 常常在截断字符串时,不知道截断点是不是一个双字节MBCS的正中间的时候使用。 对于Unicode来说,正常的操作永远不会截到一个双字节的Unicode字符的正中间, 而且这两个函数指针对MBCS字符,对于Unicode字符使用的话,后果是无法估计的。 所以, 在Unicode化时,需要把这些函数剔除,然后重新整理处理逻辑。 与此内容相关的几个资料: Unicode and Character Set Functions Character Classification Strings
10. Unicode非对应函数出于某些原因,有些函数并没有提供Unicode的版本。如果无法避免使用到的话,那就需要使用WideCharToMultiByte和MultiByteToWideChar来进行字符集转换。这里列举几个已知常用函数:
GetProcAddress因为DLL的输出函数名都是ANSI形式保存的,所以没有提供Unicode版WinSock系列例: gethostnameTCP/IP协议诞生比较早,而且只对应ANSI,所以提供的函数库自然没有Unicode版了
11. Unicode非对应DLL有时候在程序中调用了第三方的模块,但许多公司的模块只提供了MBCS或者ANSI的接口,对于这种模块,和前一点一样,不得不使用WideCharToMultiByte和MultiByteToWideChar来进行字符集转换。同时要注意,Unicode化修正代码的时候,不要盲目把第三方的头文件一起改掉了。虽然编译会通过,但是链接的时候由于在lib库中找不到完全对应的函数声明,所以最终还是徒劳。
12. CString&的陷阱在使用MFC的时候,经常会使用CString来保存字符串。MFC中提供的CString并没有显式提供A版本和W版本,当工程环境是MBCS的时候,CString保存的是LPSTR,而Unicode的时候,CString保存的是LPWSTR。其实,两种环境下CString类的结构,大小,甚至代码都不是完全一样的。如果把CString&作为一个DLL输出函数的变量类型来声明的话,在Unicode化中会碰到一点小麻烦。当然如果可执行文件和DLL都是MBCS,或者都是Unicode的话没有任何问题,唯一要保证MFC的版本是一样就行了。而如果DLL是MBCS,而可执行文件是Unicode的话,编译能正常通过,但是程序一跑就会出运行时错误。原因就是,可执行文件和DLL的CString是对两种字符集做处理的,两边都认为里面放着自己能处理的字符集字符串。对于这种问题,没有什么特别好的解决方案,唯一可行的方案就是再做一个中间层转换的DLL(MBCS版),接受可执行文件的Unicode字符串(注意不是CString&),转换成MBCS的后放入CString中,再继续调用DLL。
13. 动态调用的陷阱在没有lib库文件,只有dll的情况下,我们往往会采用动态调用,动态调用的函数声明我们会采用typedef来声明。但是typedef的掌控权在自己手里,如果在修正代码的时候,不小心把char改成了TCHAR的话,编译器是不会抛出任何怨言的。因为在进行动态调用的时候,只要有了函数的入口地址就能被调用,编译器只有在静态调用的时候才会检查参数个数和各自的类型,动态调用的时候只管typedef的声明是不是和程序中调用的一致,被调用的DLL中函数的实际类型编译器是管不了,也管不到的。※代码二进制化后,函数的声明信息就被抹除了
14. 指针相减的陷阱两个指针相减,结果并不是两个指针数值上的差,而是把这个差除以指针指向类型的大小的结果。比如: WCHAR pA = 0x00400000, pB = 0x00401000, pB - pA的结果是0x1000 / sizeof(WCHAR) = 0x0800有时候,为了计算字符串的字节数,会采用这种手段。然后在MBCS编码时并没有刻意去考虑指针相减的问题,所以得出的结果不会去除以sizeof(TCHAR)。但是到了Unicode,这种编码显然就是有问题的,弄得不好就是内存泄漏。更何况这种错误因为不会在编译阶段报错,所以要发现变得极其困难。在发现并修正后,唯一能做的也就是吸取教训,以后编码的时候多多注意这类问题了
15. 类型char的滥用这个问题涉及面和影响性也是非常庞大的。在Windows API中,内存指针往往声明成void*,这个类型表示一个泛型,能够接受其他所有类型,也因此很多人习惯性的把内存声明成char*后传入。对于这个看似不严重的的错误声明,在Unicode化的过程中,给编程人员带来极大的麻烦。如果这个地址指向字符串,那当然修改成TCHAR*是正确的,但是如果指向一块结构体内存,那么TCHAR就会把内存扩大成2倍,如果不巧这块内存体的下方有着重要数据的话,一旦发生内存覆盖后,错误会隐藏几分钟,甚至几小时几天后才会暴露,而且无法跟踪。在VC6.0中,有着BYTE这样一个宏定义,窥探一下就会发现其实是unsigned char,虽然和char相差只有一个前缀词,但是足以让维护人员知道这个是表示内存的一个字节,而不是一个ANSI字符。
16. 既存文件的影响文件中的字符和内存中的一样,基于一种字符集后才能被解释。Unicode化的过程中,我们修改了代码,使得运行时数据得到正确运行的同时,也必须注意配置文件,数据文件中的字符集是不是也被一并修改掉了。当然*.ini,注册表以及数据库也有这样的问题,不过因为Windows API,或者数据库链接提供商都已经对字符集做了相应的处理,我们也只需要调用Unicode版本的函数就能迎刃而解。需要得到注意只有那些受到你直接操控的数据文件。
17. LPTSTR的泛滥我们在写程序的时候,经常处于一种免责心理,别人怎么写我就怎么写。别人定义字符串用了LPTSTR,那么我也用这个一定没错!但是呢,程序世界是严谨的,声明如果不恰当的话,必定会引起一些麻烦。如果你正在写一段只能处理MBCS的代码(比如底层第三方的接口只提供了非Unicode版本),那么使用LPTSTR来定义就显得不够准确,虽然在MBCS下编译不会存在任何问题,但一旦这个项目要进行Unicode化的话,LPTSTR就变成了LPWSTR,在接口调用的地方会因为类型不匹配而出现编译错误,实施修改的程序员就不得不对这个糟糕的定义进行重新调整。但这个不是最可怕的,因为至少编译器还能够发现这个错误。最可怕的就是那个底层dll的编写人员也在滥用LPTSTR,那么编译器就会被蒙骗,运气好的话,静态调用在链接过程中发现了不匹配,运气不好的话,动态调用编译阶段不会报任何问题,等到跑起来就有的你够受的。所以,如果你不是在为自己写代码,那么给别人的头文件请一定要选择恰当的类型声明。
18. 其他在实际Unicode化实施过程中,因为项目大小,难度不同还会遇到一些其他零零碎碎的问题。有些可能是可以忽略的,但有些可能是难以追踪并且致命的。对于有经验的人可能会在修改过程中注意到一些问题,但是对于没有经验的人,就要通过调试,查错,参考资料来一步一步解决问题。Unicode化可以说本身就是一种高难度的开发作业,希望通过阅读本文给那些即将要实行Unicode化的程序员一些经验和建议,能在开发过程中尽早发现问题,解决困难。最后对能看完本文的读者说声谢谢~
附个人见解:
修正难度修正范围编译能发现1. 工程属性切换★☆☆☆☆★☆☆☆☆-2. 字符串定义★☆☆☆☆★★★★★○3. 字符串常量定义★☆☆☆☆★★★★★○4. 字符串文字数计算★★☆☆☆★★★☆☆×5. 字符串函数的替换★★☆☆☆★★★★★○6. Windows API函数☆☆☆☆☆☆☆☆☆☆-7. 推荐使用 wsprintf☆☆☆☆☆☆☆☆☆☆-8. WideCharToMultiByte和MultiByteToWideChar★★★☆☆★★☆☆☆○9. MBCS专用函数★★★★☆★☆☆☆☆×10. Unicode非对应函数★★☆☆☆★☆☆☆☆○11. Unicode非对应DLL★★☆☆☆★★☆☆☆○12. CString&的陷阱★★★★★★☆☆☆☆×13. 动态调用的陷阱★★★☆☆★★☆☆☆×14. 指针相减的陷阱★★☆☆☆★★★☆☆×15. 类型char的滥用★★☆☆☆★★★☆☆×16. 既存文件的影响★★☆☆☆★☆☆☆☆×17. LPTSTR的泛滥★★★★★★★★☆☆○18. 其他★★★★★★★★☆☆-