撥開字符編碼的迷霧--編譯器如何處理文件編碼


撥開字符編碼迷霧系列文章鏈接:

  1. 撥開字符編碼的迷霧--字符編碼概述
  2. 撥開字符編碼的迷霧--編譯器如何處理文件編碼
  3. 撥開字符編碼的迷霧--字符編碼轉換
  4. 撥開字符編碼的迷霧--MySQL數據庫字符編碼

1. Visual Studio字符集

使用Visual Studio創建的C++工程可以在工程屬性配置屬性-->常規中配置字符集:使用Unicode字符集(默認)、使用多字節字符集
如圖:
vs字符集設置

這個設置項不會對編譯器處理字符編碼產生直接的影響(注意這里的“直接”二字,第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環境下都正常編譯,且運行起來沒有亂碼的程序,需要遵循如下原則:

  1. 代碼文件采用UTF-8 with BOM編碼。
  2. Visual Studio字符集設置為Unicode字符集。
  3. 使用wchar_t。

做到上面3步,你的代碼被別人從github上clone下來編譯,不會因為你代碼中含有中文等字符,產生類似error C2015這樣的編譯錯誤,更不會產生亂碼。

本文介紹的方法只用來解決硬編碼字符亂碼的問題,至於數據傳輸中的亂碼,需要統一字符編碼來解決。

參考: https://blogs.msdn.microsoft.com/vcblog/2016/02/22/new-options-for-managing-character-sets-in-the-microsoft-cc-compiler


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM