


1. 前言
Windows平臺有用Unicode和不用的區分:WinNT到Windows2003一直使用Unicode;WindowsCE也是如此;Win95和Win98就非如此。Windows編程對于字符使用也有各種情況:Windows API的處理方式、MFC的處理方式、VC++的處理方式、COM的處理方式。本文對所有這些方式作了一個(gè)總結,期望程序員能夠以本文為引子,找到各種情況下處理字符透明編程的方法。
所謂字符透明編程,主要針對Unicode和ANSI字符。本來(lái)Unicode是比較簡(jiǎn)單的一個(gè)東西,說(shuō)起來(lái)一個(gè)Unicode字符就是一個(gè)無(wú)符號短整數而已(16位,2個(gè)字節),但是,我相信大多數VC++程序員都有這樣的困惑:VC++和Win32API中那些用來(lái)實(shí)現ANSI和Unicode透明編程的,樣子長(cháng)得很像的宏,都在哪兒定義的?它們之間的關(guān)系如何?
這就要求我們了解編程平臺和操作系統支持ANSI和Unicode透明編程的方法。具體來(lái)說(shuō),就是要了解VC++的運行庫和Win32API是如何解決該問(wèn)題的。更進(jìn)一步,我們還應該了解COM解決該問(wèn)題的方式。最后,由于許多VC++程序員使用MFC框架進(jìn)行編程,了解MFC框架處理該問(wèn)題的方法也有必要。
本文內容在《ATL技術(shù)內幕》的第二章也有比較詳細的講述,尤其是關(guān)于COM的內容。但是,該書(shū)沒(méi)有講VC++相關(guān)內容;也沒(méi)有將內容整理得更清晰一些(尤其是沒(méi)有給出各種情況下的表格以供查找)。所以,本文將重點(diǎn)放在VC++和Windows針對字符透明編程采用方法的歸納和比較上。
本文可以作為一個(gè)出發(fā)點(diǎn),有了本文介紹的基礎之后,想要更多地了解BSTR的細節,可以看《ATL技術(shù)內幕》;想要更多地了解針對字符透明編程問(wèn)題,可以看其他的相關(guān)資料。
2. VC++對字符透明編程
首先要說(shuō)的是,對寬字符的支持其實(shí)是ANSI C標準的一部分,用以支持多字節表示一個(gè)字符。寬字符和Unicode并不是一回事,Unicode只是寬字符能支持的一種編碼方式。但是,由于我們現在主要考慮Unicode,不妨把這兩種東西當作同義。
2.1. 寬字符的定義
在A(yíng)NSI中,一個(gè)字符(char)的長(cháng)度為一個(gè)字節(Byte)。使用Unicode時(shí),一個(gè)字符應該占據一個(gè)字(Word),VC++在wchar.h頭文件中定義了最基本的寬字符類(lèi)型wchar_t:
typedef unsigned short wchar_t;
從這里我們可以清楚地看到,所謂的寬字符就是無(wú)符號短整數。
2.2. 常量寬字符串
對C++程序員而言,構造字符串常量是一項經(jīng)常性的工作。那么,如何構造寬字符字符串常量呢?很簡(jiǎn)單,只要在字符串常量前加上一個(gè)大寫(xiě)的L就可以了,比如:
L“Hello, world!”
這個(gè)L非常重要,只有帶上它,編譯器才知道你要將字符串存成每個(gè)字符1個(gè)字。還要注意,在L和字符串之間不能有空格。
2.3. 寬字符串庫函數
為了操作寬字符串,VC++專(zhuān)門(mén)定義了一套函數,比如,求寬字符串長(cháng)度的函數是
size_t __cdel wchlen ( const wchar_t * )
為什么要專(zhuān)門(mén)定義這些函數呢?最根本的原因是,ANSI下的字符串都是以“\0”來(lái)標識字符串尾的,許多字符串函數的正確操作均以此為基礎進(jìn)行。而我們知道,在寬字符的情況下,一個(gè)字符在內存中要占據一個(gè)字的空間,這就會(huì )使操作ANSI字符的字符串函數無(wú)法正確操作。以“Hello”字符串為例,在寬字符下,它的五個(gè)字符是:
0x0048 0x0065 0x006c 0x006c 0x006f
在內存中,實(shí)際的排列是:
48 00 65 00 6c 00 6c 00 6f 00
于是,ANSI字符串函數,如 strlen,在碰到第一個(gè)48后的00時(shí),就會(huì )認為字符串到尾了,用 strlen 對寬字符串求長(cháng)度的結果就永遠會(huì )是1!
2.4. 用宏實(shí)現對ANSI和Unicode的透明編程
看到這兒,想必程序員都會(huì )感到沮喪:“完了,兩套字符串函數!”不用說(shuō),針對ANSI字符和Unicode字符維護兩套代碼是令人討厭的事情。就算是自己在一套代碼中寫(xiě)一些預編譯語(yǔ)句執行條件編譯,也是非常麻煩的事,因為要用字符串的地方實(shí)在是太多了。為了減輕大家的編程負擔,VC++定義了一系列的宏,幫助實(shí)現對ANSI和Unicode的透明編程。從上面的討論我們可以看到,要做的工作是兩個(gè):一,透明定義字符和常量字符串;二,透明調用字符串函數。下面就分別講用于這兩個(gè)方面的宏。
2.4.1. 透明定義字符和常量字符串
該工作主要是由 tchar.h 頭文件中定義的若干宏完成的,根據“_UNICODE”(注意,有下劃線(xiàn))定義與否,這些宏展開(kāi)為ANSI或Unicode字符(字符串)。有興趣者可以去查看頭文件,這里我做了如下歸納:
字符宏:
宏 未定義_UNICODE (ANSI字符) 定義了_UNICODE(Unicode字符)
_TCHAR char wchar_t
_TSCHAR signed char wchar_t
_TUCHAR unsigned char wchar_t
_TXCHAR char wchar_t
TCHAR char wchar_t
常量字符串宏:
宏 未定義_UNICODE
(ANSI常量字符串) 定義了_UNICODE
(Unicode常量字符串)
__T(x) x L##x
_T x __T(x)
_TEXT x __T(x)
* 注意,“L##x”中的“##”雖然看起來(lái)很怪,卻是ANSI C標準的預處理語(yǔ)法,它叫做“粘貼符號(token paste)”,表示將前面的L添加到宏參數上。也就是說(shuō),如果我們寫(xiě)了一個(gè)__T(“Software Department”),展開(kāi)后即為L(cháng)“Software Department”。
為了方便??梢院?jiǎn)單地總結兩條規則:
*定義字符用TCHAR
*定義常量字符串用_T
2.4.2. 透明調用字符串函數
VC++實(shí)現透明調用字符串函數也是定義了一系列宏,不過(guò),這些宏數量太多了,沒(méi)有辦法把它們都列在這里,就象征性地列出一些,給大家一個(gè)印象:
宏 未定義_UNICODE
(ANSI字符串函數) 定義了_UNICODE
(Unicode字符串函數)
_tcschr strchr wcschr
_tcscmp strcmp wcscmp
_tcslen strlen wcslen
我給大家的建議是:當你需要具體的某個(gè)函數的時(shí)候,最好去查 tchar.h,或者查某些專(zhuān)門(mén)講VC++運行庫函數的書(shū)。
好了,VC++的東西大致就是這樣,下面講Windows的處理方法。
3. Win32API對字符透明編程
3.1. Win32API定義的數據類(lèi)型
首先,Win32API中定義了若干自己的字符數據類(lèi)型。所謂自己定義,無(wú)非就是用一些宏把C中的數據類(lèi)型包裝起來(lái)而已(確實(shí)是C的數據類(lèi)型,Win32API是按C的函數調用方式定義的)。對字符數據類(lèi)型的定義基本上都在 winnt.h 頭文件中。
最基本的是兩種字符數據類(lèi)型,分別對應8位的單字節字符和16位的Unicode字符:
typedef char CHAR
typedef unsigned short WCHAR
另外,Windows還定義了6種8位字符串指針、6種16位字符串指針、4種8位常量字符串指針、4種16位常量字符串指針。這里歸納如下:
數據類(lèi)型 ANSI UNICODE 內部數據類(lèi)型
CHAR 8位 char
WCHAR 16位 unsigned short
PCHAR CHAR* char*
PCH & LPCH CHAR* char*
PSTR&NPSTR&LPSTR CHAR* char*
PCCH&LPCCH CONST CHAR* const char*
PCSTR&LPCSTR CONST CHAR* const char*
PWCHAR WCHAR* unsigned short*
PWCH&LPWCH WCHAR* unsigned short*
PWSTR&LPWSTR&NWPSTR* WCHAR* unsigned short*
PCWCH&LPCWCH CONST WCHAR* const unsigned short*
PCWSTR&LPCWSTR CONST WCHAR* const unsigned short*
* 注意,“NWPSTR”并沒(méi)有寫(xiě)錯,確實(shí)是“NWP”,本來(lái)我也認為應該是“NPW”(近指針)。
* 所謂的“遠指針”“近指針”,在32位編程環(huán)境下已經(jīng)沒(méi)有意義了。
由于這些數據類(lèi)型都是Windows內部分別針對ANSI和Unicode定義的,在編程中,當然要避免使用。把它們列在這里是為了方便大家參考。
3.2. Win32API中對ANSI和Unicode的透明編程
我們還是從兩個(gè)方面來(lái)講:透明定義字符和常量字符串;透明調用字符串函數。
3.2.1. 透明定義字符和常量字符串
這里把winnt.h 頭文件中實(shí)現透明定義字符和常量字符串的部分摘出來(lái),相信對大家的理解會(huì )有所幫助(加黑部分定義了通用字符定義):
//
// Neutral ANSI/UNICODE types and macros
//
#ifdef UNICODE // 以下是Unicode相關(guān)定義
#ifndef _TCHAR_DEFINED
typedef WCHAR TCHAR, *PTCHAR; // 定義基本通用類(lèi)型
#define _TCHAR_DEFINED
#endif /* !_TCHAR_DEFINED */
typedef LPWSTR LPTCH, PTCH; // 定義各種通用字符串指針
typedef LPWSTR PTSTR, LPTSTR;
typedef LPCWSTR LPCTSTR;
typedef LPWSTR LP; // 奇怪,為什么要定義它?
#define __TEXT(quote) L##quote // 定義字符串常量宏
#else /* UNICODE */ // 以下是ANSI相關(guān)定義
#ifndef _TCHAR_DEFINED
typedef char TCHAR, *PTCHAR; // 定義基本通用類(lèi)型
#define _TCHAR_DEFINED
#endif /* !_TCHAR_DEFINED */
typedef LPSTR LPTCH, PTCH; // 定義各種通用字符串指針
typedef LPSTR PTSTR, LPTSTR;
typedef LPCSTR LPCTSTR;
#define __TEXT(quote) quote // 定義字符串常量宏
#endif /* UNICODE */
#define TEXT(quote) __TEXT(quote) // 定義另一個(gè)字符串常量宏
從這段程序我們可以看出,winnt.h不過(guò)就是根據是否定義了UNICODE(沒(méi)有下劃線(xiàn)),利用Windows內部定義的數據類(lèi)型進(jìn)行一個(gè)條件編譯。
下面,用表格的形式將以上內容做一個(gè)總結,以方便大家查閱:
宏 未定義UNICODE
(ANSI字符和字串) 定義了UNICODE
(Unicode字符和字串)
TCHAR char WCHAR
PTCHAR char* WCHAR*
PTCH&LPTCH LPSTR LPWSTR
PTSTR& LPTSTR LPSTR LPWSTR
LPCTSTR LPCSTR LPCWSTR
__TEXT( quote ) quote L##quote
TEXT( quote ) quote L##quote
* 注意,LP是專(zhuān)門(mén)針對Unicode定義的,所以,無(wú)法用于透明編程(我不知道為什么要定義它),故未將它列在表中。
比較VC++和Windows的定義,我們可以得出如下結論:
VC++沒(méi)有直接定義指針類(lèi)型,Windows直接定義了指針類(lèi)型;
VC++和Windows都定義了TCHAR,所以我們使用TCHAR兼容性最好;
也許我們沒(méi)有必要直接使用Windows定義的指針類(lèi)型,使用TCHAR*就可以了;
Windows定義的類(lèi)型如此之多,可能和寫(xiě)Windows程序的開(kāi)發(fā)組較多,標準不一有關(guān)。由于要兼容歷史上的程序,只好定義多一些。我們沒(méi)有必要去使用那些雜亂的定義。
3.2.2. 透明調用字符串函數
Win32API中又定義了一套字符串函數,總的來(lái)說(shuō),它的解決方法是,對ANSI和Unicode字符,分別定義了不同的函數(在Kernel32.dll中實(shí)現)。比如,求字符串長(cháng)度的函數,就分別是:
WINBASEAPI int WINAPI lstrlenA ( LPCSTR lpString );
WINBASEAPI int WINAPI lstrlenW( LPCWSTR lpString );
然后,另一個(gè)宏根據是否定義了“UNICODE”分別展開(kāi)為這兩個(gè)函數:
#ifdef UNICODE
#define lstrlen lstrlenW
#else
#define lstrlen lstrlenA
#endif // !UNICODE
又是一堆函數!雖然Win32API并沒(méi)有實(shí)現所有的字符串函數,我還是不愿意再記它們。我認為VC++的運行庫函數就夠好了。想要了解Windows函數的可以自己到MSDN里面去找。
4. COM對字符透明編程
好了,現在再來(lái)說(shuō)一下COM接口和OLE中的字符類(lèi)型。進(jìn)行COM接口編程時(shí),我們經(jīng)常會(huì )接觸OLESTR之類(lèi)的東西。它們又是怎么一會(huì )事呢?大多數COM和OLE中使用的數據類(lèi)型都是在basetype.h和wtypes.h中定義的。我將有關(guān)內容總結如下:
宏 未定義OLE2ANSI
(Unicode字符和字串) 定義了OLE2ANSI
(ANSI字符和字串)
OLECHAR WCHAR char
OLESTR(x) L##x x
LPOLESTR OLECHAR __RPC_FAR * LPSTR
LPCOLESTR const OLECHAR __RPC_FAR * LCPSTR
首先,我們可以看到,在定義LPOLESTR和LPCOLESTR時(shí),利用了WIN32API的定義(那個(gè)看起來(lái)很深奧的“__RPC_FAR”,只是一個(gè)用于遠程調用時(shí)規定調用約定的宏,定義在 rpc.h 頭文件中)。
其次,我們看到,OLESTR之類(lèi)的宏,目的也是為了實(shí)現對ANSI和UNICODE的透明編譯。根據是否定義了OLE2ANSI來(lái)選擇編譯ANSI版本還是UNICODE版本。真不知道定義這么多宏做什么,也許是因為各個(gè)小組獨立開(kāi)發(fā),各自用各自的東西的原因。
要了解COM中字符串使用更多的細節,請參考《ATL技術(shù)內幕》。
5. MFC對字符透明編程
最后,講講MFC如何處理該問(wèn)題。MFC的處理很簡(jiǎn)單,它只是利用了VC++運行庫的處理方式。具體來(lái)說(shuō),就是從Afxw_32.h頭文件的152行開(kāi)始,有這么幾句:
#ifndef _INC_TCHAR
#include <tchar.h> // 該頭文件包含了VC++字符串透明編程所需內容
#endif
#ifdef _MBCS
#ifndef _INC_MBCTYPE
#include <mbctype.h>
#endif
#ifndef _INC_MBSTRING
#include <mbstring.h>
#endif
#endif
也就是說(shuō),在MFC中,我們應該使用VC++的字符串透明編程方式。
但是,要注意的是,對于BSTR這個(gè)宏(該宏主要在Oleauto.h頭文件中聲明的自動(dòng)化接口的函數中使用),MFC根據OLE2ANSI是否被定義有其不同展開(kāi)(在A(yíng)fx.h)中:
#ifndef _OLEAUTO_H_
#ifdef OLE2ANSI
typedef LPSTR BSTR; // 用Windows的類(lèi)型定義BSTR
#else
typedef LPWSTR BSTR; // 用Windows的類(lèi)型定義BSTR
#endif
#endif
6. 常見(jiàn)問(wèn)題(Q&A)
最后,根據我的體會(huì )講一些大家有可能感到迷惑的問(wèn)題(當然,也許大家認為這些問(wèn)題都很簡(jiǎn)單)。
Q1:VC++的處理方式和Win32API的處理方式,我該用哪一個(gè)?
A1:這兩種方法的關(guān)系是這樣的:Win32API給出的是操作系統內帶的支持,而VC++可以被看做是與操作系統相關(guān)性較小的一種方式(雖然微軟做了很多擴展,把它搞得和操作系統有了不少相關(guān)性)。所以,如果你要考慮程序以后在各平臺上的移植,還是少用操作系統內部直接支持的東西為妙。
Q2:我們什么時(shí)候定義_UNICODE或是UNICODE?
A2:調整Project Setting時(shí)。按Alt+F7,選擇“C/C++”標簽,將“_UNICODE”或“UNICODE”添加在“Preprocessor definitions”編輯框中就可以了。
Q3:那些定義字符串常量的宏,我該用哪一個(gè)?
A3:想必大家也注意到了,為了透明轉換常量字符串,WINDOWS和VC++都定義了各自的宏:
Windows:__TEXT和TEXT
VC++:__T、_T和_TEXT
這五個(gè)宏最容易讓程序員迷惑了,因為它們僅僅是不加下劃線(xiàn),加一個(gè)下劃線(xiàn)、兩個(gè)下劃線(xiàn)的問(wèn)題。其實(shí),只要你是開(kāi)發(fā)Windows上的應用程序,用哪個(gè)宏沒(méi)有什么關(guān)系??紤]到不和平臺關(guān)系過(guò)于緊密,以及書(shū)寫(xiě)的長(cháng)度,_T可能是一個(gè)比較好的選擇。
Q4:我如何在Debug狀態(tài)下觀(guān)察Unicode字符串的值?
A4:一般說(shuō)來(lái),如果你把一個(gè)Unicode字符串變量名拖到 Watch 窗口中,你看到的是一個(gè)32位16進(jìn)制值,也就是說(shuō),是一個(gè)指針的地址。而我們想看到的是內存中存的字符串。怎么辦?你在這個(gè)變量名后面加上“, su”就可以了。
對ANSI和Unicode字符的透明編程問(wèn)題,盡我的了解講了這么多,希望大家覺(jué)得有所幫助。大家如果發(fā)現有遺漏和謬誤之處,請指正!
Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1800155
聯(lián)系客服