Unicode化

为了程序编写方便,根除乱码问题等等需求,不少新项目都采用了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. 字符串定义编程

MBCS Unicode 兼容MBCS和Unicode
char WCHAR TCHAR
char* LPWSTR LPTSTR
LPSTR LPWSTR LPTSTR
const char* LPCWSTR LPCTSTR
LPCSTR LPCWSTR LPCTSTR

※兼容MBCS和Unicode: 根据第一步的工程属性切换来决定采用MBCS仍是Unicode
是一种比较好的,切换后无需修改代码的类型定义。不过也会带来必定的麻烦(稍后17点会针对这点进行讨论)。安全

 

 

3. 字符串常量定义须要在字符串两端加上L...,兼容模式的话则是_T(...)或者TEXT(...),好比:ide

MBCS Unicode 兼容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: 
wsprintfcode

 

 

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系列
例: gethostname
TCP/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. 其余 ★★★★★ ★★★☆☆ -
相关文章
相关标签/搜索