setlocale 與 mbstowcs 的問題


C++的字符串轉換函數mbstowcs使用時容易產生bug。。。

rapidxml_utils.hpp 的file(const char*filename)函數內會異常宕機。。。

需要在函數最開始添加

locale::global(locale(""));

 

 

 

from  http://blog.sina.com.cn/s/blog_55c1b83b0100wbah.html

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_twc_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()); }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM