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