對C++ Local的經典分析

本貼轉載自:再別流年的技術實驗室

文章地址:  http://kittsoft.xp3.biz/?p=86

“這個問題比你想象中復雜”
(我也學下BS的風格,雖然這句話是我自己臨時想說的。^^)
從字符到整數
char是一種整數類型,這句話的含義是,char所能表示的字符在C/C++中都是整數類型。好,接下來,很多文章就會舉出一個典型例子,比如,’a' 的數值就是0×61。這種說法對嗎?如果你細心的讀過K&R和BS對于C和C++描述的原著,你就會馬上反駁道,0×61只是’a'的ASCII 值,并沒有任何規定C/C++的char值必須對應ASCII。C/C++甚至沒有規定char占幾位,只是規定了sizeof(char)等于1。
當然,目前大部分情況下,char是8位的,并且,在ASCII范圍內的值,與ASCII對應。
本地化策略集(locale)
“將’a'翻譯成0×61的整數值”,“將ASCII范圍內的編碼與char的整數值對應起來”,類似這樣的規定,是特定系統和特定編譯器制定 的,C/C++中有個特定的名詞來描述這種規定的集合:本地化策略集(locale。也有翻譯成“現場”)。而翻譯——也就是代碼轉換(codecvt) 只是這個集合中的一個,C++中定義為策略(facet。也有翻譯為“刻面”)
C/C++的編譯策略
“本地化策略集”是個很好的概念,可惜在字符和字符串這個層面上,C/C++并不使用(C++的locale通常只是影響流(stream)),C/C++使用更直接簡單的策略:硬編碼。
簡單的說,字符(串)在程序文件(可執行文件,非源文件)中的表示,與在程序執行中在內存中的表示一致。考慮兩種情況:
A、char c = 0×61;
B、char c = ‘a’;
情況A下,編譯器可以直接認識作為整數的c,但是在情況B下,編譯器必須將’a'翻譯成整數。編譯器的策略也很簡單,就是直接讀取字符(串)在源文件中的編碼數值。比如:
const char* s = “中文abc”;
這段字符串在GB2312(Windows 936),也就是我們的windows默認中文系統源文件中的編碼為:
0xD6   0xD0   0xCE 0xC4 0×61 0×62 0×63
在UTF-8,也就是Linux默認系統源文件中的編碼為:
0xE4   0xB8   0xAD   0xE6   0×96   0×87   0×61   0×62   0×63
一般情況下,編譯器會忠實于源文件的編碼為s賦值,例外的情況比如VC會自作聰明的把大部分其他類型編碼的字符串轉換成GB2312(除了像UTF-8 without signature這樣的幸存者)。
程序在執行的時候,s也就保持是這樣的編碼,不會再做其他的轉換。
寬字符 wchar_t
正如char沒有規定大小,wchar_t同樣沒有標準限定,標準只是要求一個wchar_t可以表示任何系統所能認識的字符,在win32 中,wchar_t為16位;Linux中是32位。wchar_t同樣沒有規定編碼,因為Unicode的概念我們后面才解釋,所以這里只是提一下,在 win32中,wchar_t的編碼是UCS-2BE;而Linux中是UTF-32BE(等價于UCS-4BE),不過簡單的說,在16位以內,一個字 符的這3種編碼值是一樣的。因此:
const wchar_t* ws = L”中文abc”;
的編碼分別為:
0x4E2D   0×6587    0×0061   0×0062   0×0063                                                //win32,16位
0x00004E2D   0×00006587    0×00000061   0×00000062   0×00000063        //Linux,32位
大寫的L是告訴編譯器:這是寬字符串。所以,這時候是需要編譯器根據locale來進行翻譯的。
比如,在Windows環境中,編譯器的翻譯策略是GB2312到UCS-2BE;Linux環境中的策略是UTF-8到UTF-32BE。
這時候就要求源文件的編碼與編譯器的本地化策略集中代碼翻譯的策略一致,例如VC只能讀取GB2312的源代碼(這里還是例外,VC太自作聰明了 ,會將很多其他代碼在編譯時自動轉換成GB2312),而gcc只能讀取UTF-8的源代碼(這里就有個尷尬,MinGW運行win32下,所以只有 GB2312系統才認;而MinGW卻用gcc編寫,所以自己只認UTF-8,所以結果就是,MinGW的寬字符被廢掉了)。
寬字符(串)由編譯器翻譯,還是被硬編碼進程序文件中。

