1. 程式人生 > 程式設計 >C++11 Unicode編碼轉換

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編碼轉換的資料請關注我們其它相關文章!