字符集編碼與 C/C++ 源文件字符編譯亂彈[轉載]

最近在看國際化編程 (i18ninternationalization) 的東西,也弄清楚了點字符集有關的一些問題,其實網上的一些牛人已經將字符集、Unicode 等相關的問題說的很清楚了,我在這裏引用他們的總結並自己小結一下心得,並且實驗一下在編譯時,源代碼自身的字符集與編譯生成工具之間的問題。

locale與字符集

locale,中文有時翻譯成“現場”,還不如叫英文的locale好,它的意思是“一套和地域有關的習慣而形成的程序運行上下文”,它由很多方面 (category) 組成,比如:某個地區的人們習慣怎樣表示他們的貨幣金額 (LC_MONETARY) ,是用 "$100",還是用 "¥100";習慣怎麼表示十進制多位數 (LC_NUMERIC) ,是每一千位進行分隔 "100,000",還是每一萬位進行分隔 "10,0000";習慣怎麼表示日期時間 (LC_TIME) ,是日-月-年的方式 "30-01-1999",還是年-月-日的方式 "1999-01-30",等等還有其它一些方面,不過其中我們最關心的是一個叫 LC_CTYPE 的,CTYPE 的含義大概是:Character Type(字符類型),它表示某個地區的字符用哪個字符集進行編碼。還有LC_ALL,它是其它所有方面的並集。

C 標準庫中設置 locale 的函數是:setlocale(),MSDN VC10 參考:Language and Country/Region Strings

 

字符集(Character-Set)按照發明順序和繼承關係,有以下常用的幾種:

  1. ASCII

    ANSI 發佈的字符編碼標準,編碼空間 0x00-0x7F,佔用1個字節,上學時學的 C 語言書後面的字符表中就是它,因爲使用這個字符集中的字符就已經可以編寫 C 程序源代碼了,所以給這個字符集起一個 locale 名叫 C,所有實現的 C 語言運行時和系統運行時,都應該有這個 C locale,因爲它是所有字符集中最小的一個,設置爲其它 locale 時可能由於不存在而出錯,但設置 C 一定不會出錯,比如:當 Linux 的 LANG 配置出錯時,所有的 LC_* 變量就會被自動設置爲最小的 C locale。MSDN VC10 參考:Code Pages

  2. ISO-8859-1

    ISO 發佈的字符編碼標準,又稱 Latin-1 字符集,編碼空間 0x00-0xFF,佔用1個字節,可以編碼大多數的西歐地區語言。參考:ISO IEC 8859-1

  3. GB2312,GBK,GB18030

    GB 系列是由中國國標局發佈的字符編碼方案(其中 GBK 不是正式標準),後期發佈的版本兼容之前的,是之前的超集。

    參考:中文的幾個編碼 by blade2001

    • GB2312

      爲1-2字節變長編碼,漢字區中編碼 6763 個字符。

    • GBK

      是微軟對 GB2312 的擴展,後由國標局作爲指導性標準,爲1-2字節變長編碼,編碼 21886 個字符,分爲漢字區和圖形符號區。漢字區編碼 21003 個字符,支持CJK漢字(簡體、繁體、常用日韓文),Windows 代碼頁爲 CP936。

    • GB18030

      爲1-2-4字節變長編碼,漢字區編碼 27484 個字符,支持CJK漢字、常用藏文、蒙文、維吾爾文等,Windows 代碼頁爲 CP54936。

上面的所有編碼都將ASCII作爲自己的子集實現,所以這些字符集又叫做本地化的(Native)ANSI 字符集。

一般在程序中爲了支持國際化,在程序初始化時,將 locale 設置爲系統配置的 Native ANSI 字符集,即執行:setlocale(LC_ALL, "")。

ASCII、ISO-8859-1 這種用1個字節編碼的字符集,叫做單字節字符集(SBCS - Single-Byte Character Set)。

GB 系列這種用1-2、4個不等字節編碼的字符集,叫做多字節字符集(MBCS - Multi-Byte Character Set)。