Unicode和UCS
Unicode和UCS是兩個獨立的組織分別制定的一套編碼標準,但是因為歷史的原因,這兩套標準是完全一樣的。Unicode這個詞用得比較多的原因可 能是因為比較容易記住,如果沒有特別的聲明,在本文所提及的Unicode和UCS就是一個意思。Unicode的目標是建立一套可以包含人類所有語言文 字符號你想得到想不到的各種東西的編碼,其編碼容量甚至預留了火星語以及銀河系以外語言的空間——開個玩笑,反正簡單的說,Unicode編碼集足夠的 大,如果用計算機單位來表示,其數量比3個字節大一些,不到4個字節。
Unicode和UTF
因為Unicode包含的內容太多,其編碼在計算機中的表示方法就成為了一個有必要研究的問題。傳統編碼,比如標準的7位ASCII,在計算機中的表示方 法就是占一個字節的后7位,這似乎是不需要解釋就符合大家習慣的表示方法。但是當今Unicode的總數達到32位(計算機的最小單位是字節,所以大于3 字節,就只能至少用4字節表示),對于大部分常用字符,比如Unicode編碼只占一個字節大小的英語字母,占兩個字節大小漢字,都用4個字節來儲存太奢 侈了。另外,如果都用4字節直接表示,就不可避免的出現為0的字節。而我們知道,在C語言中,0×00的字節就是’\0′,表示的是一個字符串(char 字符串,非wchar_t)的結束,換句話說,C風格的char字符串無法表示Unicode。
因為類似的種種問題,為Unicode在計算機中的編碼方法出現了,這就是UTF;所對應的,為UCS編碼實現的方式也有自己的說法。一般來說,UTF- x,x表示這套編碼一個單位至少占用x位,因為Unicode最長達到32位,所以UTF-x通常是變長的——除了UTF-32;而UCS-y表示一個單 位就占用y個字節,所以能表示當今Unicode的UCS-y只有UCS-4,但是因為歷史的原因,當Unicode還沒那么龐大的時候,2個字節足夠表 示,所以有UCS-2,現在看來,UCS-2所能表示的Unicode只是當今Unicode的一個子集。
也就是說,如果某種編碼,能根據一定的規則算法,得到Unicode編碼,那么這種編碼方式就可以稱之為UTF。
UTF-8和Windows GB2312
UTF-8是一套“聰明”的編碼,可能用1,2,3,4個字節表示。通過UTF-8的算法,每一個字節表示的信息都很明確:這是不是某個Unicode編 碼的第一個字節;如果是第一個字節,這是一個幾位Unicode編碼。這種“聰明”被稱為UTF-8的自我同步,也是UTF-8成為網絡傳輸標準編碼的原 因。
另外,UTF-8也不會出現0字節,所以可以表示為char字符串,所以可以成為系統的編碼。Linux系統默認使用UTF-8編碼。
Windows GB2312一般自稱為GB2312,其實真正的名字應該是Windows Codepage 936,這也是一種變長的編碼:1個字節表示傳統的ASCII部分;漢字部分是兩個字節的GBK(國標擴(展),拼音聲母)。Codepage 936也可以表示為char字符串,是中文Windows系統的默認編碼。
我們在第1節中看到的
const char* s = “中文abc”;
在Windows中的編碼就是Codepage 936;在Linux中的編碼就是UTF-8。
需要注意的是,Codepage 936不像UTF,跟Unicode沒有換算的關系,所以只能通過“代碼頁”技術查表對應。
UTF-16和UCS-2
UTF-16用2個字節或者4個字節表示。在2個字節大小的時候,跟UCS-2是一樣的。UTF-16不像UTF-8,沒有自我同步機制,所以,編碼大位 在前還是小位在前,就成了見仁見智的問題。我們在第1節中,“中”的UCS-2BE(因為是兩個字節,所以也就是UTF-16BE)編碼是0x4E2D, 這里的BE就是大位在后的意思(也就是小位在前了),對應的,如果是UCS-2LE,編碼就成了0x2D4E。
Windows中的wchar_t就是采用UCS-2BE編碼。需要指出的是,C++標準中對wchar_t的要求是要能表示所有系統能識別的字符。Windows自稱支持Unicode,但是其wchar_t卻不能表示所有的Unicode,由此違背了C++標準。
UTF-32和UCS-4
UTF-32在目前階段等價于UCS-4,都用定長的4個字節表示。UTF-32同樣存在BE和LE的問題。Linux的wchar_t編碼就是UTF- 32BE。在16位以內的時候,UTF-32BE的后兩位(前兩位是0×00 0×00)等價于UTF-16BE也就等價于UCS-2BE
BOM
為了說明一個文件采用的是什么編碼,在文件最開始的部分,可以有BOM,比如0xFE 0xFF表示UTF-16BE,0xFF 0xFE 0×00 0×00表示UTF-32LE。UTF-8原本是不需要BOM的,因為其自我同步的特性,但是為了明確說明這是UTF-8(而不是讓文本編輯器去猜),也 可以加上UTF-8的BOM:0xEF 0xBB 0xBF
以上內容都講述得很概略,詳細信息請查閱維基百科相關內容。

