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

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

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


1. Visual Studio字符集

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

這個設置項不會對編譯器處理字符編碼產生直接的影響(注意這裏的「直接」二字,第3節會說到),只會在工程屬性配置屬性-->C/C++-->預處理器加入相應的宏:github

使用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定義的內存空間中。windows

因此,用char仍是wchar_t來存儲字符,只是內存分配和數據存儲上面的事情,它們自己也是與字符編碼無直接關係的( 一樣注意這裏的「直接」二字,第3節會說到)。函數

3. 編譯器如何處理硬編碼字符

VC++編譯器編譯源代碼的步驟中,涉及編碼處理的步驟主要有2個:
第1步:預處理
1.1) 讀取源文件,判斷源文件採用的字符編碼類型。(這一步不會改變文件內容)visual-studio

編譯器判斷源文件編碼類型的步驟爲:
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); 會發現彈出的對話框中顯示仍然是亂碼。
可使用下面的代碼進行測試(ANSIToUTF八、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

相關文章
相關標籤/搜索