由 SBCS 編碼的數據可以隨機訪問,從任意字節偏移開始解編碼都能保證解析出的字符和後繼字符是正確的。

而由MBCS編碼的數據,只能將其作爲字節流進行解析,如果從隨機任意的字節偏移開始解編碼,有可能定位到切斷一個字符的中間位置,導致後繼解析出的字符連續出錯。作爲字節流時,是從某個標識位置進行解析字符,比如從數據的開始位置,或從每個新行符 '\n' 之後開始解析字符。

Unicode 的理解

參考:談談 Unicode 編碼,簡要解釋 UCS、UTF、BMP、BOM 等名詞 by fmddlmyy

首先,Unicode 它不是一個東西,它至少涉及3個方面:Code Point,UCS,UTF。

每個地區、國家都有自己的 Native ANSI 字符集,雖然它們在 ASCII 子集部分是相同的,但其它部分都不盡相同,如果一個字符僅在一個特定的Native ANSI 字符集中編碼,那麼好,如果用戶使用別的字符集,那麼無論如何也無法解析和表現這個字符。怎樣讓你的文本數據同時可以包含英、法、德、中、日、阿拉伯甚至世界上所有可能的、完全的字符?辦法似乎只有一個,就是在世界範圍內對所有可能的字符進行窮舉編碼,這就是最初提出Unicode 的原因,全稱爲 Universal Multiple-Octet Code

  1. Code Point

    在實際編碼之前先給每個窮舉到的字符指定一個序號,叫它 Code Point,把它當做是數學概念,和用幾個字節存儲無關,只要發佈Unicode 的標準化組織(ISO 和 unicode.org)願意,將新出現的字符繼續向後編號就可以了,既然數學序號,就沒有什麼不夠用的問題。編號時有一些原則,就是越常用的字符越靠前,編號到一定數量後,發現差不多了,常用字符都編完了,截止於此將之前的編號組成的子集叫做基本多文種平面(BMP - Basic Multilingual Plane),在 BMP 裏的字符,只要4位16進制數就可以表示,當然在 BMP 以外的字符則需要使用5位或更多16進制數表示。比如:"漢" 字在 BMP 裏,它的 Code Point 可以表示爲 U+6C49。常用CJK漢字都落在 BMP 內,所以都能用U+HHHH 的形式表示其 Code Point。Windows 下有個字符映射表(charmap.exe)的工具,可以列舉每個字符的Code Point、字體支持、字符集之間的關係。

  2. UCS

    有了 Code Point 後就可以規定它的字符集,叫做 UCS - Unicode Character Set,它和存儲有關,用2個字節存儲 Code Point 叫做 UCS-2,用4個字節存儲的叫做 UCS-4,UCS-2 可以編碼並存儲 BMP 中的所有字符,而如果不夠用了(要用到 BMP 外的字符),則可以使用 UCS-4。通常交流中提到 Unicode,如果不特指,就指代的是 UCS-2。

    UCS 和 Native ANSI 字符集採用的 MBCS 編碼是不同的,UCS 不將 ASCII 作爲自己的子集,無論什麼情況 UCS 總使用定長的字節來編碼字符,UCS-2 使用2個字節,UCS-4 使用4個字節,而不是 Native ANSI 字符集中可能採用的變長編碼。比如:"A" 在 GB2312 中編碼爲 0x41,而在 UCS-2 中編碼爲 0x0041。

    當存儲多字節編碼的數據並且不將其作爲字節流解析時,就要考慮保存數據的大小端問題(Endian),可以使用 BOM(Byte Order Mark)標識一個 UCS 字符數據塊是採用 Big Endian 還是 Little Endian 進行存儲:在 Unicode 概念中有一個字符,它的 Code Point 爲 U+FEFF,實際上它不映射到任何地區、國家中的可能字符,即是一個不可能存在字符的 Code Point((-_-^),Unicode 標準對它的註釋爲:ZERO WIDTH NO-BREAK SPACE),當開始處理 UCS 數據塊時,UCS 標準建議先處理這個 ZERO WIDTH NO-BREAK SPACE 字符,比如 UCS-2 數據塊,如果一開始讀到/寫入的字節序列是 FF FE(8 進制:377 376),那麼說明後續的 UCS-2 按 Little Endian 存儲;如果是 FE FF(8進制:376 377),則說明後續的 UCS-2 按 Big Endian 存儲。

    採用定長的 UCS 有一個好處,就是可以像 SBCS 一樣隨機訪問數據塊中的任何字符,當然這裏的隨機偏移單位不是每字節:當用 UCS-2,是每2字節隨機偏移,當用 UCS-4 時,是每4字節隨機偏移。

    但是 UCS 也有缺點,一是有些浪費:比如用 UCS-2,如果在一個數據塊中只使用對應於 ASCII 中的字符,那麼有一半存儲都被浪費掉了,因爲對應於ASCII 中的字符,它的 UCS-2 編碼實際上是它的 ASCII 編碼加上填0的高1字節組成的2字節編碼,那種使用16進制編輯器打開文件後隔一列爲0的字符文件就是這種情況。二是和 ASCII 不兼容,由於太多的已有系統使用 ASCII(或 Native ANSI)了,這點使 UCS 和其它系統對接時有點麻煩。

  3. UTF

    UTF - Unicode Transformation Format,作爲 Unicode 的傳輸編碼,是對 UCS 再次編碼映射得到的字符集,能夠一定程度上解決上面 UCS 的2個缺點。UTF-8 是以8位爲單元對 UCS-2 進行再次編碼映射,是當前網絡傳輸、存儲優選的字符集。UTF-8 使用8位單元(1字節)變長編碼,並將 ASCII 作爲子集,這樣就可以將 UTF-8 當做一種 MBCS 的 Native ANSI 字符集的實現,因此 UTF-8 需要使用1字節流方式解析字符。處於BMP 中的CJK漢字,使用 UTF-8 編碼時通常會映射到3字節序列,而 GB 系列字符集中的CJK漢字通常爲2字節序列。

    UTF-8 和所有的 Native ANSI 字符集一樣:當數據塊中只有 ASCII 子集部分的字符時,是無法區分這個數據塊用哪種 Native ANSI 字符集進行編碼的,因爲這部分的編碼映射關係對於所有的 Native ANSI 字符集是共享的,只有當未來數據塊中包含像CJK漢字這種在 ASCII 子集之外的字符時,採用不同 Native ANSI 字符集的數據塊纔會表現出不同。

    不過有一種方法可以讓數據塊標識自己使用的是 UTF-8 編碼(即使字符內容都在 ASCII 內),這對於文本編輯器等應用很有用,它們可以使用這個標識判斷文件當前使用的字符集,以便未來插入 ASCII 之外的字符時決定如何編碼。這個標識方法就是使用 UCS-2 中 BOM 的 UTF-8 編碼,其1字節流爲:EF BB BF(8 進制:357 273 277)。當數據塊的開始有這個流時就說明後續字符采用 UTF-8 編碼。因爲 UTF-8 使用1字節流方式處理,這時 BOM 已經失去其在 UCS-2 中作爲標識字節序大小端的作用,而僅把 EF BB BF 作爲 UTF-8 編碼的標識功能(Magic),有時就叫它UTF-8 Signature。但並非所有能處理 UTF-8 數據的應用都假定有 Signature 這個標識功能的存在:微軟的應用大多都支持 UTF-8 Signature,但在開源領域,比如 Linux 下有相當多的程序都不支持 UTF-8 Signature。

