2010年11月11日 星期四

【心得】Generic Text Mapping

http://www.programmer-club.com.tw/ShowSameTitleN/vcdotnet/2115.html

【心得】Generic Text Mapping


【一個常見的錯誤與錯誤的解決方法】

很多初學者從 VC++ 6.0 升級到 VC++ 2005 的時候, 經常發現到原本在 VC++ 6.0 版順利編譯並執行的程式突然無法編譯了. 像以下這個簡單的例子:

char ch[80];
CString str("Hello World");
strcpy(ch, str);

當用在新產生的 VC++ 2005 專案裡的時候, VC++ 2005 會發出類似以下的錯誤訊息:

error C2664: 'strcpy' : cannot convert parameter 2 from 'CString' to 'const char *'

有些初學者不明白這個錯誤訊息的原因, 也不去深入研究, 認為 typecasting 可以解決這個問題, 就做了這個更改:

strcpy(ch, (const char*)(LPCTSTR)str);

改了之後, 錯誤沒有了, 取而代之的是個警告:

warning C4996: 'strcpy' was declared deprecated

建議程式員應該用 strcpy_s() 這個安全的函式. 這個暫且不管它, 至少程式是可以編譯了.

但 typecasting 有解決到問題嗎? 答案是沒有, 上面的 typecast 叫編譯器不出聲, 把真正的問題隱藏了起來.

寫 C 程式幾乎完全沒有必要用到 typecast. 而大部份的 C++ 程式也不需要用到 typecast. 初學者的程式用到 typecast 幾乎等同於有潛在的錯誤或邏輯上的問題.


【編譯器的字集設定】

不管那個版本, VC++ 一共有三種字集的設定: ANSI, MBCS, 及 Unicode. 前兩者合稱「非-Unicode」設定.

C 及 C++ 語言有兩種字元類型, 一個就是我們常用的 char, 另一個是 wchar_t. 為了區分這兩者, 一般把 char 稱為「窄字元」, 把 wchar_t 稱為「寬字元」. 從程式語言的角度來看, char 跟 wchar_t 純粹是語言的類型, 跟 ASCII 或 Unicode 是兩回事. ASCII 及 Unicode 是編碼, 跟類型無關.

在 VC++ 裡, char 的長度是 8 位元, 使用 ASCII 編碼. wchar_t 的長度是 16 位元, 使用 Unicode 編碼.

【視窗系統的 API】

在視窗系統上, 大多數有字串或字元作參數的 API (如 MessageBox()) 都是有兩個版本的. 一個是窄字元版, 一個是寬字元版. 實際上, 視窗系統裡根本沒有 MessageBox() 這個 API, 它有的是 MessageBoxA() 及 MessageBoxW() 這兩個版本的 API.

以 MessageBox() 為例, 這兩個版本的宣告如下:

MessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
MessageBoxW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType);

xxxA() 用的是 LPCSTR, 既 const char*, 一個「窄字元指標」.
xxxW() 用的是 LPWSTR, 既 const wchar_t*, 一個「寬字元指標」.

同時, 視窗的頭檔裡也有這個定義:

#ifdef UNICODE
#define MessageBox MessageBoxW
#else
#define MessageBox MessageBoxA
#endif

所 以當專案的字集設定是 Unicode 時, VC++ 會自動的設定兩個 preprocessor 的符號: UNICODE 及 _UNICODE. 在編譯時, preprocessor 會自動把 MessageBox() 變成 MessageBoxW(), 所呼叫的是 Unicode 的版本. 反之, 則呼叫 MessageBoxA(), 一個 非-Unicode 的版本.

MFC 的 CString 也是這樣子的. 在 Unicode 設定的專案下, 在編譯時, CString 會變成 CStringW, 一個寬字元的 CString; 如果是在 非-Unicode 的設定下, CString 則變成 CStringA, 一個窄字元的 CString.

從這個角度來看, MessageBox() 跟 CString 是「中性」的. 它可以變成「寬」, 也可以變成「窄」, 視專案的字集定義而定.

所以在使用 MessageBox() 或 CString 時, 要留意到專案的字集設定. 因為如果字集設定跟參數的類型不符, 如呼叫 MessageBoxW() 但傳給它的是窄字元, 那編譯器當然會發出錯誤訊息.

從這裡我們就可以知道上面的程式在 VC++ 6.0 可以正常編譯, 但在 2005 的專案裡卻發生錯誤的原因:
1. 因為程式裡「寬/窄」混合在一起,
2. 專案對字集的預設在 6.0 版跟 2005 版不同.

