c实现多语言编程,C/C++编程中多国语言处理
多國語言的存在、不同語言操作系統的存在,使得針對多語言的設計頗費周章,在編碼上所付出的工作量也是可觀的。所謂編碼的問題,歸結起來,就是二進制的編碼以何種編碼格式進行解析的問題。特別是在硬盤文件和內存數據的相互轉化、即讀寫過程中,如果采用了錯誤的編碼格式,就會造成亂碼。JAVA 語言在字符串、編碼等處理方面給了程序員更為直接、方便的接口,習慣使用 JAVA 做編碼的程序員,在使用 C/C++ 進行文本編碼相關的操作時,常會感到困惑。本文的目的在于以常用的 Unicode(UCS-2)、GB2312、UTF8 三種編碼為例,分析不同編碼在實用中的關系,特別是 C/C++ 中,怎樣處理各種編碼的問題。
1. 將內存中編碼 A 的字符串以編碼 B 格式處理成字節流寫入文件
2. 將原本以 A 編碼組成的文件以字節流形式讀入內存、并以編碼 B 解析為字符串。
第一種情況,可能造成數據的變化、失真。
如果使用 JAVA 語言,發生這種錯誤的情況稍少一些,因為在 JAVA 中沒有 wstring 這種概念,在內存中的 String,使用的編碼都是 Unicode,其中的轉換對于程序員來講是透明的。只要使用輸入 / 輸出方法時注意字節流的字符集選擇即可。
例如,編碼為中文 GB2312 的“標準”字符串被讀入內存后轉存為 UTF8 的過程:
圖 1. 文件轉換編碼的 JAVA 處理方式
但 C/C++ 編程,由于通常使用 char、string 類型的時候比較多,特別是進行文件讀寫,基本都是操作 char* 類型的數據。并且也沒有像 JAVA 中 getByte(String charsetname) 這種函數,不能直接根據字符集重新編碼得到字符串的 byte 數組。這時候,我們使用的 string 其實就一般不是 Unicode,而是符合某種編碼表的。這使得我們往往困惑于 string 的編碼問題。假設有 utf8 的字符串“一”(E4 B8 80),而我們錯誤的認為它是符合 gb2312(編碼 A)的,并將其轉換為 utf8(編碼 B),這種轉換結果是破壞性的,錯誤的輸出將永遠無法正確識別。
依然以“標準”為例,這是一個正確的轉換:
圖 2. 文件轉換編碼的 C/C++ 處理方式
第二種情況,則是更常見到的。例如:瀏覽器瀏覽網頁時的發生的亂碼問題;在寫 XML 文件時,指定了 < ?xml version="1.0" encoding="utf-8" ?> 然而文件中卻包含 GB2312 的字符串——這樣經常會導致 XML 文件 bad formatted,而使得解析器出錯。
這種情況下,其實數據都是正確的,只要瀏覽器選擇正確的編碼,將 XML 文件中的 GB2312 轉換為 UTF8 或者修改 encoding,就可以解決問題。
需要注意的是,ASCII 碼的字符,即單字節字符,一般不受編碼變動影響,在所有編碼表中的值是一樣的;需要小心處理的是多字節字符,例如中文語言。
一般的編碼轉換,直接做映射的不太可能,需要比較多的工作量,大多情況下還是選擇 Unicode 作為轉換的中介。
如前文所說,JAVA 的 String 對象是以 Unicode 編碼存在的,所以 JAVA 程序員主要關心的是讀入時判斷字節流的編碼,從而確??梢哉_的轉化為 Unicode 編碼;相比之下,C/C++ 將外部文件讀出的數據存為字符數組、或者是 string 類型;而 wstring 才是符合 Unicode 編碼的雙字節數組。一般常用的方法是 C 標準庫的 wcstombs、mbstowcs 函數,和 windows API 的 MultiByteToWideChar 與 WideCharToMultiByte 函數來完成向 Unicode 的轉入和轉出。
這里以 MBs2WCs 函數的實現說明 GB2312 向 Unicode 的轉換的主要過程:
清單 1. 多字節字符串向寬字節字符串轉換
wchar_t * MBs2WCs(const char* pszSrc){
wchar_t* pwcs = NULL;
intsize = 0;
#ifdefined(_linux_)
setlocale(LC_ALL, "zh_CN.GB2312");
size = mbstowcs(NULL,pszSrc,0);
pwcs = new wchar_t[size+1];
size = mbstowcs(pwcs, pszSrc, size+1);
pwcs[size] = 0;
#else
size = MultiByteToWideChar(20936, 0, pszSrc, -1, 0, 0);
if(size <= 0)
returnNULL;
pwcs = new wchar_t[size];
MultiByteToWideChar(20936, 0, pszSrc, -1, pwcs, size);
#endif
returnpwcs;
}
相應的,WCs2MBs 可以將寬字符串轉化為字節流。
清單 2. 寬字節字符串向多字節字符串轉換
char* WCs2MBs(const wchar_t * wcharStr){
char* str = NULL;
intsize = 0;
#ifdefined(_linux_)
setlocale(LC_ALL, "zh_CN.UTF8");
size = wcstombs( NULL, wcharStr, 0);
str = new char[size + 1];
wcstombs( str, wcharStr, size);
str[size] = '\0';
#else
size = WideCharToMultiByte( CP_UTF8, 0, wcharStr, -1, NULL, NULL, NULL, NULL );
str = new char[size];
WideCharToMultiByte( CP_UTF8, 0, wcharStr, -1, str, size, NULL, NULL );
#endif
returnstr;
}
Linux 的 setlocale 的具體使用可以參閱有 C/C++ 文檔,它關系到文字、貨幣單位、時間等很多格式問題。Windows 相關的代碼中 20936 和宏定義 CP_UTF8 是 GB2312 編碼對應的的 Code Page[ 類似的 Code Page 參數可以從 MSDN的 Encoding Class 有關信息中獲得 ]。
這里需要特別指出的是 setlocale 的第二個參數,Linux 和 Windows 是不同的:
1. 筆者在 Eclipse CDT + MinGW 下使用 [country].[charset](如 zh_CN.gb2312 或 zh_CN.UTF8)的格式并不能通過編碼轉換測試,但可以使用 Code Page,即可以寫成 setlocale(LC_ALL, ".20936") 這樣的代碼。這說明,這個參數與編譯器無關,而與系統定義有關,而不同操作系統對于已安裝字符集的定義是不同的。
2. Linux 系統下可以參見 /usr/lib/locale/ 路徑,系統所支持的 locale 都在這里。轉換成 UTF8 時,并不需要 [country] 部分一定是 zh_CN,en_US.UTF8 也可以正常轉換。
另外,標準 C 和 Win32 API 函數返回值是不同的,標準 C 返回的 wchar_t 數組或者是 char 數組都沒有字符串結束符,需要手動賦值,所以 Linux 部分的代碼要有區別對待。
最后,還要注意應當在調用這兩個函數后釋放分配的空間。如果將 MBs2WCs 和 WCs2MBs 的返回值分別轉化為 wstring 和 string,就可以在它們函數體內做 delete,這里為了代碼簡明,故而省略,但請讀者別忘記。
目前的第三方工具已經比較完善,這里介紹兩個,本文側重點不在此,不對其做太多探討。
Linux 上存在第三方的 iconv 項目,使用也較為簡單,其實質也是以 Unicode 作為轉換的中介。可以參閱 iconv 相關網站。
ICU 是一個很完善的國際化工具。其中的 Code Page Conversion 功能也可以支持文本數據從任何字符集向 Unicode 的雙向轉換。可以訪問其 網站
在代碼中調用“編碼轉換方法”一節里提到的函數,將 gb2312 編碼的字符串轉換為 UTF8 編碼,分析其編碼轉換的行為:
在英文 Linux 環境下,執行下列命令:
export LC_ALL=zh_CN.gb2312
然后編譯并執行以下程序(其中漢字都是在 gb2312 環境中寫入源文件)
L1: wstring ws = L"一";
L2: string s_gb2312 = "一";
L3: wchar_t * wcs = MBs2WChar(s_gb2312.c_str());
L4: char* cs = WChar2MBs(wcs);
查看輸出:
L1 - 1 wide char: 0x04bb
L2 - 2 bytes:0xd2,0xbb,即 gb2312 編碼 0xD2BB
L3 - 返回的 wchar_t 數組內容為 0x4E00,也就是 Unicode 編碼
L4 - 將 Unicode 再度轉換為 UTF8 編碼,輸出的字符長度為 3,即 0xE4,oxB8,0x80
在 L1 行,執行結果顯示編碼為一個 0x04bb,其實這是一個轉換錯誤,如果使用其他漢字,如“哈”,編譯都將無法通過。也就是說 Linux 環境下,直接聲明中文寬字符串是不正確的,編譯器不能夠正確轉換。
而在中文 windows 下使用相同測試代碼,則會在 L1 處出現區別,ws 中的 wchar_t 元素十六進制值是 0x4e00,這是漢字“一”的 Unicode 編碼。
首先,這里先簡單說明一下 Unicode 和 UTF8 的關系:Unicode 的實現方式和它的編碼方式并不相同,UTF8 就是其實現之一。比方使用 UltraEdit 打開 UTF8 編碼的中文文件,使用 16 進制查看,可以發現看到的中文對應部分應當是 Unicode 編碼,每個中文字長度 2 字節—— UltraEdit 在這里已經做了轉化;如果直接查看其二進制文件,可以發現是 3 字節。但兩者的差別僅在于 Unicode 向 UTF8 做了數學上的轉化。(更多關于 Unicode 和 UTF8 的概念,可以參見 有關文獻)
其次,關于第三方庫的選擇,應當綜合考慮項目的需求。一般的文本字符轉換,系統的庫函數已經可以滿足需求,實現也很簡單;如果需要針對不同地區的語言、文字、習慣進行編程,需要更為豐富的功能,當然選擇成熟的第三方工具可以事半功倍。
最后,從邏輯上保持字符串的編碼正確,需要注意幾條一般規律:
編碼選擇:多國語言環境的編程,以使用 UTF 編碼為原則,減少字符集轉換。
string 并不包含編碼信息,但是編碼確定了 string 的二進制內容。
讀寫一致:讀入時使用的字符集要與寫出時使用的一致。如果不需要改變字符串內容,僅僅是將字符串讀入、再寫出,建議不要調整任何字符集——即使程序使用的系統默認字符集 A 與文件的實際編碼 B 不符合,寫出的字符串依然會是正確的 B 編碼。
讀入已知:對于必須處理、解析或顯示的字符串,從文件讀入時必須知道它的編碼,避免處理字符串的代碼簡單使用系統默認字符集;即便對于程序從系統中收集到的內存字符串,也應知道其符合的編碼格式——一般為系統默認字符集。
避免直接使用 Unicode:這里是說將非 ASCII 編碼的 16 進制或者 10 進制數值用 與 ; 包含起來的使用方式,例如將中文“一”寫成“e00;”。這種方法的實質是 Unicode 編碼直接寫入文件。這不僅會降低代碼的通用性、輸出文件的可讀性,處理起來也很困難。比如法文字符在其他字符集中是大于 80H 的單字節字符,程序同時要支持中文的時候,很有可能會將多字節的中文字符錯誤割裂。
避免陷入直接的字符集編程:國際化、本地化的工具已經比較成熟,非純粹做編碼轉換的程序員沒有必要自己去處理不同編碼表的映射轉換問題。
Unicode/UTF8 并不能解決一切亂碼問題:Unicode 可以說是將世界語言統一起來的一套編碼。但是這并不意味著在一個系統中可以正常顯示的按照 UTF8 編碼的文件,在另一個系統中也可以正常顯示。例如,在中文的 UTF8 編碼或者 Unicode 編碼在沒有東亞語言包支持的法文系統中,依然是不可識別的亂碼——盡管 UTF8、Unicode 它們都支持。
學習從 Nicolar M. Josuttis, C++ Standard Library, The: A Tutorial and Reference 這本書,或者在線鏈接 c++ reference可以獲得 C++ 標準庫有關信息
wikipedia 上有
Unicode 相關的概念解說
在 developerWorks 上查閱所有 Linux 技巧 和 Linux 教程。
獲得產品和技術下載
IBM 軟件試用版,體驗強大的 DB2?,Lotus?,Rational?,Tivoli?和
WebSphere?軟件。
討論
現供職于 IBM 上海 CSTL,Tools Center 項目組,主要進行 C/C++,XML,服務器工具方面的程序開發。有 6 年 JAVA 和 C/C++ 開發經驗,擁有上海交大計算機碩士學位。
總結
以上是生活随笔為你收集整理的c实现多语言编程,C/C++编程中多国语言处理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux基础分支,Linux基础--/
- 下一篇: 华南理工大学 高级程序设计语言 c++