std::locale
通過前面兩節的知識,我們知道了在C/C++中,字符(串)和寬字符(串)之間的轉換不是簡單的,固定的數學關系,寬窄轉換依賴于本地化策略集 (locale)。換句話說,一個程序在運行之前并不知道系統的本地化策略集是什么,程序只有在運行之后才通過locale獲得當時的本地化策略集。
C有自己的locale函數,我們這里直接介紹C++的locale類。
先討論locale的構造函數:
locale() throw();
這個構造函數是獲得當前程序的locale,用法如下:
std::locale app_loc = std::locale();
或者(這是構造對象的兩種表示方式,后同)
std::locale app_loc;
另外一個構造函數是:
explicit locale(const char* name);
這個構造函數以name的名字創建新的locale。重要的locale對象有:
std::locale sys_loc(“”);      //獲得當前系統環境的locale
std::locale C_loc(“C”);      或者      std::locale C_loc = std::locale::classic();      //獲得C定義locale
std::locale old_loc = std::locale::global(new_loc);      //將new_loc設置為當前全局locale,并將原來的locale返回給old_loc
除了這些,其它的name具體名字依賴于C++編譯器和操作系統,比如Linux下gcc中文系統的locale名字為”zh_CN.UTF-8″,中文Windows可以用”chs”(更加完整的名字可以用name()函數查看)。
mbstowcs()和wcstombs()
這兩個C運行時庫函數依賴于全局locale進行轉換,所以,使用前必須先設置全局locale。
std::locale已經包含在<iostream>中了,再加上我們需要用到的C++字符串,所以包含<string>。
我們先看窄到寬的轉換函數:

  1. const std::wstring s2ws(const std::string& s)   
  2. {   
  3.     std::locale old_loc = std::locale::global(std::locale(""));   
  4.     const char* src_str = s.c_str();   
  5.     const size_t buffer_size = s.size() + 1;   
  6.     wchar_t* dst_wstr = new wchar_t[buffer_size];   
  7.     wmemset(dst_wstr, 0, buffer_size);   
  8.     mbstowcs(dst_wstr, src_str, buffer_size);   
  9.     std::wstring result = dst_wstr;   
  10.     delete []dst_wstr;   
  11.     std::locale::global(old_loc);   
  12.     return result;   
  13. }  
  14.  

我們將全局locale設置為系統locale,并保存原來的全局locale在old_loc中。
在制定轉換空間緩存大小的時候,考慮如下:char是用1個或多個對象,也就是1個或者多個字節來表示各種符號:比如,GB2312用1個字節表示數字和 字母,2個字節表示漢字;UTF-8用一個字節表示數字和字母,3個字節表示漢字,4個字節表示一些很少用到的符號,比如音樂中G大調符號等。 wchar_t是用1個對象(2字節或者4字節)來表示各種符號。因此,表示同樣的字符串,寬字符串的大小(也就是wchar_t對象的數量)總是小于或 者等于窄字符串大小(char對象數量)的。+1是為了在最后預留一個值為0的對象,以便讓C風格的char或者wchar_t字符串自動截斷——這當然 是寬串大小等于窄串大小的時候才會用上的,大部分時候,字符串早在前面某個轉換完畢的位置就被0值對象所截斷了。
最后我們將全局locale設置回原來的old_loc。
窄串到寬串的轉換函數:

  1. const std::string ws2s(const std::wstring& ws)   
  2. {   
  3.     std::locale old_loc = std::locale::global(std::locale(""));   
  4.     const wchar_t* src_wstr = ws.c_str();   
  5.     size_t buffer_size = ws.size() * 4 + 1;   
  6.     char* dst_str = new char[buffer_size];   
  7.     memset(dst_str, 0, buffer_size);   
  8.     wcstombs(dst_str ,src_wstr, buffer_size);   
  9.     std::string result = dst_str;   
  10.     delete []dst_str;   
  11.     std::locale::global(old_loc);   
  12.     return result;   
  13. }  
  14.  

