在不一樣平臺上開發C/C++程序時,爲了不源碼文件亂碼,得采用UTF-8編碼來存儲源碼文件。可是不少編譯器對UTF-8源碼文件兼容性不佳,因而我作了一些測試,分析了最佳保存方案。javascript
爲了測試編譯器對UTF-8源碼文件兼容性,我編寫了這樣的一個測試程序——java
//#if _MSC_VER >= 1600 // VC2010 //#pragma execution_character_set("utf-8") //#endif #include <stdio.h> #include <locale.h> #include <string.h> #include <wchar.h> char* psa = "\u4e00字A"; wchar_t* pdw = L"\u4e00字W"; int main(int argc, char* argv[]) { char* pa; wchar_t* pw; setlocale(LC_ALL, ""); // 使用系統當前代碼頁. // char printf("len<%d>=%d,str=%s\t//", sizeof(char), strlen(psa), psa); for(pa=psa; *pa!=0; ++pa) printf(" %.2X", (unsigned char)*pa); printf("\n"); // wchar_t printf("len<%d>=%d,str=%ls\t//", sizeof(wchar_t), wcslen(pdw), pdw); for(pw=pdw; *pw!=0; ++pw) printf(" %.4X", (unsigned int)*pw); printf("\n"); return 0; }
若是系統默認編碼是GB2312(如中文Windows系統),該程序的輸出結果應是——
len<1>=5,str=一字A // D2 BB D7 D6 41
len<2>=3,str=一字W // 4E00 5B57 0057函數
若是系統默認編碼是UTF-8(如Linux系統),該程序的輸出結果應是——
len<1>=7,str=一字A // E4 B8 80 E5 AD 97 41
len<4>=3,str=一字W // 4E00 5B57 0057測試
注:
1. 「len」旁尖括號內的是字符類型的寬度。char類型通常是1字節。而wchar_t類型跟編譯器與操做系統有關,Windows平臺下通常2字節,Linux平臺下通常4字節。
2. 「len<?>=」右側的數字是字符個數。用char類型,一個漢字的GB2312編碼是2個字符,一個漢字的UTF-8編碼通常是3個字符。而對於wchar_t類型,一個漢字通常是1個字符。
3. 「str=」右側的是所顯示的字符串。
4. 「//」右側用於顯示每個字符的值。ui
須要測試這些方面——
1. 分別測試不一樣操做系統下的多種編譯器。
2. 無簽名的UTF-8與帶簽名的UTF-8。UTF-8存儲方案分別有兩種,一是無簽名的UTF-8,另外一是帶簽名的UTF-8,這兩種方案的區別是——是否存在簽名字符(BOM)。
3. 執行字符集。VC2010增長了「#pragma execution_character_set("utf-8")」,指示char的執行字符集是UTF-8編碼。this
根據上面的要求,制定好了測試項目,分別有Window平臺下的測試與Linux平臺下的測試。
Window平臺下的測試有——
[VC6, noBOM]:VC6.0 sp1,源碼使用無簽名的UTF-8編碼。
[VC6, BOM]:VC6.0 sp1,源碼使用帶簽名的UTF-8編碼。
[VC2003, noBOM]:VC2003 sp1,源碼使用無簽名的UTF-8編碼。
[VC2003, BOM]:VC2003 sp1,源碼使用帶簽名的UTF-8編碼。
[VC2005, noBOM]:VC2005 sp1,源碼使用無簽名的UTF-8編碼。
[VC2005, BOM]:VC2005 sp1,源碼使用帶簽名的UTF-8編碼。
[VC2010, noBOM]:VC2010 sp1,源碼使用無簽名的UTF-8編碼。
[VC2010, BOM]:VC2010 sp1,源碼使用帶簽名的UTF-8編碼。
[VC2010, noBOM, execution_character_set]:VC2010 sp1,源碼使用無簽名的UTF-8編碼,並使用「#pragma execution_character_set("utf-8")」。
[VC2010, BOM, execution_character_set]:VC2010 sp1,源碼使用帶簽名的UTF-8編碼,並使用「#pragma execution_character_set("utf-8")」。
[BCB6, noBOM]:Borland C++ Builder 6.0,源碼使用無簽名的UTF-8編碼。
[BCB6, BOM]:Borland C++ Builder 6.0,源碼使用帶簽名的UTF-8編碼。
[gcc(mingw), noBOM]:MinGW中的GCC 4.6.2,源碼使用無簽名的UTF-8編碼。
[gcc(mingw), BOM]:MinGW中的GCC 4.6.2,源碼使用帶簽名的UTF-8編碼。編碼
Linux平臺下的測試有——
[gcc(fedora), noBOM, chs]:Fedora 17自帶的GCC 4.7.0,源碼使用無簽名的UTF-8編碼,系統語言設爲「簡體中文」。
[gcc(fedora), BOM, chs]:Fedora 17自帶的GCC 4.7.0,源碼使用帶簽名的UTF-8編碼,系統語言設爲「簡體中文」。
[gcc(fedora), noBOM, eng]:Fedora 17自帶的GCC 4.7.0,源碼使用無簽名的UTF-8編碼,系統語言設爲「英語」。
[gcc(fedora), BOM, eng]:Fedora 17自帶的GCC 4.7.0,源碼使用帶簽名的UTF-8編碼,系統語言設爲「英語」。spa
測試結果彙總以下(分號「;」後的是我寫的註釋)——操作系統
[VC6, noBOM] len<1>=9,str=u4e00瀛桝 // 75 34 65 30 30 E5 AD 97 41 ; VC6沒法識別「\u」轉義符,直接輸出了「u4e00」。 len<2>=7,str=u4e00瀛梂 // 0075 0034 0065 0030 0030 701B 6882 [VC6, BOM] 沒法編譯! ; 因BOM字符被編譯器當作了錯誤的語句。 [VC2003, noBOM] len<1>=0,str= // ; 編譯器沒法識別字符串。 len<2>=3,str=一瀛梂 // 4E00 701B 6882 [VC2003, BOM] len<1>=0,str= // len<2>=3,str=一字W // 4E00 5B57 0057 [VC2005, noBOM] len<1>=6,str=一瀛桝 // D2 BB E5 AD 97 41 len<2>=3,str=一瀛梂 // 4E00 701B 6882 [VC2005, BOM] len<1>=5,str=一字A // D2 BB D7 D6 41 len<2>=3,str=一字W // 4E00 5B57 0057 [VC2010, noBOM] len<1>=6,str=一瀛桝 // D2 BB E5 AD 97 41 ; 「字A」的UTF-8編碼爲「E5 AD 97 41」,編譯器將它們識別爲GB2312編碼的「瀛桝」,並將其存儲爲GB2312字符串。 len<2>=3,str=一瀛梂 // 4E00 701B 6882 ; 「字W」的UTF-8編碼爲「E5 AD 97 57」,編譯器將它們識別爲GB2312編碼的「瀛梂」,並將其存儲爲UTF-16字符串。 [VC2010, BOM] len<1>=5,str=一字A // D2 BB D7 D6 41 ; 因帶有BOM,編譯器正確的識別了字符串,並將其存儲爲GB2312字符串。 len<2>=3,str=一字W // 4E00 5B57 0057 ; 因帶有BOM,編譯器正確的識別了字符串,並將其存儲爲UTF-16字符串。 [VC2010, noBOM, execution_character_set] len<1>=8,str=一鐎涙 // D2 BB E7 80 9B E6 A1 9D ; 「\u4e00」被識別爲「一」,並存儲爲GB2312編碼「D2 BB」。「字A」的UTF-8編碼爲「E5 AD 97 41」,編譯器將它們識別爲GB2312編碼的「瀛桝」,並存儲爲UTF-8編碼的「E7 80 9B E6 A1 9D」。但顯示時系統默認是 GB2312 編碼。 len<2>=3,str=一瀛梂 // 4E00 701B 6882 [VC2010, BOM, execution_character_set] len<1>=6,str=一瀛桝 // D2 BB E5 AD 97 41 ; 「\u4e00」被識別爲「一」,並存儲爲GB2312編碼「D2 BB」。「字A」的UTF-8編碼爲「E5 AD 97 41」,編譯器正確的將其存儲爲UTF-8編碼。但顯示時系統默認是 GB2312 編碼。 len<2>=3,str=一字W // 4E00 5B57 0057 [BCB6, noBOM] len<1>=6,str=一瀛桝 // D2 BB E5 AD 97 41 len<2>=3,str=一瀛梂 // 4E00 701B 6882 [BCB6, BOM] 沒法編譯! ; 因BOM字符被編譯器當作了錯誤的語句。 [gcc(mingw), noBOM] len<1>=7,str=涓€瀛桝 // E4 B8 80 E5 AD 97 41 ; 存儲爲UTF-8編碼。但顯示時系統默認是 GB2312 編碼。 len<2>=3,str=一字W // 4E00 5B57 0057 [gcc(mingw), BOM] len<1>=7,str=涓€瀛桝 // E4 B8 80 E5 AD 97 41 len<2>=3,str=一字W // 4E00 5B57 0057 [gcc(fedora), noBOM, chs] len<1>=7,str=一字A // E4 B8 80 E5 AD 97 41 ; 存儲爲UTF-8編碼。顯示時系統默認是 zh_CN.utf8 編碼,正常輸出。 len<4>=3,str=一字W // 4E00 5B57 0057 [gcc(fedora), BOM, chs] len<1>=7,str=一字A // E4 B8 80 E5 AD 97 41 len<4>=3,str=一字W // 4E00 5B57 0057 [gcc(fedora), noBOM, eng] len<1>=7,str=一字A // E4 B8 80 E5 AD 97 41 ; 存儲爲UTF-8編碼。顯示時系統默認是 en_US.utf8 編碼,正常輸出。 len<4>=3,str=一字W // 4E00 5B57 0057 [gcc(fedora), BOM, eng] len<1>=7,str=一字A // E4 B8 80 E5 AD 97 41 len<4>=3,str=一字W // 4E00 5B57 0057
觀察測試結果,咱們首先能夠發現如下幾點——
VC6和BCB6都沒法編譯帶簽名UTF-8編碼的代碼文件,它們會將簽名字符(BOM)當作錯誤的語句。
VC6沒法識別「\u」轉義符。
VC2003沒法識別UTF-8編碼的char。.net
Windows下的測試以VC2010最爲典型,以此爲例來說解。
在編譯過程當中,處理字符串時會涉及下面兩種字符集——
源碼字符集(the source character set):源碼文件是使用何種編碼保存的。
執行字符集(the execution character set):可執行程序內保存的是何種編碼。
要想使程序不會亂碼,必須知足——
1) 編譯器準確識別了源碼字符集,從而獲得正確的字符串數據。
2) 運行環境的編碼與執行字符集相同。運行環境的編碼可經過setlocale函數來配置,「setlocale(LC_ALL, "")」表示使用系統默認編碼。對於簡體中文Windows來講通常是GB2312,若是執行字符集相同,那就能正常顯示,不然會亂碼。
VC2010是這樣處理的——
源碼字符集:若是有簽名字符,就按它的編碼來解析;不然使用本地Locale字符集。
執行字符集:對於char類型,若是有「#pragma execution_character_set」,就按它的編碼來存儲字符串;不然使用本地Locale字符集。對於wchar_t類型,老是使用UTF-16編碼。
當源碼使用帶簽名的UTF-8編碼時,VC2010能正確的識別源碼字符集是UTF-8。而後因沒有「#pragma execution_character_set」,執行字符集是本地Locale字符集——
[VC2010, BOM]
len<1>=5,str=一字A // D2 BB D7 D6 41 ; 因帶有BOM,編譯器正確的識別了字符串,並將其存儲爲GB2312字符串。
len<2>=3,str=一字W // 4E00 5B57 0057 ; 因帶有BOM,編譯器正確的識別了字符串,並將其存儲爲UTF-16字符串。
當源碼使用無簽名的UTF-8編碼時,VS2010因找不到簽名字符,源碼字符集被誤認爲是本地Locale字符集。而後因沒有「#pragma execution_character_set」,執行字符集是本地Locale字符集——
[VC2010, noBOM]
len<1>=6,str=一瀛桝 // D2 BB E5 AD 97 41 ; 「字A」的UTF-8編碼爲「E5 AD 97 41」,編譯器將它們識別爲GB2312編碼的「瀛桝」,並將其存儲爲GB2312字符串。
len<2>=3,str=一瀛梂 // 4E00 701B 6882 ; 「字W」的UTF-8編碼爲「E5 AD 97 57」,編譯器將它們識別爲GB2312編碼的「瀛梂」,並將其存儲爲UTF-16字符串。
當使用「#pragma execution_character_set("utf-8")」配置了執行字符集爲UTF-8後,狀況變得更復雜了。咱們先看看VC2010能正確識別源碼字符集的帶簽名文件——
[VC2010, BOM, execution_character_set]
len<1>=6,str=一瀛桝 // D2 BB E5 AD 97 41 ; 「\u4e00」被識別爲「一」,並存儲爲GB2312編碼「D2 BB」。「字A」的UTF-8編碼爲「E5 AD 97 41」,編譯器正確的將其存儲爲UTF-8編碼。但顯示時系統默認是 GB2312 編碼。
len<2>=3,str=一字W // 4E00 5B57 0057
再看看無簽名時的狀況。VS2010因找不到簽名字符,源碼字符集被誤認爲是本地Locale字符集,即誤將UTF-8識別爲GB2312。而後根據執行字符集,又轉換編碼爲UTF-8進行存儲。最後在運行時因默認編碼是GB2312,再次誤將UTF-8識別爲GB2312——
[VC2010, noBOM, execution_character_set]
len<1>=8,str=一鐎涙 // D2 BB E7 80 9B E6 A1 9D ; 「\u4e00」被識別爲「一」,並存儲爲GB2312編碼「D2 BB」。「字A」的UTF-8編碼爲「E5 AD 97 41」,編譯器將它們識別爲GB2312編碼的「瀛桝」,並存儲爲UTF-8編碼的「E7 80 9B E6 A1 9D」。但顯示時系統默認是 GB2312 編碼。
len<2>=3,str=一瀛梂 // 4E00 701B 6882
從上面這2個例子中,發現VC2010存在一個Bug——「#pragma execution_character_set」對「\u」轉義字符無效,「\u」轉義字符老是使用本地Locale字符集,而不是執行字符集。
GCC的源碼字符集與執行字符集默認是UTF-8編碼,這是由於如今的Linux系統大多使用UTF-8編碼。就算調整了Linux系統語言後,只是區域發生了變化,字符編碼依然是UTF-8。因此咱們的程序在「簡體中文」與「英語」下,均能正確的顯示中文字符。
MinGW中的GCC也是這樣的,源碼字符集與執行字符集默認是UTF-8編碼。可是簡體中文的Windows的默認編碼是GB2312,會將printf輸出UTF-8字符串誤認爲是GB2312,形成亂碼。
若是字符串常量中沒有非ASCII字符,建議源碼文件使用無簽名的UTF-8編碼,這樣能支持早期的編譯器。
若是字符串常量中含有非ASCII字符,建議源碼文件使用帶簽名的UTF-8編碼,這樣能使大多數編譯器正確的處理源碼字符集。
補充——
1. 注意條件僅是「字符串常量中沒有非ASCII字符」。若是是從外部文件或其餘途徑得到非ASCII字符串,只要選擇了合適的字符串函數,無簽名UTF-8編碼的源碼文件也是能行的。
2. VC2010新增的「#pragma execution_character_set」用於明確要求UTF-8字符串的場合。因爲Windows沒有UTF-8的locale,實用性較小,
參考文獻——
《ISO/IEC 9899:1999 (C99)》。ISO/IEC,1999。www.open-std.org/jtc1/sc22/wg14/www/docs/n1124.pdf
《C99標準》。yourtommy。http://blog.csdn.net/yourtommy/article/details/7495033
《QString亂談(2) 》。dbzhang800。http://blog.csdn.net/dbzhang800/article/details/7540905