文章目錄
背景
開發過Windows Mobile和Wince(Windows Embedded CE)的開發人員,特別是Native C++開發人員,或多或少都遇到過ANSI字元集和Unicode字元集的轉換問題。本文試圖把Windows Mobile和Wince(Windows Embedded CE)開發的字元集問題講明白,其實這個題目有點ambitious和aggressive,就當成標題黨吧。
簡介
本文試圖通過一篇文章講清楚Windows Mobile和Wince(Windows Embedded CE) Native C++開發中字元集的轉換問題。從字元集的概念入手,講述Wince支援的所有字串類型,以及各種類型的轉換方法,最後給出使用建議。
什麼是字元集
字元集(Character Set)是映射關係,其定義了字元和編碼的關係。這裡編碼通常來說是1和0的bit。當前流行的電腦系統任何資料存放區最終都表達為1和0。而這些1和0在不同字元集下映射成不同含義的字元。
電腦發明和使用初期,存放裝置都十分的昂貴,科學家們想盡辦法來節省成本,因此開始的是,最常見的字元集是單一位元組字元集(Signle-byte),所謂單一位元組字元集就是使用一個byte來代表一個字元,單字元集的典型是ASCII (American Standard Code for Information Interchange),你看這不是國籍標準,僅僅是美國標準,壓根就沒有考慮咱們感受,咱們從甲骨文開始發展漢字,美國人看到就A~Z幾個字母。
這個ASCII 表,學過C語言的人都學過,以前考試也用到,需要背下來的。
ANSI(American National Standards Institute) 是在ASCII 7bit 編碼通訊協定 (ASA X3.4-1963)的基礎上,加入了歐洲字母發展出來的一個標準。
但是單一位元組字元集有個最要命的缺點是一個byte只要8個bit,也就是最多表示256 (28)個可見和不可見的字元。 對於英語國家可能是夠用的,但是對於說中文的國家,漢字是沒辦法通過256個字元表達的。因此慢慢出來國際標準的字元集Unicode。
Wince與Unicode
對於剛剛接觸Windows Mobile和Wince(Windows Embedded CE) Native C++開發的人來說,會有這樣的想法:Windows Mobile和Wince僅僅支援Unicode,不支援ANSI的。TinyXML是使用ANSI string的,但是Wince使用Unicode,那麼TinyXML不能使用在Wince和Windows Mobile中。等等…… 其實這些想法有些錯誤,Wince是一個Unicode系統,沒錯,這表示Wince裡面所有字串處理代碼都是基於Unicode編碼,但是不表示Wince不支援ANSI。我們同樣可以繼續在Wince中使用ANSI,例如使用std::string, char[]等。
但是為什麼會有這些誤區呢,先看一下下面的編譯錯誤。
error C2664: 'wprintf' : cannot convert parameter 1 from 'const char [21]' to 'const wchar_t *'
error C2664: 'DeleteFileW' : cannot convert parameter 1 from 'const char [21]' to 'LPCWSTR'
我敢保證剛剛接觸Windows Mobile和Wince(Windows Embedded CE) Native C++開發的人10個有9個甚至10個都碰到過上述問題。在調用Win32 API的時候,使用MFC,WTL的介面的時候都會碰到這樣的問題,因為我們習慣使用char*,std::string,但是恰恰Win32 API,MFC和WTL的函數入口中的字串為Unicode,因此發生上述的編譯錯誤。不知道為什麼大家碰到這個錯誤後會形成一個錯誤的想法:Wince只是支援Unicode,不支援ANSI了。其實Wince還是支援ANSI的,我們定義單字元的char數組,甚至可以通過C Runtime在Console中列印出ANSI string。
char ansiStr[] = "I am ANSI string";
printf(ansiStr);
Wince支援的字串
既然Wince支援ANSI和Unicode,那什麼時候用ANSI,什麼時候用Unicode,下面我從在Wince開發中常常用到字串講起,然後講述字串的轉換以及使用建議。
char*
char*和char[]沒有本質的區別,都是指向記憶體的指標。ANSI版本的Win32的API中的字串都是使用char*。 由於Win32的API是語言無關的,因此這些參數其實傳遞的是一段應該存放字串的記憶體的指標。(很拗口,但是確實這樣,呵呵)。在ANSI環境下使用純粹C開發,程式是離不開char*的。
wchar_t *
LPWSTR, PWSTR等宏定義其實都是wchar_t*的定義, 最常見的LPCWSTR是const wchar_t*的宏定義。wchar_t表示16位Unicode字元。wchar_t*和wchar_t[]用來定義Unicode字串。在Unicode版本下,所有Win32的API的字串都由char*變成了wchar_t*了。
這個可以看一下標頭檔的先行編譯。例如以winbase.h的DeleteFile API為例。
WINBASEAPI
BOOL
WINAPI
DeleteFileA(
LPCSTR lpFileName
);
WINBASEAPI
BOOL
WINAPI
DeleteFileW(
LPCWSTR lpFileName
);
#ifdef UNICODE
#define DeleteFile DeleteFileW
#else
#define DeleteFile DeleteFileA
#endif // !UNICODE
在ANSI版本DeleteFile為DeleteFileA,參數的字串定義為LPCSTR,也就是const char*,而Unicode版本的DeleteFile為DeleteFileW,參數字串定義變成了LPCWSTR,也就是const wchar_t*。
決定DeleteFile到底是DeleteFileA或者DeleteFileW是由先行編譯宏 UNICODE 來決定的。
這個宏可以在項目屬性裡面配置,如:
當選擇Use Unicode Character Set時候,先行編譯會增加宏UNICODE和_UNICODE。
但是需要注意的是,如果目標平台為Windows Mobile或者Wince,不管是否選擇Use Unicode Character Set,UNICODE和_UNICODE的先行編譯都會加上,也就是說Wince下所有Win32 API都是Unicode版本的。
CString
CString最初在MFC裡面封裝,ATL和WTL分別封裝了CString,這三個不同封裝的CString具有語義相容性,也就是他們提供的介面是相同的。使用CString的好處是可以同時相容ANSI和Unicode,如下例子:
CString str = "Independent String";
m_wndPic.SetWindowText(str);
m_wndPic是一個CStatic控制項,上面的代碼不管在ANSI或者在Unicode都能使用,無需更改。
下面以ATL CString為例子講述CString是如何同時支援支援ANSI或者Unicode的。
typedef CAtlString CString;
typedef CStringT< TCHAR, StrTraitATL< TCHAR > > CAtlString;
CString儲存字串的類型由TCHAR來決定的,而TCHAR又是由UNICODE先行編譯來決定,見下面的宏定義。
#ifdef UNICODE // r_winnt
typedef WCHAR TCHAR, *PTCHAR;
#else /* UNICODE */ // r_winnt
typedef char TCHAR, *PTCHAR;
#endif /* UNICODE */
以此CString使用字串類型根據先行編譯選項來自動決定。
std::string
STL裡面的string,封裝的是單位元組字元,由於其跨平台的特性,我編寫的代碼中大量使用std::string,其實準確來說我大量使用STL。例如我一般使用std::string來操作TinyXML。拋開Wince平台不說,使用std::string基本上沒有缺點,可以跨任何支援標準C++的平台。可是在Wince和Windows Mobile下做開發,情況有點不一樣,因為std::string封裝的是單位元組字元,所以如果需要調用Win32的API,使用MFC,ATL和WTL的功能時都需要轉型,這姑且算是一個缺點吧,但是熟悉了轉換以後,使用std::string一點問題都沒有。
std::wstring
STL裡面的string的Unicode版本,和std::string一樣,使用了unicode字元進行封裝。其實std::wstring我用的不多,用std::string已經夠了。
如何轉換Wince支援的字串
既然Windows Mobile和Wince(Windows Embedded CE)支援上述的字串,那麼我們開發的時候會碰到這些字串直接相互轉換的問題,下面通過例子示範如何轉換。
轉換過程我推薦使用ATL的宏,關於ATL的宏可以參考 ATL and MFC String Conversion Macros
這些宏的命名規範為
CSourceType2[C]DestinationType[EX]
SourceType/DestinationType |
Description |
A |
ANSI character string. |
W |
Unicode character string. |
T |
Generic character string (equivalent to W when _UNICODE is defined, equivalent to A otherwise). |
OLE |
OLE character string (equivalent to W). |
A表示ANSI string,W表示Unicode string,T表示通用string,根據先行編譯來決定類型。OLE和W一樣,我從來不用OLE。
例如CT2CA就是通用string轉成ANSI string。
轉換到char*
void ConvertToCharArray()
{
char ansiStr[255] = "ANSI string";
wchar_t unicodeStr[255] = _T("Unicode string"); //use _T() convert const string to wchar_t string
CString cstr("ATL CString");
std::string stlStr("STL string");
std::wstring stlWStr(_T("STL wstring")); //use _T() convert const string to wchar_t string
printf("All string convert to char*\n");
strcpy(ansiStr, CT2CA(unicodeStr));
printf("Convert from wchar_t*, %s\n", ansiStr);
strcpy(ansiStr, CT2CA(cstr));
printf("Convert from CString, %s\n", ansiStr);
strcpy(ansiStr, stlStr.c_str());
printf("Convert from std::string, %s\n", ansiStr);
strcpy(ansiStr, CT2CA(stlWStr.c_str()));
printf("Convert from std::wstring, %s\n", ansiStr);
}
例子中用到了ATL CString,如果建立的是Win32項目需要加入ATL支援,方法可以參考: 在Windows Mobile和Wince(Windows Embedded CE)下Win32項目加入ATL支援
上面講過ATL CString和WTL以及MFC CString語義相同,因此本文所有CString的代碼在MFC下同樣有效。
轉換到wchar_t*
void ConvertToWCharArray()
{
char ansiStr[255] = "ANSI string";
wchar_t unicodeStr[255] = _T("Unicode string"); //use _T() convert const string to wchar_t string
CString cstr("ATL CString");
std::string stlStr("STL string");
std::wstring stlWStr(_T("STL wstring")); //use _T() convert const string to wchar_t string
printf("All string convert to wchar_t*\n");
wcscpy(unicodeStr, CComBSTR(ansiStr));
wprintf(_T("Convert from char*, %s\n"), unicodeStr);
wcscpy(unicodeStr, cstr);
wprintf(_T("Convert from CString, %s\n"), unicodeStr);
wcscpy(unicodeStr, CComBSTR(stlStr.c_str()));
wprintf(_T("Convert from std::string, %s\n"), unicodeStr);
wcscpy(unicodeStr, stlWStr.c_str());
wprintf(_T("Convert from std::wstring, %s\n"), unicodeStr);
}
這裡使用了微軟推薦的CComBSTR(),而不是CA2W()。
轉換到CString
void ConvertToCString()
{
char ansiStr[255] = "ANSI string";
wchar_t unicodeStr[255] = _T("Unicode string"); //use _T() convert const string to wchar_t string
CString cstr("ATL CString");
std::string stlStr("STL string");
std::wstring stlWStr(_T("STL wstring")); //use _T() convert const string to wchar_t string
printf("All string convert to CString\n");
cstr = ansiStr;
wprintf(_T("Convert from char*, %s\n"), cstr);
cstr = unicodeStr;
wprintf(_T("Convert from wchar_t*, %s\n"), cstr);
cstr = stlStr.c_str();
wprintf(_T("Convert from std::string, %s\n"), cstr);
cstr = stlWStr.c_str();
wprintf(_T("Convert from std::wstring, %s\n"), cstr);
}
轉換到std::string
void ConvertToStlString()
{
char ansiStr[255] = "ANSI string";
wchar_t unicodeStr[255] = _T("Unicode string"); //use _T() convert const string to wchar_t string
CString cstr("ATL CString");
std::string stlStr("STL string");
std::wstring stlWStr(_T("STL wstring")); //use _T() convert const string to wchar_t string
printf("All string convert to STL string\n");
stlStr = ansiStr;
printf("Convert from char*, %s\n", stlStr.c_str());
stlStr = CT2CA(unicodeStr);
printf("Convert from wchar_t*, %s\n", stlStr.c_str());
stlStr = CT2CA(cstr);
printf("Convert from CString, %s\n", stlStr.c_str());
stlStr = CT2CA(stlWStr.c_str());
printf("Convert from std::wstring, %s\n", stlStr.c_str());
}
轉換到std::wstring
void ConvertToStlWstring()
{
char ansiStr[255] = "ANSI string";
wchar_t unicodeStr[255] = _T("Unicode string"); //use _T() convert const string to wchar_t string
CString cstr("ATL CString");
std::string stlStr("STL string");
std::wstring stlWStr(_T("STL wstring")); //use _T() convert const string to wchar_t string
printf("All string convert to STL wstring\n");
stlWStr = CComBSTR(ansiStr);
wprintf(_T("Convert from char*, %s\n"), stlWStr.c_str());
stlWStr = unicodeStr;
wprintf(_T("Convert from wchar_t*, %s\n"), stlWStr.c_str());
stlWStr = cstr;
wprintf(_T("Convert from CString, %s\n"), stlWStr.c_str());
stlWStr = CComBSTR(stlStr.c_str());
wprintf(_T("Convert from std::string, %s\n"), stlWStr.c_str());
}
純C Runtime庫轉換
有時候使用Win32進行純C的開發,例如進行今日外掛程式的開發,不使用ATL,WTL,MFC以及STL的情況下,也會有轉換char*和wchar_t*的需求,但是不能使用ATL的宏,下面示範如何使用C Runtime庫來轉換。
void ConvertToWCharArrayUsingCRuntime()
{
char ansiStr[255] = "ANSI string";
wchar_t unicodeStr[255] = _T("Unicode string"); //use _T() convert const string to wchar_t string
printf("Convert to char* from wchar_t* using C Runtime library.\n");
sprintf(ansiStr, "%S", unicodeStr);
printf("Convert from wchar_t*, %s\n", ansiStr);
}
void ConvertToCharArrayUsingCRuntime()
{
char ansiStr[255] = "ANSI string";
wchar_t unicodeStr[255] = _T("Unicode string"); //use _T() convert const string to wchar_t string
printf("Convert to wchar_t* from char* using C Runtime library.\n");
swprintf(unicodeStr, _T("%S"), ansiStr);
wprintf(_T("Convert from char*, %s\n"), unicodeStr);
}
使用建議
上面講述了Windows Mobile和Wince(Windows Embedded CE)支援那麼多字串,那麼我們到底如何選擇使用的字串呢?其實這個沒有準則,我下面談一下我的經驗。這不是準則,所以只做參考之用。
一.盡量避免使用char*和wchar_t*
除了以下情況,不得不使用char*和wchar_t*時,大部分時候盡量避免使用char*和wchar_t*。
情況1
做今日組件開發,只是使用Win32,如果不依賴於ATL,WTL,MFC和STL,那麼沒得選擇只能使用char*和wchar_t*。
關於今日組件的開發,可以參考:
關於在Windows Mobile下今日外掛程式使用WTL的問題
情況2
封裝DLL或者通用靜態庫提供給第三方使用,例如TinyXML, CppUnitLite這樣的類庫,他們內部都在char*基礎上實現字串處理類,這樣庫就不依賴於ATL,WTL,MFC和STL了。
關於TinyXML, CppUnitLite可以參考:
Windows Mobile和Wince下使用TinyXML進行Native C++的開發
Wince和Windows Mobile下native C++的單元測試
Windows Mobile下使用CppUnitLite輸出測試結果
情況3
封裝DLL給.NET Compact Framework使用,介面函數只能使用char*和wchar_t*,不能使用CString和std::string。
關於DLL的封裝,可以參考:
Windows Mobile和Wince(Windows Embedded CE)下如何封裝Native DLL提供給.NET Compact Framework進行調用
Windows Mobile和Wince(Windows Embedded CE)下封裝Native DLL進一步探討
情況4
可以使用char*和wchar_t*包括一些字串常量,用於替換宏定義。
除了上述情況以外,應當盡量避免使用char*和wchar_t*,而是用CString,std::string等封裝好的字串類。
二.程式需要同時支援PC和Window Mobile版本時使用CString
如果使用C++加上ATL,WTL或者MFC開發,程式需要同時支援Windows案頭版和Windows Mobile以及Wince,可以考慮使用CString。CString可以很好的相容ANSI和Unicode版本。
例如我封裝的一個SQL Server Compact的資料庫訪問類,使用到CString,這個類可以支援PC和Windows Mobile。可以參考:
Windows Mobile下Native C++訪問SqlCe的封裝
三.程式需要跨平台時使用std::string
程式不僅僅用於windows平台,而且用於Linux,Unix,BSD等平台,可以考慮使用std::string,我一般不使用std::wstring,覺得沒有這個必要,使用std::string在需要的時候轉換就可以了。但是如果追求更高的跨平台性,那隻能使用char*和wchar_t*了,連STL都不依賴。
我個人喜歡使用std::string,因為我大量使用STL。在設計的時候把介面和處理邏輯分開,處理邏輯內部統一使用std::string以及STL的容器。需要介面互動出來,或者調用Win32的時候進行字串的轉換。
可以進一步參考的文章
http://www.tenouk.com/ModuleG.html
http://www.codeproject.com/KB/string/cppstringguide1.aspx