• 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(...),比如:

    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: 
    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系列
    例: 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. 其他 ★★★★★ ★★★☆☆ -
  • 相关阅读:
    [ERR] Node 10.211.55.8:7001 is not empty. Either the node already knows other nodes (check with CLUSTER NODES) or contains some key in database 0.
    PAT A1137 Final Grading (25 分)——排序
    PAT A1136 A Delayed Palindrome (20 分)——回文,大整数
    PAT A1134 Vertex Cover (25 分)——图遍历
    PAT A1133 Splitting A Linked List (25 分)——链表
    PAT A1132 Cut Integer (20 分)——数学题
    PAT A1130 Infix Expression (25 分)——中序遍历
    PAT A1142 Maximal Clique (25 分)——图
    PAT A1141 PAT Ranking of Institutions (25 分)——排序,结构体初始化
    PAT A1140 Look-and-say Sequence (20 分)——数学题
  • 原文地址:https://www.cnblogs.com/Dageking/p/3791494.html
Copyright © 2020-2023  润新知