1. 程式人生 > >C++:中文編碼轉換

C++:中文編碼轉換

在介紹如何使用C++11標準庫進行中文編碼轉換之前,先說說一下byte string、multibyte string、wide string之間的區別。

byte string

由8位元的位元組組成的字串。由char表示位元組。因而字串長度=位元組數=char數

multibyte string

在記憶體佈局上與byte string相同。但是由於它是區域(locale)相關的,所以它有可能表示的是UTF-8, GB18030, EUC-JP, Shift-JIS等格式的字串,而這些格式中的每個字碼(codepoint)可能是由多個連續的char組合構成的,所以字串長度=字碼數,但!=char數,也!=位元組數。

 wide string

每個寬字元由於作業系統的不同,其寬度為16位或32位的多語言字元程式碼。格式分別採用unicode(UTF-16LE)、UTF-16、UTF-32,由wchar_t, char16_t, char32_t表示。

C++標準庫中對應關係:

char *        std::string
wchar_t*   std::wstring
char16_t*  std::u16string
char32_t*  std::u32string

1.string與wstring互相轉化

C++標準庫從C++11開始提供了std::codecvt_utf8和std::codecvt_byname兩個轉換器來完成編碼轉換,可使用的通用程式碼如下:

#include <string>
#include <locale>
#include <codecvt>

// string的編碼方式為utf8,則採用:
std::string wstring2utf8string(const std::wstring& str)
{
    std::wstring_convert<std::codecvt_utf8<wchar_t> > strCnv;
    return strCnv.to_bytes(str);
}

std::wstring utf8string2wstring(const std::string& str)
{
    std::wstring_convert< std::codecvt_utf8<wchar_t> > strCnv;
    return strCnv.from_bytes(str);
}

// string的編碼方式為除utf8外的其它編碼方式,可採用:
std::string wstring2string(const std::wstring& str, const std::string& locale)
{
    typedef std::codecvt_byname<wchar_t, char, std::mbstate_t> F;
    std::wstring_convert<F> strCnv(new F(locale));

    return strCnv.to_bytes(str);
}

std::wstring string2wstring(const std::string& str, const std::string& locale)
{
    typedef std::codecvt_byname<wchar_t, char, std::mbstate_t> F;
    std::wstring_convert<F> strCnv(new F(locale));

    return strCnv.from_bytes(str);
}

如果是GBK string與wstring互相轉化,locale可取值:
linux下:
zh_CN.GBK
zh_CN.GB2312
zh_CN.GB18030

windows下:
標準格式的locale:
Chinese_China.936
zh-CN
.936
非標準格式的locale:
chs
Chinese-simplified
Chinese
ZHI
不能使用的locale:

Chinese.936,chs.936,Chinese.GB2312,chs.GB18030等此類值。

下面通過一些例子說明上面的函式如何使用吧。

2.string與wstring如何輸出到控制檯

內碼表為936

以在windows控制檯為例,舉例說明:

// testCode.cpp
#include <string>
#include <iostream>
#include <fstream>
#include <codecvt>
#include <locale>

int main() {
	std::wstring txt = L"中國人";

	std::wcout << txt << endl;
}

程式編譯後在中文版windows中執行結果:

控制檯使用的內碼表為936(也就是GBK編碼),輸出結果為亂碼。因為txt是unicode,而控制檯是GBK編碼,亂碼是由於沒有做編碼轉換造成的。

修改程式碼,新增一個轉換器:

int main() {
	std::wstring txt = L"中國人";
	wcout.imbue(std::locale(std::locale("Chinese"), new std::codecvt_byname<wchar_t, char, std::mbstate_t>("Chinese")));  
	// wcout.imbue(std::locale("Chinese")); 
	// 也可以簡寫成這種形式,其中預設帶了std::codecvt_byname<wchar_t, char, std::mbstate_t>("Chinese")轉換器
	
	std::wcout << txt << endl;
}

此時,輸出結果就正常了。

如果修改內碼表為65001(也就是UTF-8編碼),再執行如下程式碼:

int main() {
	std::wstring txt = L"中國人";

	std::wcout << txt << endl;
}

會發現沒有結果輸出,說明從unicode到utf-8沒有轉換成功。

修改程式碼,新增一個轉換器:

int main() {
	std::wstring txt = L"中國人";
	wcout.imbue(std::locale(std::locale("Chinese"), new std::codecvt_utf8<wchar_t>()));
	
	std::wcout << txt << endl;
}

此時,結果正確輸出了:

由於wstring是unicode,它轉換到其它編碼格式,只需要使用一次轉換器就可以了,但如果使用的是string,又該怎樣做轉換?比如使用如下程式碼:

int main() {
	std::string txt = u8"中國人";

	std::cout << txt << endl;
}

在內碼表為936的情況下,執行輸出為亂碼:

修改程式碼,新增兩個轉換器:

int main() {
	std::string txt = u8"中國人";   // string的編碼格式為utf-8

	std::wstring wtxt = utf8string2wstring(txt);    // 將utf-8的string轉換為wstring
	std::string txt_gbk = wstring2string(wtxt, "Chinese");    // 再將wstring轉換為gbk的string

	std::cout << txt_gbk << endl;
}

此時,輸出結果正常:

