本文摘自:http://www.vckbase.com/index.php/wv/1206
一、前言
上回書(shū)介紹了GUID、CLSID、IID和接口的概念。本回的重點(diǎn)是介紹 COM 中的數(shù)據(jù)類(lèi)型。咋還不介紹組件程序的設(shè)計(jì)步驟呀?咳......別著急,別著急!孔子曰:“飯要一口一口地吃”;老子語(yǔ):“心急吃不了熱豆腐”,孫子云:“走一步看一步吧” ...... 先掌握必要的知識(shí),將來(lái)寫(xiě)起程序來(lái)才會(huì)得心應(yīng)手也:-)
走入正題之前,請(qǐng)大家牢牢記住一條原則:COM 組件是運(yùn)行在分布式環(huán)境中的。比如,你寫(xiě)了一個(gè)組件程序(DLL或EXE),那么使用者可能是在本機(jī)的某個(gè)進(jìn)程內(nèi)加載組件(INPROC_SERVER);也可能是從另一個(gè)進(jìn)程中調(diào)用組件的進(jìn)程(LOCAL_SERVER);也可能是在這臺(tái)計(jì)算機(jī)上調(diào)用地球那邊計(jì)算機(jī)上的組件(REMOTE_SERVER)。所以在理解和設(shè)計(jì)的時(shí)候,要時(shí)時(shí)刻刻想起這句話。快!拿出小本本,記下來(lái)!
二、HRESULT 函數(shù)返回值
每個(gè)人在做程序設(shè)計(jì)的時(shí)候,都有他們各自的哲學(xué)思想。拿函數(shù)返回值來(lái)說(shuō),就有好多種形式。
| 函數(shù) |
返回值 |
返回值信息 |
| double sin(double) |
浮點(diǎn)數(shù)值 |
計(jì)算正玄值 |
| BOOL DeleteFile(LPCTSTR) |
布爾值 |
文件刪除是否成功。如失敗,需要GetLastError()才能取得失敗原因 |
| void * malloc(size_t) |
內(nèi)存指針 |
內(nèi)存申請(qǐng),如果失敗,返回空指針 NULL |
| LONG RegDeleteKey(HKEY,LPCTSTR) |
整數(shù) |
刪除注冊(cè)表項(xiàng)。0表示成功,非0失敗,同時(shí)這個(gè)值就反映了失敗的原因 |
| UINT DragQueryFile(HDROP,UINT,LPTSTR,UINT) |
整數(shù) |
取得拖放文件信息。以不同的參數(shù)調(diào)用,則返回不同的含義: 一會(huì)兒表示文件個(gè)數(shù),一會(huì)兒表示文件名長(zhǎng)度,一會(huì)兒表示字符長(zhǎng)度 |
| ...... ...... |
... |
...... ...... |
如此紛繁復(fù)雜的返回值,如此含義多變的返回值,使得大家在學(xué)習(xí)和使用的過(guò)程中,增加了額外的困難。好了,COM 的設(shè)計(jì)規(guī)范終于對(duì)他們進(jìn)行了統(tǒng)一。組件API及接口指針中,除了IUnknown::AddRef()和IUnknown::Release()兩個(gè)函數(shù)外,其它所有的函數(shù),都以 HRESULT 作為返回值。大家想象一個(gè)組件的接口函數(shù)比如叫Add(),完成2個(gè)整數(shù)的加法運(yùn)算,在C語(yǔ)言中,我們可以如下定義:
- long Add( long n1, long n2 )
- {
- return n1 + n2;
- }
long Add( long n1, long n2 )
{
return n1 + n2;
}
還記得剛才我們說(shuō)的原則嗎?COM 組件是運(yùn)行在分布式環(huán)境中的。也就是說(shuō),這個(gè)函數(shù)可能運(yùn)行在“地球另一邊”的計(jì)算機(jī)上,既然運(yùn)行在那么遙遠(yuǎn)的地方,就有可能出現(xiàn)服務(wù)器關(guān)機(jī)、網(wǎng)絡(luò)掉線、運(yùn)行超時(shí)、對(duì)方不在服務(wù)區(qū)......等異常。于是,這個(gè)加法函數(shù),除了需要返回運(yùn)算結(jié)果以外,還應(yīng)該返回一個(gè)值------函數(shù)是否被正常執(zhí)行了。
- HRESULT Add( long n1, long n2, long *pSum )
- {
- 3*pSum = n1 + n2;
- return S_OK;
- }
HRESULT Add( long n1, long n2, long *pSum )
{
3*pSum = n1 + n2;
return S_OK;
}
如果函數(shù)正常執(zhí)行,則返回 S_OK,同時(shí)真正的函數(shù)運(yùn)行結(jié)果則通過(guò)參數(shù)指針?lè)祷?。如果遇到了異常情況,則COM系統(tǒng)經(jīng)過(guò)判斷,會(huì)返回相應(yīng)的錯(cuò)誤值。常見(jiàn)的返回值有:
| HRESULT |
值 |
含義 |
| S_OK |
0x00000000 |
成功 |
| S_FALSE |
0x00000001 |
函數(shù)成功執(zhí)行完成,但返回時(shí)出現(xiàn)錯(cuò)誤 |
| E_INVALIDARG |
0x80070057 |
參數(shù)有錯(cuò)誤 |
| E_OUTOFMEMORY |
0x8007000E |
內(nèi)存申請(qǐng)錯(cuò)誤 |
| E_UNEXPECTED |
0x8000FFFF |
未知的異常 |
| E_NOTIMPL |
0x80004001 |
未實(shí)現(xiàn)功能 |
| E_FAIL |
0x80004005 |
沒(méi)有詳細(xì)說(shuō)明的錯(cuò)誤。一般需要取得 Rich Error 錯(cuò)誤信息(注1) |
| E_POINTER |
0x80004003 |
無(wú)效的指針 |
| E_HANDLE |
0x80070006 |
無(wú)效的句柄 |
| E_ABORT |
0x80004004 |
終止操作 |
| E_ACCESSDENIED |
0x80070005 |
訪問(wèn)被拒絕 |
| E_NOINTERFACE |
0x80004002 |
不支持接口 |

