完全解密C++寬字符(一)html
轉:http://club.topsage.com/thread-2227977-1-1.htmlios
一、從char到wchar_t
「這個問題比你想象中複雜」
從字符到整數
char 是一種整數類型,這句話的含義是,char所能表示的字符在C/C++中都是整數類型。好,接下來,不少文章就會舉出一個典型例子,好比,'a'的數值就是0x61。這種說法對嗎?若是你細心的讀過K&R和BS對於C和C++描述的原著,你就會立刻反駁道,0x61只是'a'的ASCII值,並無任何規定C/C++的char值必須對應ASCII。C/C++甚至沒有規定char佔幾位,只是規定了sizeof(char)等於1。
固然,目前大部分狀況下,char是8位的,而且,在ASCII範圍內的值,與ASCII對應。
本地化策略集(locale)
「將 'a'翻譯成0x61的整數值」,「將ASCII範圍內的編碼與char的整數值對應起來」,相似這樣的規定,是特定系統和特定編譯器制定的,C/C++ 中有個特定的名詞來描述這種規定的集合:本地化策略集(locale。也有翻譯成「現場」)。而翻譯——也就是代碼轉換(codecvt)只是這個集合中的一個,C++中定義爲策略(facet。也有翻譯爲「刻面」)
C/C++的編譯策略
「本地化策略集」是個很好的概念,惋惜在字符和字符串這個層面上,C/C++並不使用(C++的locale一般只是影響流(stream)),C/C++使用更直接簡單的策略:硬編碼。
簡單的說,字符(串)在程序文件(可執行文件,非源文件)中的表示,與在程序執行中在內存中的表示一致。考慮兩種狀況:
A、char c = 0x61;
B、char c = 'a';
狀況A下,編譯器能夠直接認識做爲整數的c,可是在狀況B下,編譯器必須將'a'翻譯成整數。編譯器的策略也很簡單,就是直接讀取字符(串)在源文件中的編碼數值。好比:
const char* s = "中文abc";
這段字符串在GB2312(Windows 936),也就是咱們的windows默認中文系統源文件中的編碼爲:
0xD6 0xD0 0xCE 0xC4 0x61 0x62 0x63
在UTF-8,也就是Linux默認系統源文件中的編碼爲:
0xE4 0xB8 0xAD 0xE6 0x96 0x87 0x61 0x62 0x63
通常狀況下,編譯器會忠實於源文件的編碼爲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 0x6587 0x0061 0x0062 0x0063 //win32,16位
0x00004E2D 0x00006587 0x00000061 0x00000062 0x00000063 //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和UTF
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語言中,0x00的字節就是'\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的後兩位(前兩位是0x00 0x00)等價於UTF-16BE也就等價於UCS-2BE
BOM
爲了說明一個文件採用的是什麼編碼,在文件最開始的部分,能夠有BOM,好比0xFE 0xFF表示UTF-16BE,0xFF 0xFE 0x00 0x00表示UTF-32LE。UTF-8本來是不須要BOM的,由於其自我同步的特性,可是爲了明確說明這是UTF-8(而不是讓文本編輯器去猜),也能夠加上UTF-8的BOM:0xEF 0xBB 0xBF
以上內容都講述得很概略,詳細信息請查閱維基百科相關內容。windows
三、利用C運行時庫函數轉換
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>。
咱們先看窄到寬的轉換函數:緩存
const std::wstring s2ws(const std::string& s) { std::locale old_loc = std::locale::global(std::locale("")); const char* src_str = s.c_str(); const size_t buffer_size = s.size() + 1; wchar_t* dst_wstr = new wchar_t[buffer_size]; wmemset(dst_wstr, 0, buffer_size); mbstowcs(dst_wstr, src_str, buffer_size); std::wstring result = dst_wstr; delete []dst_wstr; std::locale::global(old_loc); return result; }
咱們將全局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。
窄串到寬串的轉換函數:安全
const std::string ws2s(const std::wstring& ws) { std::locale old_loc = std::locale::global(std::locale("")); const wchar_t* src_wstr = ws.c_str(); size_t buffer_size = ws.size() * 4 + 1; char* dst_str = new char[buffer_size]; memset(dst_str, 0, buffer_size); wcstombs(dst_str ,src_wstr, buffer_size); std::string result = dst_str; delete []dst_str; std::locale::global(old_loc); return result; }
這裏考慮轉換空間緩存大小的策略正好相反,在最極端的狀況下,全部的wchar_t都須要4個char來表示,因此最大的可能就是4倍加1。
這兩個函數在VC和gcc中都能正常運行(MinGW由於前面說到的緣由不支持寬字符的正常使用),在VC中會給出不安全的警告,這是告訴給那些弄不清寬窄轉換實質的人的警告,對於瞭解到目前這些知識的你我來講,這就是囉嗦了。網絡
(未完待續)app