這里考慮轉換空間緩存大小的策略正好相反,在最極端的情況下,所有的wchar_t都需要4個char來表示,所以最大的可能就是4倍加1。
這兩個函數在VC和gcc中都能正常運行(MinGW因為前面說到的原因不支持寬字符的正常使用),在VC中會給出不安全的警告,這是告訴給那些弄不清寬窄轉換實質的人的警告,對于了解到目前這些知識的你我來說,這就是啰嗦了。

locale和facet
C++的locale框架比C更完備。C++除了一個籠統本地策略集locale,還可以為locale指定具體的策略facet,甚至可以用自己定義的 facet去改造一個現有的locale產生一個新的locale。如果有一個facet類NewFacet需要添加到某個old_loc中形成新 new_loc,需要另外一個構造函數,通常的做法是:
std::locale new_loc(old_loc, new NewFacet);
標準庫里的標準facet都具有自己特有的功能,訪問一個locale對象中特定的facet需要使用模板函數use_facet:
template <class Facet> const Facet& use_factet(const locale&);
換一種說法,use_facet把一個facet類實例化成了對象,由此就可以使用這個facet對象的成員函數。
codecvt
codecvt就是一個標準facet。在C++的設計框架里,這是一個通用的代碼轉換模板——也就是說,并不是僅僅為寬窄轉換制定的。
templat <class I, class E, class State> class std::codecvt: public locale, public codecvt_base{…};
I表示內部編碼,E表示外部編碼,State是不同轉換方式的標識,如果定義如下類型:
typedef std::codecvt<wchar_t, char, mbstate_t> CodecvtFacet;
那么CodecvtFacet就是一個標準的寬窄轉換facet,其中mbstate_t是標準寬窄轉換的State。
內部編碼和外部編碼
我們考慮第1節中提到的C++編譯器讀取源文件時候的情形,當讀到L”中文abc”的時候,外部編碼,也就是源文件的編碼,是GB2312或者UTF-8 的char,而編譯器必須將其翻譯為UCS-2BE或者UTF-32BE的wchar_t,這也就是程序的內部編碼。如果不是寬字符串,內外編碼都是 char,也就不需要轉換了。類似的,當C++讀寫文件的時候 ,就會可能需要到內外編碼轉換。事實上,codecvt就正是被文件流緩存basic_filebuf所使用的。理解這一點很重要,原因會在下一小節看 到。
CodecvtFacet的in()和out()
因為在CodecvtFacet中,內部編碼設置為wchar_t,外部編碼設置為char,轉換模式是標準寬窄轉換mbstate_t,所以,類方法 in()就是從char標準轉換到wchar_t,out()就是從wchar_t標準轉換到char。這就成了我們正需要的內外轉換函數。
result in(State& s, const E* from, const E* from_end, const E*& from_next, I* to,  I* to_end, I*& to_next) const;
result out(State& s, const I* from, const I* from_end, const I*& from_next, E* to, E* to_end, E*& to_next) const;
其中,s是非const引用,保存著轉換位移狀態信息。這里需要重點強調的是,因為轉換的實際工作交給了運行時庫,也就是說,轉換可能不是在程序的主進程 中完成的,而轉換工作依賴于查詢s的值,因此,如果s在轉換結束前析構,就可能拋出運行時異常。所以,最安全的辦法是,將s設置為全局變量!
const的3個指針分別是待轉換字符串的起點,終點,和出現錯誤時候的停點(的下一個位置);另外3個指針是轉換目標字符串的起點,終點以及出現錯誤時候的停點(的下一個位置)。
代碼如下:
頭文件:

  1. //Filename string_wstring_cppcvt.hpp   
  2. #ifndef STRING_WSTRING_CPPCVT_HPP   
  3. #define STRING_WSTRING_CPPCVT_HPP   
  4. #include <iostream>   
  5. #include <string>   
  6. const std::wstring s2ws(const std::string& s);   
  7. const std::string ws2s(const std::wstring& s);   
  8. #endif  
  9.  