圖一、HRESULT 的結(jié)構(gòu)
HRESULT 其實(shí)是一個(gè)雙字節(jié)的值,其最高位(bit)如果是0表示成功,1表示錯(cuò)誤。具體參見(jiàn) MSDN 之"Structure of COM Error Codes"說(shuō)明。我們?cè)诔绦蛑腥绻枰袛喾祷刂?,則可以使用比較運(yùn)算符號(hào);switch開(kāi)關(guān)語(yǔ)句;也可以使用VC提供的宏:
三、UNICODE
計(jì)算機(jī)發(fā)明后,為了在計(jì)算機(jī)中表示字符,人們制定了一種編碼,叫ASCII碼。ASCII碼由一個(gè)字節(jié)中的7位(bit)表示,范圍是0x00 - 0x7F 共128個(gè)字符。他們以為這128個(gè)數(shù)字就足夠表示abcd....ABCD....1234 這些字符了。
咳......說(shuō)英語(yǔ)的人就是“笨”!后來(lái)他們突然發(fā)現(xiàn),如果需要按照表格方式打印這些字符的時(shí)候,缺少了“制表符”。于是又?jǐn)U展了ASCII的定義,使用一個(gè)字節(jié)的全部8位(bit)來(lái)表示字符了,這就叫擴(kuò)展ASCII碼。范圍是0x00 - 0xFF 共256個(gè)字符。
咳......說(shuō)中文的人就是聰明!中國(guó)人利用連續(xù)2個(gè)擴(kuò)展ASCII碼的擴(kuò)展區(qū)域(0xA0以后)來(lái)表示一個(gè)漢字,該方法的標(biāo)準(zhǔn)叫GB-2312。后來(lái),日文、韓文、阿拉伯文、臺(tái)灣繁體(BIG-5)......都使用類(lèi)似的方法擴(kuò)展了本地字符集的定義,現(xiàn)在統(tǒng)一稱(chēng)為 MBCS 字符集(多字節(jié)字符集)。這個(gè)方法是有缺陷的,因?yàn)楦鱾€(gè)國(guó)家地區(qū)定義的字符集有交集,因此使用GB-2312的軟件,就不能在BIG-5的環(huán)境下運(yùn)行(顯示亂碼),反之亦然。
咳......說(shuō)英語(yǔ)的人終于變“聰明”一些了。為了把全世界人民所有的所有的文字符號(hào)都統(tǒng)一進(jìn)行編碼,于是制定了UNICODE標(biāo)準(zhǔn)字符集。UNICODE 使用2個(gè)字節(jié)表示一個(gè)字符(unsigned shor int、WCHAR、_wchar_t、OLECHAR)。這下終于好啦,全世界任何一個(gè)地區(qū)的軟件,可以不用修改地就能在另一個(gè)地區(qū)運(yùn)行了。雖然我用 IE 瀏覽日本網(wǎng)站,顯示出我不認(rèn)識(shí)的日文文字,但至少不會(huì)是亂碼了。UNICODE 的范圍是 0x0000 - 0xFFFF 共6萬(wàn)多個(gè)字符,其中光漢字就占用了4萬(wàn)多個(gè)。嘿嘿,中國(guó)人賺大發(fā)了:0)
在程序中使用各種字符集的方法:
在上面的例子中,T是非常有意思的一個(gè)符號(hào)(TCHAR、LPCTSTR、LPTSTR、_T()、_TEXT()...),它表示使用一種中間類(lèi)型,既不明確表示使用 MBCS,也不明確表示使用 UNICODE。那到底使用哪種字符集那?嘿嘿......編譯的時(shí)候決定吧。設(shè)置條件編譯的方式是:VC6中,"Project\Settings...\C/C++卡片 Preprocessor definitions" 中添加或修改 _MBCS、_UNICODE;VC.NET中,"項(xiàng)目\屬性\配置屬性\常規(guī)\字符集"然后用組合窗進(jìn)行選擇。使用 T 類(lèi)型,是非常好的習(xí)慣,嚴(yán)重推薦!
四、BSTR
COM 中除了使用一些簡(jiǎn)單標(biāo)準(zhǔn)的數(shù)據(jù)類(lèi)型外(注2),字符串類(lèi)型需要特別重點(diǎn)地說(shuō)明一下。還記得原則嗎?COM 組件是運(yùn)行在分布式環(huán)境中的。通俗地說(shuō),你不能直接把一個(gè)內(nèi)存指針直接作為參數(shù)傳遞給COM函數(shù)。你想想,系統(tǒng)需要把這塊內(nèi)存的內(nèi)容傳遞到“地球另一 邊”的計(jì)算機(jī)上,因此,我至少需要知道你這塊內(nèi)存的尺寸吧?不然讓我如何傳遞呀?傳遞多少字節(jié)呀?!而字符串又是非常常用的一種類(lèi)型,因此 COM 設(shè)計(jì)者引入了 BASIC 中字符串類(lèi)型的表示方式---BSTR。BSTR 其實(shí)是一個(gè)指針類(lèi)型,它的內(nèi)存結(jié)構(gòu)是:(輸入程序片段 BSTR p = ::SysAllocString(L"Hello,你好");斷點(diǎn)執(zhí)行,然后觀察p的內(nèi)存)

