我們傳統(tǒng)的程序基本都只在Windows或只在Linux下運(yùn)行,Windows程序使用簡體中文GB18030編碼,Linux程序則只使用英文,多年以來這些程序運(yùn)行起來都沒有問題。
近年來,隨著程序的組件化,部分代碼特別是公用組件都需要同時(shí)支持Windows及Linux平臺,這樣就出現(xiàn)了不同程度的編碼問題,例如在編譯時(shí)編譯器報(bào)錯(cuò),或者在運(yùn)行時(shí)出現(xiàn)亂碼。這些問題都和程序選用的字符編碼不正確有關(guān)。
本文簡要地分析了C++的一些字符編碼問題,并提供了建議的方案。受經(jīng)驗(yàn)和時(shí)間的限制,有些內(nèi)容可能不一定全面,僅供大家參考。
首先要區(qū)分幾個(gè)概念:
指的是C++源程序文件(.cpp/.h)本身使用什么字符編碼(GB18030/UTF-8等)。
編譯后,C++中的字符串常量都會變成一串字節(jié)存放在可執(zhí)行文件中。這個(gè)內(nèi)碼指的就是在可執(zhí)行文件中,字符串以什么編碼進(jìn)行存放。這里的字符串常量指的是窄字符(char)而非寬字符(wchar_t)。寬字符通常是以Unicode(VC使用UTF-16BE,gcc使用UTF-32BE)存放。
指的是執(zhí)行程序時(shí),操作系統(tǒng)或終端所使用的編碼。程序中輸出的字符最終要轉(zhuǎn)換為運(yùn)行環(huán)境編碼才能正確顯示,否則就會出現(xiàn)亂碼。
通常在簡體中文Windows環(huán)境下,各種編輯器(包括Visual Studio)新建文件的缺省編碼都是GB18030,所以不特別指定的話,Windows環(huán)境下C++源文件的編碼通常為GB18030。
而在Linux環(huán)境下,最常使用,也是推薦使用的是UTF-8編碼。
一般來說,我們常用的簡體中文版VC所使用的內(nèi)碼是GB18030,而gcc/g++使用的內(nèi)碼缺省是utf-8,但可以通過-fexec-charset參數(shù)進(jìn)行修改。
我們常用的簡體中文版Windows的環(huán)境編碼是GB18030,而Linux下最常用的環(huán)境編碼是UTF-8。
源程序需要由編譯器編譯為目標(biāo)文件,目標(biāo)文件運(yùn)行后輸出信息到終端,因此這幾個(gè)編碼之間存在一些的關(guān)聯(lián):
編譯器需要正確識別源文件的編碼,把源文件編譯為目標(biāo)文件,并把源文件中的以源文件編碼的字符串轉(zhuǎn)換為以程序內(nèi)碼編制的字符串保存在目標(biāo)文件中。
C++標(biāo)準(zhǔn)庫需要正確識別終端的運(yùn)行環(huán)境編碼,并把程序的輸出轉(zhuǎn)換為運(yùn)行環(huán)境所使用的編碼,以便正確顯示。
在這過程中,如果有一個(gè)環(huán)節(jié)出現(xiàn)問題,就會導(dǎo)致程序的輸出發(fā)生異常,產(chǎn)生亂碼或其它更嚴(yán)重的后果。
根據(jù) http://stackoverflow.com/questions/688760/how-to-create-a-utf-8-string-literal-in-visual-c-2008一文中提供的資料,gcc/vc各版本對C++源文件編碼有不同的處理:
gcc (v4.3.2 20081105):
支持UTF-8編碼的源文件,UTF-8編碼的源文件不能有BOM。
根據(jù) http://gcc.gnu.org/bugzilla/show_bug.cgi?id=33415 ,似乎gcc 4.4.0開始支持帶BOM的UTF-8文件。
vc2003:
支持UTF-8編碼的源文件,UTF-8編碼的源文件可以有BOM,也可以沒有。
vc2005+:
如果源文件使用UTF-8編碼的話,必須有BOM。
很多文章都推薦C/C++代碼中只使用ascii字符,如果有非ascii字符可以用\xHH或\uXXXX表示。注釋中建議使用utf-8編碼。也可以使用gettext 把非ascii字符串放到單獨(dú)的語言文件中,而在源代碼中只保留ascii字符。
在實(shí)踐中,由于\xHH或\uXXXX等方式很不直觀,容易出錯(cuò)且不易發(fā)現(xiàn),而未必所有程序都需要支持多語言,因此未必想引入gettext或類似的解決方案。在這樣的情況下,大家都習(xí)慣在源程序文件中直接寫入中文等非ascii字符,這就需要選擇一種至少能被gcc和vc接受的文件編碼。
本來,Unicode是解決多語言問題的最好選擇,而UTF-8由于與ASCII兼容,也是最通用的Unicode編碼方式,但從上面的資料中可見,如果用UTF-8的話,gcc(至少是低版本)不允許有BOM,而vc2005 以上要求必須有BOM,因此同一個(gè)文件無法在gcc及vc下通過編譯,UTF-8似乎不是一個(gè)好的選擇。但如果使用gcc比較高的版本(4.4.0以上?),使用帶BOM的UTF-8編碼文件應(yīng)該也是可行的。
考慮到目前現(xiàn)狀,我們一般都在簡體中文Windows下工作,源文件中使用GB18030編碼似乎是一個(gè)比較現(xiàn)實(shí)的選擇。在vc下可以直接編譯,而在gcc下也可以通過增加編譯選項(xiàng)-finput-charset=gb18030予以支持。而且根據(jù)維基百科中GB18030的詞條內(nèi)容,GB18030 is a superset of ASCII and can represent the wholerange of Unicode code points(GB18030向后兼容ASCII,并且能表示所有的Unicode碼點(diǎn)),因此使用GB18030有足夠的表達(dá)能力,可以表示所有的Unicode字符。使用GB18030的唯一缺點(diǎn)就是在非簡體中文版本的VC下,由于無法指定源文件的編碼,因此有可能無法正確識別此編碼的源文件。
正如前面提到的,C++有窄字符(char)和寬字符(wchar_t)的分別,分別有一套相應(yīng)的類和函數(shù)(string/cout/strlen與wstring/wcout/wcslen等)。前者在不同的編譯器下有不同的缺省編碼(簡體中文vc是GB18030,gcc是UTF-8),后者一般都使用Unicode,其中vc下使用UTF-16,gcc缺省使用UTF-32。
C++在輸出窄字符時(shí)會按程序內(nèi)碼原樣輸出,不會進(jìn)行編碼轉(zhuǎn)換,因此在使用窄字符時(shí)要求程序內(nèi)碼與運(yùn)行環(huán)境編碼一致,這樣才不會出現(xiàn)亂碼。由于簡體中文版vc的程序內(nèi)碼是GB18030,因此使用窄字符的vc程序只能運(yùn)行在GB18030環(huán)境下。同樣,由于gcc缺省使用UTF-8作為程序內(nèi)碼,因此使用窄字符的gcc程序只能運(yùn)行在UTF-8的終端環(huán)境下。(這里說的都是在源代碼中直接寫中文等非ascii字符的程序。用前面提到的gettext及其它工具,使用窄字符的程序也可以在不同編碼的運(yùn)行環(huán)境中正確輸出中文)
C++在輸出寬字符時(shí)會自動轉(zhuǎn)換為運(yùn)行環(huán)境的編碼,因此只要正確設(shè)置了運(yùn)行環(huán)境編碼,同一個(gè)程序就可以在不同編碼的運(yùn)行環(huán)境中正確顯示中文。這一點(diǎn)與Java/.Net很象,Java/.Net的字符串類型都使用Unicode,在輸入/輸出時(shí)都需要與當(dāng)前運(yùn)行環(huán)境的編碼進(jìn)行互轉(zhuǎn)。
一般來說,如果需要支持多語言,有兩種比較好的做法:
使用窄字符,但源程序中只使用ascii字符,非ascii字符通過gettext或其它工具放到單獨(dú)的文件中,由gettext等工具處理編碼轉(zhuǎn)換的問題。
在各種編碼的運(yùn)行環(huán)境中均能正確輸出中文。
程序中不能直接出現(xiàn)非ascii字符,也不能通過\uXXXX方式指定非ascii字符,后者也會被編譯器轉(zhuǎn)換為非ascii字符并存放在目標(biāo)文件中。
注釋中可以使用ascii兼容的編碼,不影響編譯器。
有比較多的現(xiàn)成代碼可供重用。
使用寬字符。
程序中可以使用非ascii字符。
需要配合前面的源程序文件編碼設(shè)置,讓編譯器能正確識別源程序中的非ascii字符。
由于以前使用寬字符的程序比較少,可供重用的代碼較少。
正如上面提到的,使用窄字符和使用寬字符的程序?qū)\(yùn)行環(huán)境的字符編碼要求是不一樣的。
使用寬字符,只要在程序中正確設(shè)置當(dāng)前環(huán)境的字符編碼(一般通過locale::global(locale("")) 進(jìn)行設(shè)置),C++標(biāo)準(zhǔn)庫會在輸入、輸出時(shí)正確進(jìn)行字符編碼轉(zhuǎn)換,因此可以適應(yīng)各種編碼的運(yùn)行環(huán)境。
使用窄字符,但程序中不出現(xiàn)非ascii字符的話,對運(yùn)行環(huán)境沒有特別要求,可以適應(yīng)各種編碼的運(yùn)行環(huán)境。
使用窄字符,程序中也直接使用漢字等非ascii字符的話,由于C++標(biāo)準(zhǔn)庫會把目標(biāo)文件中保存的字符串(以程序內(nèi)碼保存)直接輸出,不會進(jìn)行字符編碼轉(zhuǎn)換,因此要求運(yùn)行環(huán)境的編碼與程序內(nèi)碼一致。即簡體中文VC編譯的程序只能運(yùn)行在GB18030環(huán)境下,gcc編譯的程序只能運(yùn)行在UTF-8環(huán)境下(可以在編譯時(shí)通過-fexec-charset參數(shù)進(jìn)行修改)。
根據(jù)上面的討論,目前看來,要兼容Windows/Linux,VC/gcc的話,有幾種做法:
使用窄字符,源程序中只使用ascii字符,非ascii字符,如中文等通過gettext等工具放到單獨(dú)的語言包中。
這種做法比較多人推薦。
兼容VC及gcc各版本。
由于源程序中不出現(xiàn)非ascii字符,因此不需要考慮源程序文件的編碼問題。
兼容各種編碼的運(yùn)行環(huán)境。
使用窄字符,源程序中允許使用非ascii字符。
要求運(yùn)行環(huán)境的編碼與程序內(nèi)碼一致,即只支持GB18030編碼的Windows及UTF-8編碼的Linux。
根據(jù)源程序使用的編碼不同,對編譯器的兼容性也不同:
使用窄字符,源程序使用帶BOM的UTF-8編碼。
兼容VC各語種的各版本。
兼容gcc 4.4.0以上版本。
使用窄字符,源程序使用GB18030編碼。
兼容VC的簡體中文各版本。
兼容gcc各版本,但在編譯時(shí)需要加上-finput-char=gb18030參數(shù)。
使用寬字符,源程序中允許使用非ascii字符。
根據(jù)我們的現(xiàn)狀,對于需要支持多語種的程序,建議使用窄字符,源程序中只使用ascii字符。
對于不需要支持多語種的程序,考慮到重用已有的代碼,可以考慮使用窄字符,采用GB18030編碼,但只能運(yùn)行在GB18030編碼的Windows環(huán)境及UTF-8編碼的Linux環(huán)境下。
由于用戶輸入、輸出及從文件、網(wǎng)絡(luò)等設(shè)施讀寫的數(shù)據(jù)在程序底層看來都是字節(jié)流,因此存在在輸入時(shí)如何把這些字節(jié)流解釋成有效的信息,在輸出時(shí)怎么把程序中的信息轉(zhuǎn)換為正確的字節(jié)流的問題。
如果程序本身不需要處理這些數(shù)據(jù),只是把數(shù)據(jù)從一個(gè)來源搬到另一個(gè)地方(如把用戶輸入保存到文件,或者從一個(gè)流讀入,寫到另一個(gè)流等),而輸入的字符編碼與輸出的字符編碼一致的話,程序不需要對數(shù)據(jù)進(jìn)行任何編碼轉(zhuǎn)換,只需要把讀入的數(shù)據(jù)按原樣寫到輸出即可,數(shù)據(jù)的字符編碼與程序的編碼沒有關(guān)系。
比如網(wǎng)站應(yīng)用程序,只需要保證用戶頁面使用UTF-8編碼,數(shù)據(jù)庫、數(shù)據(jù)文件也都使用UTF-8編碼,那么用戶輸入的數(shù)據(jù)可以直接寫入數(shù)據(jù)庫及數(shù)據(jù)文件,從數(shù)據(jù)庫或數(shù)據(jù)文件中讀取的數(shù)據(jù)也可以直接展現(xiàn)給用戶,不需要進(jìn)行編碼轉(zhuǎn)換。
如果程序需要在一定程序上對數(shù)據(jù)進(jìn)行處理(如需要判斷字符個(gè)數(shù)、對字符進(jìn)行比較、在字符串上附加或去掉內(nèi)容),就要把數(shù)據(jù)轉(zhuǎn)換為一種明確的字符編碼,一般來說是程序內(nèi)碼,再進(jìn)行處理,在處理后再轉(zhuǎn)換為所需的字符編碼進(jìn)行輸出。
對于寬字符程序,如果只需要處理采用當(dāng)前運(yùn)行環(huán)境字符編碼的數(shù)據(jù),可以通過ios::imbue()可以指定io流的字符編碼,在輸入、輸出時(shí)C++標(biāo)準(zhǔn)庫會自動在所指定的字符編碼與程序內(nèi)碼之間進(jìn)行編碼轉(zhuǎn)換。如果不使用流的話,也可以通過標(biāo)準(zhǔn)的wcstombs()或mbstowcs()函數(shù)進(jìn)行當(dāng)前編碼(通過locale::global()或setlocale()指定)與寬字符之間的轉(zhuǎn)換。
對于窄字符程序,如果數(shù)據(jù)的字符編碼與程序內(nèi)碼一致也不需要進(jìn)行編碼轉(zhuǎn)換,直接處理即可。
對于其它情形,需要引入iconv或類似的字符編碼轉(zhuǎn)換庫,以便實(shí)現(xiàn)不同字符編碼之間的轉(zhuǎn)換。
由于gettext及iconv都屬于GNU Project,考慮到版權(quán)因素,并非所有程序,特別是商業(yè)程序,都適合使用這些庫。在Boost 1.48.0中,Boost.Locale庫首次正式發(fā)布,該庫提供了gettext、iconv的功能,并在此基礎(chǔ)上進(jìn)行了增強(qiáng),提供了大小寫變換、字符順序比較、時(shí)間的處理 、分詞、數(shù)字的格式化輸入/輸出、消息格式化、多語種支持、字符編碼轉(zhuǎn)換等功能,值得進(jìn)一步研究及使用。
Powered by: C++博客 Copyright © 金慶