實現:

  1. #include "string_wstring_cppcvt.hpp"   
  2. mbstate_t in_cvt_state;   
  3. mbstate_t out_cvt_state;   
  4. const std::wstring s2ws(const std::string& s)   
  5. {   
  6.     std::locale sys_loc("");   
  7.     const char* src_str = s.c_str();   
  8.     const size_t BUFFER_SIZE = s.size() + 1;   
  9.     wchar_t* intern_buffer = new wchar_t[BUFFER_SIZE];   
  10.     wmemset(intern_buffer, 0, BUFFER_SIZE);   
  11.     const char* extern_from = src_str;   
  12.     const char* extern_from_end = extern_from + s.size();   
  13.     const char* extern_from_next = 0;   
  14.     wchar_t* intern_to = intern_buffer;   
  15.     wchar_t* intern_to_end = intern_to + BUFFER_SIZE;   
  16.     wchar_t* intern_to_next = 0;   
  17.     typedef std::codecvt<wchar_t, char, mbstate_t> CodecvtFacet;   
  18.     CodecvtFacet::result cvt_rst =   
  19.     std::use_facet<CodecvtFacet>(sys_loc).in(   
  20.             in_cvt_state,   
  21.             extern_from, extern_from_end, extern_from_next,   
  22.             intern_to, intern_to_end, intern_to_next);   
  23.     if (cvt_rst != CodecvtFacet::ok) {   
  24.         switch(cvt_rst) {   
  25.         case CodecvtFacet::partial:   
  26.                 std::cerr << "partial";   
  27.                 break;   
  28.         case CodecvtFacet::error:   
  29.                 std::cerr << "error";   
  30.                 break;   
  31.         case CodecvtFacet::noconv:   
  32.                 std::cerr << "noconv";   
  33.                 break;   
  34.         default:   
  35.                 std::cerr << "unknown";   
  36.         }   
  37.         std::cerr  << ", please check in_cvt_state."   
  38. << std::endl;   
  39.     }   
  40.     std::wstring result = intern_buffer;   
  41.     delete []intern_buffer;   
  42.     return result;   
  43. }   
  44. const std::string ws2s(const std::wstring& ws)   
  45. {   
  46.     std::locale sys_loc("");   
  47.     const wchar_t* src_wstr = ws.c_str();   
  48.     const size_t MAX_UNICODE_BYTES = 4;   
  49.     const size_t BUFFER_SIZE =   
  50.                 ws.size() * MAX_UNICODE_BYTES + 1;   
  51.     char* extern_buffer = new char[BUFFER_SIZE];   
  52.     memset(extern_buffer, 0, BUFFER_SIZE);   
  53.     const wchar_t* intern_from = src_wstr;   
  54.     const wchar_t* intern_from_end = intern_from + ws.size();   
  55.     const wchar_t* intern_from_next = 0;   
  56.     char* extern_to = extern_buffer;   
  57.     char* extern_to_end = extern_to + BUFFER_SIZE;   
  58.     char* extern_to_next = 0;   
  59.     typedef std::codecvt&lt;wchar_t, char, mbstate_t> CodecvtFacet;   
  60.     CodecvtFacet::result cvt_rst =   
  61.     std::use_facet<CodecvtFacet>(sys_loc).out(   
  62.             out_cvt_state,   
  63.             intern_from, intern_from_end, intern_from_next,   
  64.             extern_to, extern_to_end, extern_to_next);   
  65.     if (cvt_rst != CodecvtFacet::ok) {   
  66.         switch(cvt_rst) {   
  67.         case CodecvtFacet::partial:   
  68.                 std::cerr << "partial";   
  69.                 break;   
  70.         case CodecvtFacet::error:   
  71.                 std::cerr << "error";   
  72.                 break;   
  73.         case CodecvtFacet::noconv:   
  74.                 std::cerr << "noconv";   
  75.                 break;   
  76.         default:   
  77.                 std::cerr << "unknown";   
  78.         }   
  79.         std::cerr << ", please check out_cvt_state."   
  80. << std::endl;   
  81.     }   
  82.     std::string result = extern_buffer;   
  83.     delete []extern_buffer;   
  84.     return result;   
  85. }  
  86.  