內碼表為65001

再修改一下程式碼:

int main() {
	std::string txt = "中國人";

	std::cout << txt << endl;
}

在內碼表為65001的情況下,執行無輸出,說明執行結果失敗:

新增兩個轉換方法再試試:

int main() {
	std::string txt = "中國人";

        std::wstring wtxt = string2wstring(txt, "Chinese");
        std::string txt_uft8 = wstring2utf8string(wtxt);

	std::cout << txt_uft8 << endl;
}

在內碼表為65001的情況下,執行成功:

使用std::wcout試一下:

int main() {
	std::string txt = "中國人";

	std::wstring wtxt = string2wstring(txt, "Chinese");
	wcout.imbue(std::locale(std::locale("Chinese"), new std::codecvt_utf8<wchar_t>()));

	std::wcout << wtxt << endl;
}

在內碼表為65001的情況下,執行結果正常:

再舉一個儲存到檔案的例子:

int main() {
	std::string txt = "中國人";

	std::wstring wtxt = string2wstring(txt, "Chinese");
	std::string txt_uft8 = wstring2utf8string(wtxt);

	std::ofstream of("D:/temp/text.txt");

	of << txt_uft8 << endl;
}

以上程式碼執行成功,並將字串以utf-8儲存到檔案了。

再使用如下程式碼試試:

int main() {
	std::string txt = u8"中國人";

	std::ofstream of("D:/temp/text.txt");

	of << txt << endl;
}

也能以utf-8儲存到檔案。

3.在輸入輸出流中使用編碼轉換

除了直接使用轉換器做字串之間的轉換外,如果用到的輸入與輸出流,則可以直接在輸入輸出流上配置需要的轉換器。上面已經有多個例子了,這裡再補充一個例子,程式碼如下:

#include <iostream>
#include <fstream>
#include <string>
#include <locale>
 
int main()
{
    // text.txt是UTF-8編譯的檔案
    std::wifstream fin("D:/temp/text.txt");
    // 檔案輸入流中使用UTF-8轉換器
    fin.imbue(std::locale(std::locale("zh-CN"), new std::codecvt_utf8<wchar_t>()));
    // 控制檯輸出流中使用GBK轉換器
    std::wcout.imbue(std::locale(std::locale("zh-CN"), new std::codecvt_byname<wchar_t, char, std::mbstate_t>("Chinese_China.936")));
    
    for (wchar_t c; fin.get(c); ) {
        std::wcout  << c << endl;
    }
}

以上是在windows上可執行的程式碼 ,如果要在Linux上執行,只需要把Chinese_China.936替換成zh_CN.GBK即可。

4.char* 與wchar_t*互相轉化

在C++11之前,C++標準庫中提供瞭如下兩個函式進行編碼轉換。需要說明的是,這兩個函式在windows下不支援UTF-8,而在Linux下是可以支援UTF-8的:

#include <cstdlib>

std::size_t mbstowcs( wchar_t* dst, const char* src, std::size_t len);
std::size_t wcstombs( char* dst, const wchar_t* src, std::size_t len);

windows不支援UTF-8的原因,據說是因為這兩個函式一開始只支援ANSI內的多位元組編碼方式,而ANSI內的多位元組編碼方式的特點是每個字元不超過兩個位元組,後來utf-8出現後,由於UTF-8中的字元是有可能超過兩個位元組的,如果要加入UTF-8,會對現在函式做大量修改,因此Microsoft沒有讓這兩個函式支援UTF-8。也可以說,這兩個函式不認為UTF-8是多位元組編碼方式。

以下為Linux下可以執行的程式碼:

#include <iostream>
#include <clocale>
#include <cstdlib>
int main()
{
    std::setlocale(LC_ALL, "en_US.utf8");
    std::wcout.imbue(std::locale("en_US.utf8"));
	
    const char* mbstr = u8"中國人";
	
    wchar_t wstr[5];
    std::mbstowcs(wstr, mbstr, 5);
    std::wcout << "wide string: " << wstr << '\n';
}
#include <iostream>
#include <clocale>
#include <cstdlib>
 
int main()
{
    std::setlocale(LC_ALL, "en_US.utf8");
    // UTF-8 narrow multibyte encoding
    const wchar_t* wstr = L"中國人";
	
    char mbstr[11];
    std::wcstombs(mbstr, wstr, 11);
    std::cout << "multibyte string: " << mbstr << '\n';
}

如果要在windows上使用與UTF-8相關的編碼轉換,也可以考慮使用如下函式,只不過這將失去平臺移植性:

MultiByteToWideChar
WideCharToMultiByte
void mbtowchar(const char* input, wchar_t* output) {
  int len = MultiByteToWideChar(CP_UTF8, 0, input, -1, NULL, 0);
  MultiByteToWideChar(CP_UTF8, 0, input, -1, output, len);
}

總結

從C++11開始,標準C++庫引入相關API,提供了標準的字元編碼轉換方式,方便開發者開發跨平臺的字元編碼轉換程式碼。然而,上述程式碼中使用的wstring_convert、codecvt_utf8已在C++17被棄用了,至於由什麼來替代,C++標準中沒有說,現在只好繼續使用它們,等待新的C++標準出來了。

參考文件: