撥開字符編碼迷霧系列文章鏈接:
1. Visual Studio字符集
使用Visual Studio創建的C++工程可以在工程屬性配置屬性-->常規
中配置字符集:使用Unicode字符集
(默認)、使用多字節字符集
。
如圖:
但這個設置項不會對編譯器處理字符編碼產生直接的影響(注意這里的“直接”二字,第3節會說到),只會在工程屬性配置屬性-->C/C++-->預處理器
加入相應的宏:
使用Unicode字符集 --> _UNICODE和UNICODE宏
使用多字節字符集 --> _MBCS宏
這幾個宏一般用來判斷是使用char還是wchar_t,在系統API中使用比較多,如MessegeBox通過是否定義了UNICODE宏來決定是使用LPCSTR還是LPCWSTR(LPCSTR即const char, LPCWSTR即const wchar_t):
#ifdef UNICODE
#define MessageBox MessageBoxW
#else
#define MessageBox MessageBoxA
#endif // !UNICODE
2. char和wchar_t
上面提到了,定義API時通過判斷UNICODE宏是否定義來決定是使用char還是wchar_t,那么char和wchar_t有什么不同了?
char和wchar_t是標准C/C++字符類型,並不是windows特有的。 char固定占1個字節,wchar_t固定占2個字節,從內存的角度來看,char、wchar_t和其他數據類型一樣,只是代表一段內存塊,用來存儲固定長度的二進制0或1。 在編程時,我們一般習慣於將字符串儲到char或wchar_t定義的內存空間中,將整形存儲在int定義的內存空間中。
所以,用char還是wchar_t來存儲字符,只是內存分配和數據存儲上面的事情,它們本身也是與字符編碼無直接關系的( 同樣注意這里的“直接”二字,第3節會說到)。
3. 編譯器如何處理硬編碼字符
VC++編譯器編譯源代碼的步驟中,涉及編碼處理的步驟主要有2個:
第1步:預處理
1.1) 讀取源文件,判斷源文件采用的字符編碼類型。(這一步不會改變文件內容)
編譯器判斷源文件編碼類型的步驟為:
1. 若文件開始處有BOM(EF BB BF),則判定為UTF-8編碼;
2. 若沒有BOM,則試圖從文件的前8個字節來判斷文件是否像UTF-16編碼,如果像,則就判斷為UTF-16編碼。
3. 如果既沒BOM,也不是UTF-16編碼,則使用系統當前的代碼頁(簡體中文操作系統為CP936)。
不了解字符編碼的朋友可以參考前一篇博客撥開字符編碼的迷霧--字符編碼概述
1.2) 將源文件內容轉成源字符集
(Source Character Set),默認為UTF-8編碼。
第2步:鏈接
2.1) 將1.2中得到的UTF-8轉為執行字符集
(Execution Character Set):
- 對於寬字符串(即C/C++中以
L
標記的串,如L"abc"
,L'中'
),執行字符集
為UTF-16編碼。 - 對於窄字符串(和寬字符串對應,即不以
L
標記的串),執行字符集
為系統當前的代碼頁。
現在我們就可以說清楚Visual Studio字符集設置、char、wchar_t是如何間接影響到編譯器對字符編碼的處理了:
Visual Studio字符集設置
|
決定聲明哪一個宏(UNICODE還是_MBCS宏)
|
宏又決定了API參數使用char還是wchar_t
|
編譯器在進行【執行字符集】編碼時對char和wchar_采用不同的處理方式,從而對字符編碼產生了影響。
在Visual Studio 2010(含)之后,支持使用
# pragma execution_character_set
來設置執行字符集。
4. 實例分析
- 已知漢字“中”的各種編碼如下:
GBK D6 D0
Unicode 2D 4E
UTF-8 E4 B8 AD
- 函數
DumpCharacterCode
用於按字節打印內存中的數據:
void DumpCharacterCode(const char* pChar, int iSize) {
for(int i = 0; i < iSize; i++) {
char a = *pChar++;
printf("%02X ", a & 0xff);
}
printf("\n");
}
-
設置系統代碼頁的方法:
“控制面板” --> “區域和語言” --> “管理” --> “非Unicode程序的語言” --> “更改系統區域設置” -
Visual Studio保存文件到指定編碼方法:
“文件” --> “高級保存選項”
4.1 測試編譯器處理窄字符編碼
測試代碼如下:
int _tmain(int argc, _TCHAR* argv[])
{
char buf[100] = {"中"}; // char
DumpCharacterCode(buf, 2); // 也可以打印4個字節
return 0;
}
針對不同的系統代碼頁和源文件編碼,打印出的漢字“中”的編碼分別為:
測試用例 | 系統代碼頁 | 保存源文件編碼 | 編譯器判斷文件采用的編碼 | 源字符集(Source Character Set) | 執行字符集(Execution Character Set) | 打印輸出 |
---|---|---|---|---|---|---|
用例1 | 簡體中文 CP936 | 簡體中文 CP936 | 簡體中文 CP936 | UTF-8 | 簡體中文 CP936 | D6 D0 |
用例2 | 簡體中文 CP936 | UTF-8 BOM | UTF-8 | UTF-8 | 簡體中文 CP936 | D6 D0 |
用例3 | 簡體中文 CP936 | UTF-8 | 簡體中文 CP936 | UTF-8 | 簡體中文 CP936 | 編譯錯誤(C2146) |
用例4 | 西歐 CP1252 | 簡體中文 CP936 | 西歐 CP1252 | UTF-8 | 西歐 CP1252 | D6 D0 |
用例5 | 西歐 CP1252 | UTF-8 BOM | UTF-8 BOM | UTF-8 | 西歐 CP1252 | 3F 00 |
表格中列4~6依次對應編譯處理源文件的幾個步驟。
3F
對應的ASCII字符為?
,編譯器遇到不能識別的字符時,就會用?
來替代。 出現?
的情況會伴隨着編譯警告C4566
。
上面出現了1次3F
(用例5),導致亂碼的原因是UTF-8 --> 西歐 CP1252
. 西歐 CP1252
也就是ASCII的擴展,不支持漢字,所以用3F
替代。
用例3為什么會編譯錯誤?
微軟的編譯器只能識別帶BOM的UTF-8,用例3的UTF-8沒帶BOM,編譯器會判定源文件編碼為系統當前代碼頁CP936。“中”的UTF-8編碼為E4 B8 AD
,列5執行從CP936到UTF-8轉換之后變成了E6 B6 93 3F
,列6再要將E6 B6 93 3F
轉換為CP936肯定是轉換不回去的,相當於 UTF-8(1) --> UTF-8 (2),再將UTF-8(2)轉換回CP936,這時肯定得到的字符不是原來的字符了。
用例4為什么輸出的D6 D0
,而不是3F
?
對着用例4的各個順序來看,源文件通過CP936保存着,但編譯器通過CP1252來讀取的,CP1252就是ASCII擴展,單字節的,雖然此時顯示為亂碼,但各字節仍然是D6 D0;然后將讀取到的文件內容從CP1252轉成UTF-8編碼,轉碼后為C3 96 C3 90;然后再將UTF-8編碼轉回為CP1251,轉碼就又變成了D6 D0。 但這個D6 D0
在CP1252中是無法顯示的,如果我們在用例4加入MessageBoxA(NULL, "中", "test", MB_OK);
會發現彈出的對話框中顯示仍然是亂碼。
可以使用下面的代碼進行測試(ANSIToUTF8、UTF8ToANSI函數見撥開字符編碼的迷霧--字符編碼轉換):
int _tmain(int argc, _TCHAR* argv[])
{
char buf[3] = { 0 }; // 模擬CP936編碼的“中”
buf[0] = 0xD6;
buf[1] = 0xD0;
std::string strUTF8 = ANSIToUTF8(buf, 1252);
char *p = (char*)strUTF8.c_str(); // 通過visual studio查看指針p處內存為: C3 96 C3 90
std::string str = UTF8ToANSI(strUTF8, 1252);
p = (char*)str.c_str(); // 通過visual studio查看指針p處內存為: D6 D0
return 0;
}
4.2 測試編譯器處理寬字符編碼
測試代碼如下:
int _tmain(int argc, _TCHAR* argv[])
{
wchar_t buf[100] = {L"中"}; // wchar_t
DumpCharacterCode((char*)buf, 4); // 打印4個字節
return 0;
}
同樣,針對不同的系統代碼頁和源文件編碼,打印出的漢字“中”的編碼分別為:
測試用例 | 系統代碼頁 | 保存源文件編碼 | 編譯器判斷文件采用的編碼 | 源字符集(Source Character Set) | 執行字符集(Execution Character Set) | 打印輸出 |
---|---|---|---|---|---|---|
用例1 | 簡體中文 CP936 | 簡體中文 CP936 | 簡體中文 CP936 | UTF-8 | UTF-16 | 2D 4E 00 00 |
用例2 | 簡體中文 CP936 | UTF-8 BOM | UTF-8 | UTF-8 | UTF-16 | 2D 4E 00 00 |
用例3 | 簡體中文 CP936 | UTF-8 | 簡體中文 CP936 | UTF-8 | UTF-16 | 編譯錯誤(C2146) |
用例4 | 西歐 CP1252 | 簡體中文 CP936 | 西歐 CP1252 | UTF-8 | UTF-16 | D6 00 D0 00 大小端 |
用例5 | 西歐 CP1252 | UTF-8 BOM | UTF-8 BOM | UTF-8 | UTF-16 | 2D 4E 00 00 |
5. 徹底避免硬編碼字符亂碼
通過第3節的說明,很容易知道,要開發支持多語言,在任意語言(系統代碼頁)的windows環境下都正常編譯,且運行起來沒有亂碼的程序,需要遵循如下原則:
- 代碼文件采用UTF-8 with BOM編碼。
- Visual Studio字符集設置為Unicode字符集。
- 使用wchar_t。
做到上面3步,你的代碼被別人從github上clone下來編譯,不會因為你代碼中含有中文等字符,產生類似error C2015
這樣的編譯錯誤,更不會產生亂碼。
本文介紹的方法只用來解決硬編碼字符亂碼的問題,至於數據傳輸中的亂碼,需要統一字符編碼來解決。