最后補充說明一下std::use_facet&lt;CodecvtFacet>(sys_loc).in()和 std::use_facet<CodecvtFacet>(sys_loc).out()。sys_loc是系統的locale,這個 locale中就包含著特定的codecvt facet,我們已經typedef為了CodecvtFacet。用use_facet對CodecvtFacet進行了實例化,所以可以使用這個 facet的方法in()和out()。

C++的流和本地化策略集
BS在設計C++流的時候希望其具備智能化,并且是可擴展的智能化,也就是說,C++的流可以“讀懂”一些內容。比如:

std::cout <&lt; 123 &lt;&lt; “ok” &lt;&lt; std::endl;

這句代碼中,std::cout是能判斷出123是int而”ok”是const char[3]。利用流的智能,甚至可以做一些基礎類型的轉換,比如從int到string,string到int:

  1. std::string str("123");   
  2. std::stringstream sstr(str);   
  3. int i;   
  4. sstr >&gt; i;  
  5.  
  6. int i = 123;   
  7. std::stringstream sstr;   
  8. sstr <&lt; i;   
  9. std::string str = sstr.str();  
  10.  

盡管如此,C++并不滿足,C++甚至希望流能“明白”時間,貨幣的表示法。而時間和貨幣的表示方法在世界范圍內是不同的,所以,每一個流都有自己 的locale在影響其行為,C++中叫做激活(imbue,也有翻譯成浸染)。而我們知道,每一個locale都有多個facet,這些facet并非 總是被use_facet使用的。決定使用哪些facet的,是流的緩存basic_streambuf及其派生類basic_stringbuf和 basic_filebuf。我們要用到的facet是codecvt,這個facet只被basic_filebuf使用——這就是為什么只能用 fstream來實現寬窄轉換,而無法使用sstream來實現的原因。
頭文件:

  1. //filename string_wstring_fstream.hpp   
  2. #ifndef STRING_WSTRING_FSTREAM_HPP   
  3. #define STRING_WSTRING_FSTREAM_HPP   
  4. #include &lt;string>   
  5. const std::wstring s2ws(const std::string& s);   
  6. const std::string ws2s(const std::wstring& s);   
  7. #endif 

實現:

  1. #include <string>   
  2. #include <fstream>   
  3. #include "string_wstring_fstream.hpp"   
  4. const std::wstring s2ws(const std::string& s)   
  5. {   
  6.     std::locale sys_loc("");   
  7.     std::ofstream ofs("cvt_buf");   
  8.     ofs <&lt; s;   
  9.     ofs.close();   
  10.     std::wifstream wifs("cvt_buf");   
  11.     wifs.imbue(sys_loc);   
  12.     std::wstring wstr;   
  13.     wifs >&gt; wstr;   
  14.     wifs.close();   
  15.     return wstr;   
  16. }   
  17. const std::string ws2s(const std::wstring& s)   
  18. {   
  19.     std::locale sys_loc("");   
  20.     std::wofstream wofs("cvt_buf");   
  21.     wofs.imbue(sys_loc);   
  22.     wofs <&lt; s;   
  23.     wofs.close();   
  24.     std::ifstream ifs("cvt_buf");   
  25.     std::string str;   
  26.     ifs >&gt; str;   
  27.     ifs.close();   
  28.     return str;   
  29. }  
  30.  

在窄到寬的轉化中,我們先使用默認的本地化策略集(locale)將s通過窄文件流ofs傳入文件,這是char到char的傳遞,沒有任何轉換; 然后我們打開寬文件流wifs,并用系統的本地化策略集(locale)去激活(imbue)之,流在讀回寬串wstr的時候,就是char到 wchar_t的轉換,并且因為激活了sys_loc,所以實現標準窄到寬的轉換。
在寬到窄的轉化中,我們先打開的是寬文件流wofs,并且用系統的本地化策略集sys_loc激活(imbue)之,這時候,因為要寫的文件 cvt_buf是一個外部編碼,所以執行了從wchar_t到char的標準轉換。讀回來的文件流從char到char,不做任何轉換。