在 6.0 版上, 專案對「字集」的預設是 MBCS (Multi-Byte Character Set). 在 2005 版上, 專案對「字集」的預設是 Unicode.

混合式的寫法, 在轉換字集設定時一定會遇到問題. 所以這種寫法要避免.


【純「窄」或「寬」的程式】

當然我們可以不用管編譯器字集的設定, 而全用單一「寬」或「窄」的類型, 函式來寫我們的程式. 上面的例子用窄字元來寫的話是這樣子的:

char ch[80];
CStringA str("Hello World");
strcpy(ch, str);


用寬字元則是:

wchar_t ch[80];
CStringW str(L"Hello World");
wcscpy(ch, str);

wcscpy() 是 strcpy() 的「寬」版本.

但 是如果我們要用同一個專案來產生 Unicode 及 非-Unicode 的執行檔, 用這種比較極端的寫法就不是很方便. 同時, 大部份人都熟悉 CString 及 MessageBox() 而不是 CStringW 或 MessageBoxA(). 另一點, MSDN 上的資料大部份是 MessageBox() 而不是 MessageBoxA(). 原因很簡單, Microsoft 希望我們用「中性」的寫法而不是寬或窄的寫法.


【Generic Text Mapping】

像 MessageBox() 或 CString 這樣的函式定義或類型, 會依據專案字集的設定而自動選擇寬或窄版本的機制, 在視窗上有個專有名詞, 叫 "Generic Text Function" 或 "Generic Text Type". 因為它們是根據專案裡字集的設定來 map 到寬或窄的真實函式或類型, 它們也叫 "Generic Text Mapping".

同樣的, C 及 C++ 函式庫裡以字元, 字串為參數的函式, 結構也有兩個版本. 本文一開始例子裡的 strcpy() 是個「窄字元」版本的函式. 它相對的「寬字元」版是 wcscpy().


【C 標準函式庫】

雖 然語言標準並沒有提供 generic text mapping 的機制, 但 Windows 的頭檔 <tchar.h> 有提供 C 函式庫的 generic text mapping. strcpy()/wcscpy() 的 generic text function 是 _tcscpy(), 它是這樣定義的:

#ifdef UNICODE
#define _tcscpy wcscpy
#else
#define _tcscpy strcpy
#endif

該頭檔也定義了一系列的 generic text 類型:

http://msdn2.microsoft.com/en-us/library/se784sk6(VS.80).aspx

常數:

http://msdn2.microsoft.com/en-us/library/z94kex77(VS.80).aspx

函式:

http://msdn2.microsoft.com/en-us/library/tsbaswba(VS.80).aspx


所以除了之前所講的「全寬」, 「全窄」的寫法外, 多了一個叫「全 generic text mapping」的寫法:

_TCHAR ch[80];
CString str(_T("Hello World"));
_tcscpy(ch, str);

這也是微軟建議的做法. 因為在 Unicode 的專案設定下, preprocessor 將它們變成:

wchar_t ch[80];
CStringW str(L"Hello World");
wcscpy(ch, str);

而在 非-Unicode 專案的設定下, preprocessor 後, 編譯器所看到的是:

char ch[80];
CStringA str("Hello World");
strcpy(ch, str);

這樣同一個專案就可以以 Unicode 或 非-Unicode 來產生兩種不同的執行檔.

當然, 只要明白這個原理, 程式員並不一定要用 generic text 的做法. 如果專案只用單一種字集, 那混合式的寫法也是可以的.


【C++ 標準函式庫】

可惜的是微軟並沒有提供 C++ 的 generic text mapping. 如果你不想自己寫的話, 這裡有一個可以用:

http://www.codeproject.com/string/tstl_h.asp



【「寬」-「窄」之間的轉換】

並不是每個 API 都是用 generic text mapping 的, 有些 API 只有寬字元版本. 所以有時, 寬-窄字元/字串之間的轉換有其必要.

視窗有寬-窄字串轉換的 API - MultiByteToWideChar() 及 WideCharToMultiByte().

MFC/ATL 的使用者有一系列非常簡單的 macro 可以用 (頭檔是 <atlconv.h>):

http://msdn2.microsoft.com/en-us/library/87zae4a3(VS.80).aspx

上面的 A2W(), W2T() 等自 6.0 起就有了, 但它們有潛在的 stack overflow 的問題. 7.0 版的 ATL 用了新的 class 來做, 更正了之前的問題:

http://msdn2.microsoft.com/en-us/library/awt7k7f5(VS.80).aspx

沒有留言:

張貼留言