源文件字符集與編譯

在 ISO C99 中有了寬字符處理的標準,例程大多在 wchar.h 中聲明,並且有了 wchar_t 這麼一個類型。不管哪種 C 編譯器和標準/RT庫實現,wchar_t 通常都可以認爲是存儲 UCS 字符的類型,C 語言語法中也使用前綴的 L 字符來說明一個字符常量、字符串字面量在編譯時採用 UCS 編碼。

VC8 cl的實現中,默認的編譯選項將 wchar_t 做爲內建類型(選項:/Zc:wchar_t),此時 sizeof(wchar_t) 爲 2,可存儲 UCS-2 編碼。Linux GCC 4 的實現中,sizeof(wchar_t) 爲 4,可存儲 UCS-4 編碼。MinGW 和 Cygwin 的 GCC 4 中,sizeof(wchar_t) 爲 2。

如此有這麼一個疑問:

  1. 源文件程序語法中的字符編碼指示。

  2. 源文件自身的字符編碼。

這2者有何種聯繫?於是我做了如下實驗,試着搞明白編譯器對上面2者的處理作用。分別實驗了3個我常用的編譯工具集:VC8、MinGW GCC、Linux GCC。

  • 先看當程序語法中使用非 wchar_t 字符編碼指示時的情況,按照教科書上的說法這種字符串字面量編譯時使用 ASCII 編碼,因爲其中有漢字,因此我把它想象成用某種 Native ANSI 字符集進行編碼,在隨後的測試和調試中便可判斷這種假定是否正確。

    源碼如下:

    01 #include "common.h"
    02  
    03 #define MAX_BUF_SIZE    256
    04 typedef unsigned char BYTE;
    05  
    06 const char g_szZhong[] = "這是ABC 123漢字";
    07  
    08 int main(int argc, char* argv[])
    09 {
    10     BYTE buf[MAX_BUF_SIZE] = {0};
    11     FILE* fs = NULL;
    12     if ( argc < 2 )
    13     {    return 1;    }
    14  
    15     if ( (fs = fopen(argv[1], "wb")) == NULL )
    16     {    return errno;    }
    17  
    18     memcpy(buf, g_szZhong, sizeof(g_szZhong));
    19  
    20     fwrite(buf, sizeof(char), sizeof(g_szZhong), fs);
    21     fclose(fs);
    22  
    23     return errno;
    24 }

    使用記事本、iconv 等工具將上面的源文件做出5份不同的字符集編碼的出來:GBK、UCS-2 LE、UCS-2 LE(BOM)、UTF-8、UTF-8(BOM)。其中 LE 表示UCS-2 採用 Little Endian 字節序存儲;帶 BOM 的表示:在文件頭有 BOM 標識,對於 UTF-8 來說就是 Signature,沒有帶 BOM 的就沒有這個文件頭標識。

    我編譯生成了上面的程序後,查看了3處字符串的編碼:

    1. 內存中的字符串:使用 gdb、VC 等調試工具,跟蹤 memcpy() 時向 buf[] 中複製的字符數據。

    2. 可執行文件中的字串常量:使用 WinHex、hd 等16進制查看工具,在編譯生成的對象文件和可執行映像文件中查找字符串字面量的中間部分 "ABC 123"。

    3. 該程序寫入的文件:該程序使用 fwrite() 向某個命令行參數指定的文件寫入 buf[] 中的字符數據,查看這個寫入文件的編碼。

    實驗結果

    • MinGW GCC 4.4.0

      源代碼 內存中的字符串 可執行文件中的字串常量 寫入的文件
      GBK GBK GBK GBK
      UCS-2 LE (BOM) 編譯出錯
      UCS-2 LE 編譯出錯
      UTF-8 (BOM) UTF-8 UTF-8 UTF-8
      UTF-8 UTF-8 UTF-8 UTF-8
    • Linux GCC 4.3.2

      源代碼 內存中的字符串 可執行文件中的字串常量 寫入的文件
      GBK GBK GBK GBK
      UCS-2 LE (BOM) 編譯出錯:不識別 BOM (FF FE),且源代碼字符處理出錯
      UCS-2 LE 編譯出錯:源代碼字符處理出錯
      UTF-8 (BOM) 編譯出錯:不識別 BOM (EF BB BF)
      UTF-8 UTF-8 UTF-8 UTF-8
    • VC8 cl 14.00.50727.42

      源代碼 內存中的字符串 可執行文件中的字串常量 寫入的文件
      GBK GBK GBK GBK
      UCS-2 LE (BOM) GBK GBK GBK
      UCS-2 LE GBK GBK GBK
      UTF-8 (BOM) GBK GBK GBK
      UTF-8 UTF-8 UTF-8 UTF-8

    實驗小結

    1. 基本上每種編譯器都有自己的源代碼字符集編碼處理方式。

    2. 基本印證了開始的觀點:如果能編譯成功,則編譯器確實是用某種 Native ANSI 字符集來編碼非 L 前綴的字符串字面量,並且如果需要會在編譯過程中做一些字符集編碼轉換,如 VC8 cl。

    3. GCC的工作方式似乎很直接了當:不管你源文件是什麼編碼,編譯時只將兩引號之間的字符串複製一份到對象文件中,參考:在 Linux C 編程中使用 Unicode 和 UTF-8。VC8 也可以有這種工作方式,那就是不帶 BOM 的 UTF-8 源文件。

    4. VC8 對 Unicode 類源文件有較好的支持沒有什麼稀奇的,因爲微軟也是 Unicode 不遺餘力的推廣者,參考:各 C/C++ 編譯器對 wchar_t 字符和字符串的正確支持程度

  • 再看當程序語法中使用wchar_t字符編碼指示時,即在程序中使用UCS字符或字符串時情況,將上面源程序改爲如下:

    01 #include "common.h"
    02  
    03 #define MAX_BUF_SIZE    256
    04 typedef unsigned char BYTE;
    05  
    06 const wchar_t g_szZhong[] = L"這是ABC 123漢字";
    07  
    08 int main(int argc, char* argv[])
    09 {
    10     BYTE buf[MAX_BUF_SIZE] = {0};
    11     FILE* fs = NULL;
    12     if ( argc < 2 )
    13     {    return 1;    }
    14  
    15     if ( (fs = fopen(argv[1], "wb")) == NULL )
    16     {    return errno;    }
    17  
    18     memcpy(buf, g_szZhong, sizeof(g_szZhong));
    19  
    20     fwrite(buf, sizeof(char), sizeof(g_szZhong), fs);
    21     fclose(fs);
    22  
    23     return errno;
    24 }

    實驗結果

    • MinGW GCC 4.4.0

      源代碼 內存中的字符串 可執行文件中的字串常量 寫入的文件
      GBK 編譯出錯:無法將源代碼中字符量轉換到 UCS
      UCS-2 LE (BOM) 編譯出錯:不識別 BOM (FF FE),且源代碼字符處理出錯
      UCS-2 LE 編譯出錯:源代碼字符處理出錯
      UTF-8 (BOM) UCS-2 LE UCS-2 LE UCS-2 LE
      UTF-8 UCS-2 LE UCS-2 LE UCS-2 LE
    • Linux GCC 4.3.2

      源代碼 內存中的字符串 可執行文件中的字串常量 寫入的文件
      GBK 編譯出錯:無法將源代碼中字符量轉換到 UCS
      UCS-2 LE (BOM) 編譯出錯:不識別 BOM (FF FE),且源代碼字符處理出錯
      UCS-2 LE 編譯出錯:源代碼字符處理出錯
      UTF-8 (BOM) 編譯出錯:不識別 BOM (EF BB BF)
      UTF-8 UCS-4 LE UCS-4 LE UCS-4 LE
    • VC8 cl 14.00.50727.42

      源代碼 內存中的字符串 可執行文件中的字串常量 寫入的文件
      GBK UCS-2 LE UCS-2 LE UCS-2 LE
      UCS-2 LE (BOM) UCS-2 LE UCS-2 LE UCS-2 LE
      UCS-2 LE UCS-2 LE UCS-2 LE UCS-2 LE
      UTF-8 (BOM) UCS-2 LE UCS-2 LE UCS-2 LE
      UTF-8 UCS-2 LE (Wrong) UCS-2 LE (Wrong) UCS-2 LE (Wrong)

    實驗小結

    1. 如果用 GCC 編譯器,並且要在程序中使用 UCS 字符,目前一定要滿足這個條件:源代碼用不帶 BOM 的 UTF-8 編碼保存。

    2. Linux GCC 的實現中果然將 wchar_t 作爲4字節的 UCS-4 的存儲類型,能處理 UCS-4 的應用似乎不多(反正 Windows 記事本不行),所以最好就不要把它作爲保存文件的編碼了,把它做爲程序內部字符處理的編碼就好(例如計算含CJK漢字的字符串中字符個數)。

    3. 上面所有的 UCS 字符存儲都使用 Little Endian 字節序,是因爲我在x86架構的PC上作的實驗,沒有什麼特別的。

    4. 當用 VC8 編譯不帶 BOM 的 UTF-8 源文件時,雖然可以生成可執行程序,但是其中對 UCS 字符的編碼出現了錯誤,在上表中用 Wrong 表示,其表現爲:超出 ASCII 之外的字符變爲另外奇怪的字符,即沒有將源代碼中的 UTF-8 字符串 "這是ABC 123漢字" 正確轉換成 UCS-2 編碼。原因是:當使用不帶 BOM 的 UTF-8 文件時,是沒有辦法從“表象”上區分它究竟是 UTF-8 還是別的 Native ANSI 字符集編碼,只能按照應用特定的優選編碼來解析字符,而 VC8 編譯器會優選系統配置的 Native ANSI 字符集作爲解析字符集,因爲是簡體中文 Windows 所以這個是 GBK 字符集,編碼錯誤就是因爲 VC8 編譯器把實際爲 UTF-8 編碼的字符當做 GBK 編碼向 UCS-2 轉換造成的。另外,VC8 有個特殊的 #pragma setlocale 預編譯指示,用來指示源文件的編碼。

    5. 後來又用 Cygwin GCC 4.3.4 測試了下,它和 MinGW GCC 的效果基本相同,也用 UCS-2 編碼 wchar_t 型字符,只是無法識別帶 BOM 的 UTF-8 源文件,所以更像原版的 Linux GCC,而 MinGW 的目標是 Windows native 的工具集,所以考慮了 Windows 的習慣。