硬編碼的硬傷
我們現在知道,C/C++的寬窄轉換是依賴系統的locale的,并且在運行時完成。考慮這樣一種情況,我們在簡體中文Windows下編譯如下語句:
const char* s = “中文abc”;
根據我們之前的討論,編譯器將按照Windows Codepage936(GB2312)對這個字符串進行編碼。如果我們在程序中運行寬窄轉換函數,將s轉換為寬字符串ws,如果這個程序運行在簡體中文 環境下是沒問題的,將執行從GB2312到UCS-2BE的轉換;但是,如果在其他語言環境下,比如是繁體中文BIG5,程序將根據系統的locale執 行從BIG5到UCS-2BE的轉換,這顯然就出現了錯誤。
補救
有沒有補救這個問題的辦法呢?一個解決方案就是執行不依賴locale的寬窄轉換。實際上,這就已經不是寬窄轉換之間的問題了,而是編碼之間轉換的問題 了。我們可以用GNU的libiconv實現任意編碼間的轉換,對于以上的具體情況,指明是從GB2312到UCS-2BE就不會出錯。(請參考本人前面 的章節:win32下的libiconv),但這顯然是一個笨拙的策略:我們在簡體中文Windows下必須使用GB2312到UCS-2BE版本的寬窄轉換函數;到了BIG5環境下,就必須重新寫從BIG5到UCS-2BE的寬窄轉換函數。
Windows的策略
Windows的策略是淘汰了窄字符串,干脆只用寬字符串。所有的硬編碼全部加上特定宏,比如TEXT(),如果程序是所謂Unicode編譯,在編譯時就翻譯為UCS2-BE——Windows自稱為Unicode編程,其本質是使用了UCS-2BE的16位寬字符串。
Linux的策略
Linux下根本就不存在這個問題!因為各種語言的Linux都使用UTF-8的編碼,所以,無論系統locale如何變化,窄到寬轉換的規則一直是UTF-8到UTF32-BE 。
跨平臺策略
因為在16位的范圍內,UTF32-BE的前16位為0,后16位與UCS2-BE是一樣的,所以,即使wchar_t的sizeof()不一樣,在一般情況下,跨平臺使用寬字符(串)也應該是兼容的。但是依然存在潛在的問題,就是那些4字節的UTF32編碼。
gettext策略
以上都是將ASCII及以外的編碼硬編碼在程序中的辦法。GNU的gettext提供了另外一種選擇:在程序中只硬編碼ASCII,多語言支持由gettext函數庫在運行時加載。(對gettext的介紹請參考本人前面的章節:Win32下的GetText)。 gettext的多語言翻譯文件不在程序中,而是單獨的提出來放在特定的位置。gettext明確的知道這些翻譯文件的編碼,所以可以準確的告訴給系統翻 譯的正確信息,而系統將這些信息以當前的系統locale編碼成窄字符串反饋給程序。例如,在簡體中文Windows中,gettext的po文件也可以 以UTF-8儲存,gettext將po文件翻譯成mo文件,確保mo文件在任何系統和語言環境下都能夠正確翻譯。在運行是傳給win32程序的窄串符合 當前locale,是GB2312。gettext讓國際化的翻譯更加的方便,缺點是目前我沒找到支持寬字符串的版本(據說是有ugettext()支持 寬字符串),所以要使用gettext只能使用窄字符串。但是gettext可以轉換到寬字符串,而且不會出現寬窄轉換的問題,因為gettext是運行 時根據locale翻譯的。例如:
const char* s = gettext(“Chinese a b c”);
其中”Chinese a b c”在po中的翻譯是”中文abc”
使用依賴locale的運行時寬窄轉換函數:
const std::wstring wstr = s2ws(s);
運行時調用該po文件對應的mo文件,在簡體中文環境下就以GB2312傳給程序,在繁體中文中就以BIG5傳給程序,這樣s2ws()總能夠正常換算編碼。
更多
在本文的最后,我想回到C++的stream問題上。用fstream轉換如此的簡單,sstream卻不支持。改造一個支持codecvt的 string stream需要改造basic_stringbuf。basic_stringbuf和basic_filebuf都派生自 basic_streambuf,所不同的是basic_filebuf在構造和open()的時候調用了codecvt,只需要在 basic_stringbuf中添加這個功能就可以了。說起來容易,實際上是需要重新改造一個STL模板,盡管這些模板源代碼都是在標準庫頭文件中現成 的,但是我還是水平有限,沒有去深究了。另外一個思路是構建一個基于內存映射的虛擬文件,這個框架在boost的iostreams庫中,有興趣的朋友可 以深入的研究。
(完)