zoukankan      html  css  js  c++  java
  • 字节转换

    setlocale 与 mbstowcs 的问题

    Qiang Post in 编程,Tags: c++i18n

    1 问题

    在 Windows XP 多语言简中环境下,用 VC2005 中的 std::fstream 打开中文名文件,系统报错找不到此文件。

    std::ifstream file("\xd6\xd0.txt"); // GBK 编码的 "中.txt"
    if (!file)
    {
       std::cerr << "Cannot open file!"; // Oops!
    }

     

    2 原因

    在 VC2005 中 std::fstream 的打开文件的函数实现里,传入的 char const* 文件名作为多字节首先被 mbstowcs 转换成宽字节后,再转发给 Unicode 版本的 API 进行实际的打开文件操作。见 fiopen.cpp:

    _MRTIMP2_NCEEPURE FILE *__CLRCALL_PURE_OR_CDECL _Fiopen(const char *filename,
        ios_base::openmode mode, int prot)
        {    // open wide-named file with byte name
        wchar_t wc_name[FILENAME_MAX];
     
        if (mbstowcs_s(NULL, wc_name, FILENAME_MAX, filename, FILENAME_MAX - 1) != 0)
            return (0);
        return _Fiopen(wc_name, mode, prot);
        }

    问题的关键在于,对于 mbstowcs 函数来说,它需要知道多字节的编码类型才能正确的将其转换成宽字节的 unicode,很可惜这个编码类型并没有体现在函数的参数列表里,而是隐含依赖全局的 locale 。更加不幸的是,全局 locale 默认没有使用系统当前语言,而是设置为没什么用处的 “C” locale 。于是 GBK 编码的文件名在 “C” locale 下转换错误,悲剧发生了……

    3 解

    知道了原因,解就很简单了。在调用 mbstowcs 或使用它的函数之前,先用 setlocale 将全局默认 locale 设为当前系统默认 locale :

    setlocale(LC_ALL, "");

    如果是在非中文系统上转 GBK 编码,就需要指定中文 locale :

    setlocale(LC_ALL, "chs"); // chs 是 VC 里简中的 locale 名字

    还有一种方法,直接使用宽字节版本的API,之前的编码由自己转换好,避免系统语言环境设置的影响。在 VS2005 中 fstream 有个扩展,可以直接打开宽字节文件名:

    std::ifstream file(L"\u4E2D.txt"); // UCS2 编码的“中.txt”

    4 引申

    API 中隐藏依赖关系是不好的,这种隐藏总意谓着外部环境能通过潜规则来影响 API 的功能。这影响了该API的复用性,可测性,也容易让用户出现意外错误。进一步设想一下,如果环境原来的 locale 是被其它代码块故意设置的,如果为了修正打开中文名文件的 Bug 而冒冒然修改当前全局的 locale ,很可能会让依赖于原 locale 工作的代码出现 bug 。在这样的 API 设计下,如果要尽量避免顾此失彼的发生,我们可以在修改前保存当前的 locale ,用完后再恢复回原来的 locale 。在 C++ 里,最好是将这样的逻辑用 RAII 来封装:

    class  scoped_locale
    {
    public:
        scoped_locale(std::string const&amp; loc_name)
            : _new_locale(loc_name)
            , _setted(false)
        {
            try
            {
                char const* old_locale = setlocale(LC_CTYPE, _new_locale.c_str());
     
                if (NULL != old_locale)
                {
                    _old_locale = old_locale;
                    _setted = true;
                }
            }
            catch (...)
            {
            }
        }
     
        ~scoped_locale()
        {
            try
            {
                if (_setted)
                {
                    char const* pre_locale = setlocale(LC_CTYPE, _old_locale.c_str());
     
                    if (pre_locale)
                    {
                        assert(pre_locale == _new_locale);
                        _setted = false;
                    }
                }
            }
            catch (...)
            {
            }
        }
     
    private:
        std::string _new_locale;
        std::string _old_locale;
        bool _setted;
    };

    原代码可以改为:

    {
        scoped_locale change_locale_to("");
        std::ifstream file("\xd6\xd0.txt"); // GBK 编码的“中.txt”
        if (!file)
        {
            std::cerr << "Cannot open file!"; // Oops!
        }
    }

    当然,如果是多线程环境的话,还需要查明 locale 的全局性是进程级的还是线程级的。如果是前者,那还是会有潜在的相互影响的风险。从这点上来看,C/C++ 标准库中 mbstowcs 的设计是有瑕疵的。这也从反面体现了 Dependency Injection 思想的重要性。在 Win32 API 有个类似的函数 WideCharToMultiByte() ,它的作用也是进行多字节到宽字节的编码转换,但在API设计上,它就将 code page 作为第一个入参显示传入,而不是默认使用全局系统的某个状态。用它来写一个通用的转换函数就可以避免 mbstowcs 的问题了:

    std::wstring native_to_utf16(std::string const& native_string)  
    {  
        UINT const codepage = CP_ACP;  
        DWORD const sizeNeeded = MultiByteToWideChar(  
            codepage, 0, native_string.c_str(), -1, NULL, 0);  
     
        std::vector<wchar_t> buffer(sizeNeeded, 0);  
     
        if (0 == MultiByteToWideChar(codepage, 0,  
                native_string.c_str(), -1,  
                &buffer[0], buffer.size()))
        {  
            throw std::runtime_error("wrong convertion from native string to utf16");  
        }  
     
        return std::wstring(buffer.begin(), buffer.end());
    }
  • 相关阅读:
    61. 最长不含重复字符的子字符串
    60. 礼物的最大价值 (未理解)
    59. 把数字翻译成字符串
    58. 把数组排成最小的数
    57. 数字序列中某一位的数字 (不懂)
    spring data jpa 官方文档
    idea 编译报错 源发行版 1.8 需要目标发行版 1.8
    idea maven 依赖报错 invalid classes root
    solr
    spring boot 官方文档
  • 原文地址:https://www.cnblogs.com/BloodAndBone/p/2086065.html
Copyright © 2011-2022 走看看