C++11 Unicode編碼轉換
1.char16_t與char32_t
在C++98中,為了支援Unicode字元,使用wchar_t型別來表示“寬字元”,但並沒有嚴格規定位寬,而是讓wchar_t的寬度由編譯器實現,因此不同的編譯器有著不同的實現方式,GNU C++規定wchar_t為32位,Visual C++規定為16位。由於wchar_t寬度沒有一個統規定,導致使用wchar_t的程式碼在不同平臺間移植時,可能出現問題。這一狀況在C++11中得到了一定的改善,從此Unicode字元的儲存有了統一型別:
(1)char16_t:用於儲存UTF-16編碼的Unicode字元。
(2)char32_t:用於儲存UTF-32編碼的Unicode字元。
至於UTF-8編碼的Unicode資料,C++11還是使用了8bits寬度的char型別陣列來表示,而char16_t和char32_t的寬度由其名稱可以看出,char16_t為16bits,char32_t為32bits。
2.定義字串的5種方式
除了使用新型別char16_t與char32_t來表示Unicode字元,此外,C++11還新增了三種字首來定義不同編碼的字串,新增字首如下:
(1)u8表示為UTF-8編碼;
(2)u表示為UTF-16編碼;
(3)U表示為UTF-32編碼。
C++98中有兩種定義字串的方式,一是直接使用雙引號定義多位元組字串,二是通過字首“L”表示wchar_t字串(寬字串)。至此,C++中共有5種定義字串的方式。
3.影響字串正確處理的因素
在使用不同方式定義不同編碼的字串時,我們需要注意影響字串處理和顯示的幾個因素有編輯器、編譯器和輸出環境。
程式碼編輯器採用何種編碼方式決定了字串最初的編碼,比如編輯器如果採用GBK,那麼程式碼檔案中的所有字元都是以GBK編碼儲存。當編譯器處理字串時,可以通過字首來判斷字串的編碼型別,如果目標編碼與原編碼不同,則編譯器會進行轉換,比如C++11中的字首u8表示目標編碼為UTF-8的字元,如果程式碼檔案採用的是GBK,編譯器按照UTF-8去解析字串常量,則可能會出現錯誤。
//程式碼檔案為GBK編碼 #include <iomanip> #include <iostream> using namespace std; int main() { const char* sTest = u8"你好"; for(int i=0;sTest[i]!=0;++i) { cout<<setiosflags(ios::uppercase)<<hex<<(uint32_t)(uint8_t)sTest[i]<<" "; } return 0; } //編譯選項:g++ -std=c++0x -finput-charset=utf-8 test.cpp
程式輸出結果:C4 E3 BA C3。這個碼值是GBK的碼值,因為“你”的GBK碼值是0xC4E3,“好”的GBK碼值是0xBAC3。可見,編譯器未成功地將GBK編碼的“你好”轉換為UTF-8的碼值“你”(E4 BD A0)“好”(E5 A5 BD),原因是使用編譯選項-finput-charset=utf-8指定程式碼檔案編碼為UTF-8,而實際上程式碼檔案編碼為GBK,導致編譯器出現錯誤的認知。如果使用-finput-charset=gbk,那麼編譯器在編譯時會將GBK編碼的“你好”轉換為UTF-8編碼,正確輸出E4 BD A0 E5 A5 BD。
程式碼編輯器和編譯器這兩個環節在處理字串如果沒有問題,那麼最後就是顯示環節。字串的正確顯示依賴於輸出環境。C++輸出流物件cout能夠保證的是將資料以二進位制輸出到輸出裝置,但輸出裝置(比如Linux shell或者Windows console)是否能夠支援特定的編碼型別的輸出,則取決於輸出環境。比如Linux虛擬終端XShell,配置終端編碼型別為GBK,則無法顯示輸出的UTF-8編碼字串。
一個字串從定義到處理再到輸出,涉及到編輯器、編譯器和輸出環境三個因素,正確的處理和顯示需要三個因素的共同保障,每一個環節都不能出錯。一個字串的處理流程與因素如下圖所示:
當然如果想避開編輯器編碼對字串的影響,可以使用Unicode碼值來定義字串常量,參看如下程式碼:
//程式碼檔案為GBK編碼 #include <iomanip> #include <iostream> using namespace std; int main() { const char* sTest = u8"\u4F60\u597D"; //你好的Uunicode碼值分別是:0x4F60和0x597D for(int i=0;sTest[i]!=0;++i) { cout<<setiosflags(ios::uppercase)<<hex<<(uint32_t)(uint8_t)sTest[i]<<" "; } return 0; } //編譯選項:g++ -std=c++0x -finput-charset=utf-8 test.cpp
程式輸出結果:E4 BD A0 E5 A5 BD。可見,即使編譯器對程式碼檔案的編碼理解有誤,仍然可以正確地以UTF-8編碼輸出“你好”的碼值。原因是ASCII字元使用GBK與UTF-8編碼碼值是相同的,所以直接書寫Unicode碼值來表示字串是一種比較保險的做法,缺點就是難以閱讀。
4.Unicode的庫支援
C++11在標準庫中增加了一些Unicode編碼轉換的函式,開發人員可以使用庫中的一些新增編碼轉換函式來完成各種Unicode編碼間的轉換,函式原型如下:
//多位元組字元轉換為UTF-16編碼 size_t mbrtoc16 ( char16_t * pc16,const char * pmb,size_t max,mbstate_t * ps); //UTF-16字元轉換為多位元組字元 size_t c16rtomb ( char * pmb,char16_t c16,mbstate_t * ps ); //多位元組字元轉換為UTF-32編碼 size_t mbrtoc32 ( char32_t * pc32,mbstate_t * ps); //UTF-32字元轉換為多位元組字元 size_t c32rtomb ( char * pmb,char32_t c32,mbstate_t * ps );
函式名稱中mb表示multi-byte(多位元組),rto表示convert to(轉換為),c16表示char16_t,瞭解這些,可以根據函式名稱直觀的理解它們的作用。下面給一下UTF-16字串轉換為多位元組字串(以GBK為例)的例子:
#include <uchar.h> #include <string.h> #include <locale> #include <iomanip> #include <iostream> using namespace std; int main() { const char16_t* utf16 = u"\u4F60\u597D\u554A"; size_t utf16Len=char_traits<char16_t>::length(utf16); char* gbk =new char[utf16Len*2+1]; memset(gbk,utf16Len * 2 + 1); char* pGbk = gbk; setlocale(LC_ALL,"zh_CN.gbk"); mbstate_t mbs; //轉換狀態 size_t length = 0; while (*utf16) { pGbk += length; length = c16rtomb(pGbk,*utf16,&mbs); if (length == 0 || pGbk - gbk>sizeof(gbk)) { cout << "failed" << endl; break; //轉換失敗 } ++utf16; } for (int i = 0; gbk[i] != 0; ++i) { cout << setiosflags(ios::uppercase) << hex << (uint32_t)(uint8_t)gbk[i] << " "; } return 0; } //編譯選項:g++ -std=c++0x test.cpp
程式輸出結果:C4 E3 BA C3 B0 A1。可見,使用c16rtomb()完成了將“你好啊”從UTF-16編碼到多位元組編碼(GBK)的轉換。上面的轉換,我們用到了locale機制。locale表示的是一個地域的特徵,包括字元編碼、數字時間表示形式、貨幣符號等。locale串使用“zh_CN.gbk”表示目的多位元組字串使用GBK編碼。
上面通過Unicode字元的轉換來完成字串的轉換,實際上C++提供了一個類模板codecvt用於完成Unicode字串與多位元組字串之間的轉換,主要分為4種:
codecvt<char,char,mbstate_t> //performs no conversion codecvt<wchar_t,mbstate_t> //converts between native wide and narrow character sets codecvt<char16_t,mbstate_t> //converts between UTF16 and UTF8 encodings,since C++11 codecvt<char32_t,mbstate_t> //converts between UTF32 and UTF8 encodings,since C++11
上面的codecvt實際上是locale的一個facet,facet可以簡單地理解為locale的一些介面。通過codecvt,可以完成當前locale下多位元組編碼字串與Unicode字元間的轉換,也包括Unicode字元編碼間的轉換。這裡的多位元組字串不僅可以試UTF-8,也可以是GBK或者其它編碼,實際依賴於locale所採用的編碼方式。每種codecvt負責不同型別編碼的轉換,但是目前編譯器的支援情況並沒有那麼完整,一種locale並不一定支援所有的codecvt,程式設計師可以通過has_facet函式模板來查詢指定locale下的支援情況。參考程式碼如下:
#include <locale> #include <iostream> using namespace std; int main() { //定義一個locale並查詢該locale是否支援一些facet locale lc("zh_CN.gbk"); bool can_cvt = has_facet<codecvt<char,mbstate_t>>(lc); if (!can_cvt) cout<<"do not support char-char facet"<<endl; can_cvt = has_facet<codecvt<wchar_t,mbstate_t>>(lc); if (!can_cvt) cout << "do not support wchar_t-char facet" << endl; can_cvt = has_facet<codecvt<char16_t,mbstate_t>>(lc); if (!can_cvt) cout << "do not support char16_t-char facet" << endl; can_cvt = has_facet<codecvt<char32_t,mbstate_t>>(lc); if (!can_cvt) cout << "do not support char32_t-char facet" << endl; } //編譯選項:g++ -std=c++11 test.cpp //g++版本:gcc version 4.8.5 20150623 (Red Hat 4.8.5-4) (GCC)
程式輸出結果:
do not support char16_t-char facet
do not support char32_t-char facet
由此可見,從char到char16_t與char32_t轉換的兩種facet還沒有被實驗機使用的編譯器支援。
假如實驗機支援從char與char16_t的轉換,可參考如下程式碼:
#include <uchar.h> #include <string.h> #include <locale> #include <iomanip> #include <iostream> using namespace std; int main() { typedef std::codecvt<char16_t,std::mbstate_t> facet_type; std::locale mylocale("zh_CN.gbk"); try { const facet_type& myfacet = std::use_facet<facet_type>(mylocale); const char16_t* utf16 = u"\u4F60\u597D\u554A"; //你好啊 size_t utf16Len = char_traits<char16_t>::length(utf16); cout<< utf16Len <<endl; char* gbk = new char[utf16Len*2+1]; memset(gbk,utf16Len * 2 + 1); std::mbstate_t mystate; //轉換狀態 const char16_t* pwc; //from_next char* pc; //to_next facet_type::result myresult = myfacet.out(mystate,utf16,utf16+utf16Len+1,pwc,gbk,gbk + utf16Len * 2+1,pc); if (myresult == facet_type::ok) { std::cout << "Translation successful:" << endl; } for (int i = 0; gbk[i] != 0; ++i) { cout << setiosflags(ios::uppercase) << hex << (uint32_t)(uint8_t)gbk[i] << " "; } delete[] gbk; } catch(...) { cout<<"do not support char16_t-char facet"<<endl; return -1; } return 0; }
由於實驗環境並不支援char與char16_t相互轉換的facet,所以程式輸出結果為:do not support char16_t-char facet。
5.u16string與u32string
C++11新增了UTF-16和UTF-32編碼的字元型別char16_t和char32_t,當然少不了對應的字串型別,分別是u16string與與u32string,二者的存在類似與string與wstring。四者的定義如下:
typedef basic_string<char> string; typedef basic_string<wchar_t> wstring; typedef basic_string<char16_t> u16string; typedef basic_string<char32_t> u32string;
我們對string與wstring應該比較熟悉,對於u16string與u32string在用法上是差不多了,有相同的成員介面與型別,只需要記住其儲存的字元編碼型別不同即可。下面看一下u16string使用的簡單示例。
#include <iomanip> #include <iostream> using namespace std; int main() { u16string u16str = u"\u4F60\u597D\u554A"; //你好啊 cout << u16str.length() << endl; //字元數 for (int i = 0; i<u16str.length(); ++i) { cout << setiosflags(ios::uppercase) << hex << (uint16_t)u16str[i] << " "; } }
程式輸出:
3
4F60 597D 554A。
以上就是C++11 Unicode編碼轉換的詳細內容,更多關於C++11 Unicode編碼轉換的資料請關注我們其它相關文章!