圖二、BSTR 內(nèi)存結(jié)構(gòu)
BSTR 是一個(gè)指向 UNICODE 字符串的指針,且 BSTR 向前的4個(gè)字節(jié)中,使用DWORD保存著這個(gè)字符串的字節(jié)長(zhǎng)度( 沒(méi)有含字符串的結(jié)束符)。因此系統(tǒng)就能夠正確處理并傳送這個(gè)字符串到“地球另一 邊”了。特別需要注意的是,由于BSTR的指針就是指向 UNICODE 串,因此 BSTR 和 LPOLESTR 可以在一定程度上混用,但一定要注意:
有函數(shù) fun(LPCOLESTR lp),則你調(diào)用 BSTR p=...; fun(p); 正確
有函數(shù) fun(const BSTR bstr),則你調(diào)用 LPCOLESTR p=...; fun(p); 錯(cuò)誤!??!
有關(guān) BSTR 的處理函數(shù):
| API 函數(shù) |
說(shuō)明 |
| SysAllocString() |
申請(qǐng)一個(gè) BSTR 指針,并初始化為一個(gè)字符串 |
| SysFreeString() |
釋放 BSTR 內(nèi)存 |
| SysAllocStringLen() |
申請(qǐng)一個(gè)指定字符長(zhǎng)度的 BSTR 指針,并初始化為一個(gè)字符串 |
| SysAllocStringByteLen() |
申請(qǐng)一個(gè)指定字節(jié)長(zhǎng)度的 BSTR 指針,并初始化為一個(gè)字符串 |
| SysReAllocStringLen() |
重新申請(qǐng) BSTR 指針 |
|
CString 函數(shù) |
說(shuō)明 |
| AllocSysString() |
從 CString 得到 BSTR |
| SetSysString() |
重新申請(qǐng) BSTR 指針,并復(fù)制到 CString 中 |
|
CComBSTR 函數(shù)
ATL 的 BSTR 包裝類(lèi)。在 atlbase.h 中定義 |
Append()、AppendBSTR()、AppendBytes()、ArrayToBSTR()、BSTRToArray()、AssignBSTR()、Attach()、Detach()、Copy()、CopyTo()、Empty()、Length()、ByteLength()、ReadFromStream()、WriteToStream()、LoadString()、ToLower()、ToUpper() 運(yùn)算符重載:!,!=,==,<,>,&,+=,+,=,BSTR |
太多了,但從函數(shù)名稱(chēng)不能看出其基本功能。詳細(xì)資料,查看MSDN 吧。另外,左側(cè)函數(shù),有很多是 ATL 7.0 提供的,VC6.0 下所帶的 ATL 3.0 不支持。 由于我們將來(lái)主要用 ATL 開(kāi)發(fā)組件程序,因此使用 ATL 的 CComBSTR 為主。VC也提供了其它的包裝類(lèi) _bstr_t。 |
五、各種字符串類(lèi)型之間的轉(zhuǎn)換
1、函數(shù) WideCharToMultiByte(),轉(zhuǎn)換 UNICODE 到 MBCS。使用范例:
2、函數(shù) MultiByteToWideChar(),轉(zhuǎn)換 MBCS 到 UNICODE。使用范例:
3、使用 ATL 提供的轉(zhuǎn)換宏。
| A2BSTR |
OLE2A |
T2A |
W2A |
| A2COLE |
OLE2BSTR |
T2BSTR |
W2BSTR |
| A2CT |
OLE2CA |
T2CA |
W2CA |
| A2CW |
OLE2CT |
T2COLE |
W2COLE |
| A2OLE |
OLE2CW |
T2CW |
W2CT |
| A2T |
OLE2T |
T2OLE |
W2OLE |
| A2W |
OLE2W |
T2W |
W2T |
上表中的宏函數(shù),其實(shí)非常容易記憶:
| 2 |
好搞笑的縮寫(xiě),to 的發(fā)音和 2 一樣,所以借用來(lái)表示“轉(zhuǎn)換為、轉(zhuǎn)換到”的含義。 |
| A |
ANSI 字符串,也就是 MBCS。 |
| W、OLE |
寬字符串,也就是 UNICODE。 |
| T |
中間類(lèi)型T。如果定義了 _UNICODE,則T表示W(wǎng);如果定義了 _MBCS,則T表示A |
| C |
const 的縮寫(xiě) |
使用范例:
使用 ATL 轉(zhuǎn)換宏,由于不用釋放臨時(shí)空間,所以使用起來(lái)非常方便。但是考慮到??臻g的尺寸(VC 默認(rèn)2M),使用時(shí)要注意幾點(diǎn):
1、只適合于進(jìn)行短字符串的轉(zhuǎn)換;
2、不要試圖在一個(gè)次數(shù)比較多的循環(huán)體內(nèi)進(jìn)行轉(zhuǎn)換;
3、不要試圖對(duì)字符型文件內(nèi)容進(jìn)行轉(zhuǎn)換,因?yàn)槲募叽缫话闱闆r下是比較大的;
4、對(duì)情況 2 和 3,要使用 MultiByteToWideChar() 和 WideCharToMultiByte();
六、VARIANT
C++、BASIC、Java、Pascal、Script......計(jì)算機(jī)語(yǔ)言多種多樣,而它們各自又都有自己的數(shù)據(jù)類(lèi)型,COM 產(chǎn)生目的,其中之一就是要跨語(yǔ)言(注3)。而 VARIANT 數(shù)據(jù)類(lèi)型就具有跨語(yǔ)言的特性,同時(shí)它可以表示(存儲(chǔ))任意類(lèi)型的數(shù)據(jù)。從C語(yǔ)言的角度來(lái)講,VARIANT 其實(shí)是一個(gè)結(jié)構(gòu),結(jié)構(gòu)中用一個(gè)域(vt)表示------該變量到底表示的是什么類(lèi)型數(shù)據(jù),同時(shí)真正的數(shù)據(jù)則存貯在 union 空間中。結(jié)構(gòu)的定義太長(zhǎng)了(雖然長(zhǎng),但其實(shí)很簡(jiǎn)單)大家去看 MSDN 的描述吧,這里給出如何使用的簡(jiǎn)單示例:
學(xué)生:我想用 VARIANT 表示一個(gè)4字節(jié)長(zhǎng)的整數(shù),如何做?
老師:VARIANT v; v.vt=VT_I4; v.lVal=100;
學(xué)生:我想用 VARIANT 表示布爾值“真”,如何做?
老師:VARIANT v; v.vt=VT_BOOL; v.boolVal=VARIANT_TRUE;
學(xué)生:這么麻煩?我能不能 v.boolVal=true; 這樣寫(xiě)?
老師:不可以!因?yàn)椤?
| 類(lèi)型 |
字節(jié)長(zhǎng)度 |
假值 |
真值 |
| bool |
1(char) |
0(false) |
1(true) |
| BOOL |
4(int) |
0(FALSE) |
1(TRUE) |
| VT_BOOL |
2(short int) |
0(VARIANT_FALSE) |
-1(VARIANT_TRUE) |
所以如果你 v.boolVal=true 這樣賦值,那么將來(lái) if(VARIANT_TRUE==v.boolVal) 的時(shí)候會(huì)出問(wèn)題(-1 != 1)。但是你注意觀察,任何布爾類(lèi)型的“假”都是0,因此作為一個(gè)好習(xí)慣,在做布爾判斷的時(shí)候,不要和“真值”相比較,而要與“假值”做比較。
學(xué)生:謝謝老師,你太牛了。我對(duì)老師的敬仰如滔滔江水,連綿不絕......
學(xué)生:我想用 VARIANT 保存字符串,如何做?
老師:VARIANT v; v.vt=VT_BSTR; v.bstrVal=SysAllocString(L"Hello,你好");
學(xué)生:哦......我明白了??墒沁@么操作真夠麻煩的,有沒(méi)有簡(jiǎn)單一些的方法?
老師:有呀,你可以使用現(xiàn)成的包裝類(lèi) CComVariant、COleVariant、_variant_t。比如上面三個(gè)問(wèn)題就可以這樣書(shū)寫(xiě):CComVariant v1(100),v2(true),v3("Hello,你好"); 簡(jiǎn)單了吧?!(注4)
學(xué)生:老師,我再問(wèn)最后一個(gè)問(wèn)題,我如何用 VARIANT 保存一個(gè)數(shù)組?
老師:這個(gè)問(wèn)題很復(fù)雜,我現(xiàn)在不能告訴你,我現(xiàn)在告訴你怕你印象不深......(注5)
學(xué)生:~!@#$%^&*()......暈!
七、小結(jié)
以上所介紹的內(nèi)容,是基本功,必須熟練掌握。先到這里吧,休息一會(huì)兒......更多精彩內(nèi)容,敬請(qǐng)關(guān)注《COM 組件設(shè)計(jì)與應(yīng)用(四)》
注1:在后續(xù)的 ISupportErrorInfo 接口中介紹。
注2:常見(jiàn)的數(shù)據(jù)類(lèi)型,請(qǐng)參考 IDL 文件的說(shuō)明。(別著急,還沒(méi)寫(xiě)那......嘿嘿)
注3:跨語(yǔ)言就是各種語(yǔ)言中都能使用COM組件。但啥時(shí)候能跨平臺(tái)呢?
注4:CComVariant/COlevariant/_variant_t 請(qǐng)參看 MSDN。
注5:關(guān)于安全數(shù)組 SafeArray 的使用,在后續(xù)的文章中討論。