中文在風靡全球的路上如果一定有阻礙,那就是亂碼啊。引無數大神盡折腰的編碼轉換問題,這篇文章就記錄下這個問題。
大家知道,計算機是只認識二進制的,如果一個字符變成了我們肉眼可見的亂碼時,一定是因為我們給了計算機錯誤的編碼格式導致的。
文件編碼
文章開始,我們先說說編程時,我們的保存代碼的文件的編碼,以VS2008為例,在中文操作系統下,我們的編程文件會被保存為GB2312編碼,但凡文件中使用了中文,為了正常顯示中文,我們必須使用擴展的編碼方式來顯示這個文件。GB2312算是最早的中文編碼方式了,后面依次出了GBK和GB18030,這些暫且不提,編碼參考可以看這里
#include <iostream> //this file for coder test #include <string> using std::cout ; using std::string; int main() { string ansiByte = "this"; string chineseByte = "這是中文"; cout << "english lenght:" << ansiByte.length() <<std::endl; cout << "chinese lenght:" << chineseByte.length() <<std::endl; return 0; }
分別把以上代碼,以GB2312和Utf-8保存后,生成的結果分別為:
english lenght:4 chinese lenght:8
english lenght:4 chinese lenght:12
以上結果很清楚的證明了以上說法。當然了,英文字符是編碼界的一等公民的,所以編碼方式變化基本不會影響代碼最后的結果。而對於中文,我們就很尷尬了,畢竟string的length函數不可用的情況下,對應的c語言里的strlen函數也是不准確的。這個問題 ,我們在后面說說如何解決。現在,我們還是說回文件的編碼問題,因為WINDOWS的文本文件換行格式與LINUX不同的原因,WINDOWS行尾使用\r\n來換行,LINUX行尾使用\n來換行,\r在LINUX就會顯示成奇怪的符號。這樣的系統級的差異,我們不會擴展,只以windows系統說明后面的問題。
不同的文件編碼在轉換時,必然會引起亂碼問題,這里的亂碼是因為錯誤的字符映射關系導致的。類似的說明網文也是很多的。
這里只說明一點,為使計算機支持更多語言,兼容ASCII碼表,在0x00~0x7F之間的字符,依舊是1個字節代表1個字符。通常使用 0x80~0xFF 范圍的 2 個字節來表示 1 個非英語字符。像GB2312, BIG5, JIS 等使用ANSI碼表的0x80~0xFF范圍的 2 個字節來代表一個字符的各種漢字延伸編碼方式,統稱為ANSI 編碼。因此,在簡體中文系統中,ANSI編碼實際上是指GBK(GB2312或者GB18030).
字符編碼
開發者除了需要處理文件的編碼導致的亂碼外,還需要處理編碼過程中,網絡傳輸時的亂碼問題。在開發過程中,我們經常遇到TCHAR來表示一個字符字節,眾所周知的原因,這里其實隱藏了char和wchar_t類型,即所謂的窄字節 和 寬字節,一個char對應1個8bit,而wchar_t又存在平台差異,windows下是2個8bits,對應一個UTF-16編碼字符(Linux下是4個8bit,對應一個UTF-32字符),Windows下的wchar_t定義為 typedef unsigned short wchar_t; /* 16 bits */ 在編碼時,我們使用L來表示我們使用的是Unicode編碼,但是wchar_t也可以存儲其他編碼的字符,即如果我們不加L標識的話,這個字符就是我們的編程文件的編碼格式,但是在linux下我們無法使用不加L的字符串給wchar_t類型賦值了。來,show me the code:
wchar_t width_t1 = '中'; wchar_t width_t2 = L'中'; printf( "%0x %0x\n",width_t1,width_t2);
文件編碼為GB2312時的輸出
d6d0 4e2d
文件編碼為UTF-8時的輸出
e4b8ad 4e2d
由此可見,當使用L修飾的中文后, 因為它已經表明使用了特定的編碼方式,所以它在內存中的數值是一致的,不會隨着文件的編碼而改變,但是不加入L則就與文件的編碼格式相關了。現在,我們已經積累了2個隨着文件編碼方式不同,導致程序輸出不同的問題了,這些都是亂碼的根源。
亂碼回歸
為了弄清楚亂碼的問題,我們從文件的編碼方式,編程方式來說明了潛在的問題,現在,我們來看看可以從哪些方面解決這個問題。
-
文件編碼
為了正常顯示中文,我們可以將文件編碼設置為GB2312的,但是現在很多IDE把字符編碼默認為UTF-8了比如Qt的IDE ,QtCreator。如前所述,UTF-8存儲中文時占用的磁盤空間比GB2312更大些,嗯,其實吧,存儲空間並不是什么大問題。不同的文件編碼其實影響的是我們在做網絡數據的傳輸,我們需要根據不同的編碼來轉碼為UTF-8了。
備注說明下:在linux下,含中文的文件最好保存為UTF-8格式的,使用ANSI編碼在編輯時可能會報"converting to execution character set: Invalid or incomplete multibyte or wide character";這需要做很多額外的工作來消除。
-
使用嚴格的寬字節
現在,我們假設,文件的編碼格式為GB2312,而我們需要使用中文字符串了,使用string char等類型來存儲中文字符是完全可以的,那么這里就會涉及到1個中文2個char字節的口訣了,古時候(嗯,那時候),即時現在,很多人都還沒玩壞這個口訣。這自然不是武功秘籍,所以,我們需要改變下編碼思路,因為,直接使用string存儲中文,字符長度等函數都是不准確的。於是,我們使用了嚴格的寬字節來規避這些問題,VS中可以把字符設置為Unicode,這個設置不是為文件編碼准備的,是為編程中的編碼准備的。設置完畢后,我們將合理的使用TCHAT等字符類型。當字符設置為Unicode后,這里的TCHAR會被解析為wchar_t,個人其實並不喜歡微軟的字符封裝,所以更喜歡直接使用C++標准庫中的wchar_t及其標准操作函數。show me the code:
wchar_t width_t1[] = "中文1測試起來"; //error C2440: “初始化”: 無法從“const char[14]”轉換為“wchar_t []”
以上代碼說明,vs下沒有加L的中文串默認為chat類型,
wchar_t width_t2[] = L"中文2測試起來"; printf( "%s \n",width_t2);
結果(powershell下顯示)
-N噀2
又見亂碼,喜不勝收啊,分析下,加了L后,我們的wchar中加入的為UTF-16的字符編碼,而我們的顯示界面為powershell的字符集為GBK的,把utf-16的字符解析為GBK自然就是亂碼了。於是,就涉及到解碼了,這里,我們的目標就是把wchar_t類型的字符轉為ANSI編碼,如果不知道為什么是這樣的轉換,可以多查看資料,其實,本文前面也略有提及,具體的轉換在下節。
wstring wchar1(L"this"); wstring wchar2(L"只有中文"); wstring wchar3(L"中e混合"); std::cout << "e文長度 :" << wchar1.length()<<std::endl; std::cout << "中文長度 :" << wchar2.length()<<std::endl; std::cout << "混合長度 :" << wchar3.length()<<std::endl;
e文長度 :4
中文長度 :4
混合長度 :4
可見,在寬字節加持下,中英文的長度等總算統一了,所有的字符都是按一個計算,讓一個中文是兩個英文字符見鬼去吧,我們完全不需要關系在磁盤或者內存中字符的存儲方式,因為我們更關心編程的字符串本身。
-
編碼格式轉換
在Windows下,編碼轉換主要是WideCharToMultiByte和MultiByteToWideChar函數在此之前,我們先來了解下代碼頁的概念,參考這里,知道了codepage的存在,我們就很好理解這兩個函數了,這兩個函數就是在查表,找到寬字節 到 多字節(ANSI)的對應關系;
show me the code
//函數名中的Unicode實際就是UTF-16, //需要加頭文件 Windows.h CHAR * UnicodeToAnsi(const WCHAR * lpszStr) { CHAR * lpAnsi; int nLen; if (NULL == lpszStr) return NULL; //查找代碼頁,先獲取寬字節到多字節時需要的byte長度 nLen = ::WideCharToMultiByte(CP_ACP, 0, lpszStr, -1, NULL, 0, NULL, NULL); if (0 == nLen) return NULL; lpAnsi = new CHAR[nLen + 1]; if (NULL == lpAnsi) return NULL; memset(lpAnsi, 0, nLen + 1); //執行映射轉換,兩次調用同樣的函數,獲取不同的值,總覺得 哪里不對,不過任務算是完成了 nLen = ::WideCharToMultiByte(CP_ACP, 0, lpszStr, -1, lpAnsi, nLen, NULL, NULL); if (0 == nLen) { delete []lpAnsi; return NULL; } return lpAnsi; } char * charansi = UnicodeToAnsi(width_t2); printf( "%s \n",charansi); delete []charansi;
執行后OK,這一次總算顯示正常了。

以上,告訴了你從wchar_t向ANSI轉換的過程,那么同理,你可以寫出其他的轉換函數了,只是,知道為什么要轉其實比知道怎么轉更重要。
-
網絡通訊使用UTF-8編碼
UTF-8的地位是互聯網給與的,而Unicode的推廣是因為UTF-8,其中緣由自己百度咯。同樣的道理,在聯網通信時,為了讓對方正確識別,我們需要把字符全部轉成UTF-8,
前文說過,我們需要使用寬字節來使用中文字符串,所以,這里我們只需要把寬字節的UTF-16轉換為UTF-8即可。只有英文的可以不做轉換。