源文件字符集總結

目前 GCC 對 UCS-2、BOM 的源文件都不太感冒,對於 GCC 來說最好的源文件編碼是不帶 BOM 的 UTF-8。而 VC8 中相反,如果使用 Unicode 類編碼來保存源文件,最好帶上 BOM。

碎碎念

以前在聽人談 Unicode 時,總會說:“Unicode 用2個字節編碼字符,所以可以表示所有的字符,不存在平臺移植的問題,Java 中就用它”,聽的很迷糊,在自己靜下心來看看 i18n 的東西后,才發現說這些話的人也許根本不明白 Unicode 是怎麼回事,或者是知道點皮毛、用過其中最簡單的方法就以爲自己會了然後妄論一番,自從上學以來遇見的這種人太多了,其中還包括爲數不少的老師,所以至今對大學老師有欺人惑鬼的偏見,真不知道孔老二的那個“知之爲知之,不知爲不知”是怎麼被這些傢伙忘到九霄雲外去的。

 

轉載或改編時,請務必以鏈接形式註明文章 原始出處、作者信息 和 著作聲明:

文章標題:字符集編碼與 C/C++ 源文件字符編譯亂彈

原文作者:Breaker

著作聲明:原創 分類文章,採用 知識共享(CC) 署名-非商業性使用-相同方式共享 (by-nc-sa) 2.5 中國大陸許可協議

文章地址:http://codingdao.com/wp/post/cset-loale-cpp-source-build-talk/

發佈了151 篇原創文章 · 獲贊 3 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章