COM組件設計與應用(三) 數據類型

作者:楊老師  原文:http://www.vckbase.net/document/viewdoc/?id=1488

一、前言
  上回書介紹了GUID、CLSID、IID和接口的概念。本回的重點是介紹 COM 中的數據類型。咋還不介紹組件程序的設計步驟呀?咳......彆着急,彆着急!孔子曰:“飯要一口一口地喫”;老子語:“心急吃不了熱豆腐”,孫子云:“走一步看一步吧” ...... 先掌握必要的知識,將來寫起程序來纔會得心應手也:-)
  走入正題之前,請大家牢牢記住一條原則:COM 組件是運行在分佈式環境中的。比如,你寫了一個組件程序(DLL或EXE),那麼使用者可能是在本機的某個進程內加載組件(INPROC_SERVER);也可能是從另一個進程中調用組件的進程(LOCAL_SERVER);也可能是在這臺計算機上調用地球那邊計算機上的組件(REMOTE_SERVER)。所以在理解和設計的時候,要時時刻刻想起這句話。快!拿出小本本,記下來!

二、HRESULT 函數返回值
  每個人在做程序設計的時候,都有他們各自的哲學思想。拿函數返回值來說,就有好多種形式。
 

函數 返回值 返回值信息
double sin(double)

浮點數值

計算正玄值
BOOL DeleteFile(LPCTSTR)

布爾值

文件刪除是否成功。如失敗,需要GetLastError()才能取得失敗原因
void * malloc(size_t)

內存指針

內存申請,如果失敗,返回空指針 NULL
LONG RegDeleteKey(HKEY,LPCTSTR)

整數

刪除註冊表項。0表示成功,非0失敗,同時這個值就反映了失敗的原因
UINT DragQueryFile(HDROP,UINT,LPTSTR,UINT)

整數

取得拖放文件信息。以不同的參數調用,則返回不同的含義:
一會兒表示文件個數,一會兒表示文件名長度,一會兒表示字符長度
......  ......

...

......  ......

  如此紛繁複雜的返回值,如此含義多變的返回值,使得大家在學習和使用的過程中,增加了額外的困難。好了,COM 的設計規範終於對他們進行了統一。組件API及接口指針中,除了IUnknown::AddRef()和IUnknown::Release()兩個函數外,其它所有的函數,都以 HRESULT 作爲返回值。大家想象一個組件的接口函數比如叫Add(),完成2個整數的加法運算,在C語言中,我們可以如下定義:

      long Add( long n1, long n2 )
      {
          return n1 + n2;
      }
  還記得剛纔我們說的原則嗎?COM 組件是運行在分佈式環境中的。也就是說,這個函數可能運行在“地球另一邊”的計算機上,既然運行在那麼遙遠的地方,就有可能出現服務器關機、網絡掉線、運行超時、對方不在服務區......等異常。於是,這個加法函數,除了需要返回運算結果以外,還應該返回一個值------函數是否被正常執行了。
      HRESULT Add( long n1, long n2, long *pSum )
      {
          *pSum = n1 + n2;
          return S_OK;
      }
  如果函數正常執行,則返回 S_OK,同時真正的函數運行結果則通過參數指針返回。如果遇到了異常情況,則COM系統經過判斷,會返回相應的錯誤值。常見的返回值有:
 
HRESULT 含義
S_OK 0x00000000 成功
S_FALSE 0x00000001 函數成功執行完成,但返回時出現錯誤
E_INVALIDARG 0x80070057 參數有錯誤
E_OUTOFMEMORY 0x8007000E 內存申請錯誤
E_UNEXPECTED 0x8000FFFF 未知的異常
E_NOTIMPL 0x80004001 未實現功能
E_FAIL 0x80004005 沒有詳細說明的錯誤。一般需要取得 Rich Error 錯誤信息(注1)
E_POINTER 0x80004003 無效的指針
E_HANDLE 0x80070006 無效的句柄
E_ABORT 0x80004004 終止操作
E_ACCESSDENIED 0x80070005 訪問被拒絕
E_NOINTERFACE 0x80004002 不支持接口


圖一、HRESULT 的結構

  HRESULT 其實是一個雙字節的值,其最高位(bit)如果是0表示成功,1表示錯誤。具體參見 MSDN 之"Structure of COM Error Codes"說明。我們在程序中如果需要判斷返回值,則可以使用比較運算符號;switch開關語句;也可以使用VC提供的宏:

      HRESULT hr = 調用組件函數;
      if( SUCCEEDED( hr ) ){...} // 如果成功
      ......
      if( FAILED( hr ) ){...} // 如果失敗
      ......

三、UNICODE
  計算機發明後,爲了在計算機中表示字符,人們制定了一種編碼,叫ASCII碼。ASCII碼由一個字節中的7位(bit)表示,範圍是0x00 - 0x7F 共128個字符。他們以爲這128個數字就足夠表示abcd....ABCD....1234 這些字符了。
  咳......說英語的人就是“笨”!後來他們突然發現,如果需要按照表格方式打印這些字符的時候,缺少了“製表符”。於是又擴展了ASCII的定義,使用一個字節的全部8位(bit)來表示字符了,這就叫擴展ASCII碼。範圍是0x00 - 0xFF 共256個字符。
  咳......說中文的人就是聰明!中國人利用連續2個擴展ASCII碼的擴展區域(0xA0以後)來表示一個漢字,該方法的標準叫GB-2312。後來,日文、韓文、阿拉伯文、臺灣繁體(BIG-5)......都使用類似的方法擴展了本地字符集的定義,現在統一稱爲 MBCS 字符集(多字節字符集)。這個方法是有缺陷的,因爲各個國家地區定義的字符集有交集,因此使用GB-2312的軟件,就不能在BIG-5的環境下運行(顯示亂碼),反之亦然。
  咳......說英語的人終於變“聰明”一些了。爲了把全世界人民所有的所有的文字符號都統一進行編碼,於是制定了UNICODE標準字符集。UNICODE 使用2個字節表示一個字符(unsigned shor int、WCHAR、_wchar_t、OLECHAR)。這下終於好啦,全世界任何一個地區的軟件,可以不用修改地就能在另一個地區運行了。雖然我用 IE 瀏覽日本網站,顯示出我不認識的日文文字,但至少不會是亂碼了。UNICODE 的範圍是 0x0000 - 0xFFFF 共6萬多個字符,其中光漢字就佔用了4萬多個。嘿嘿,中國人賺大發了:0)
  在程序中使用各種字符集的方法:

      const char * p = "Hello"; // 使用 ASCII 字符集
      const char * p = "你好"; // 使用 MBCS 字符集,由於 MBCS 完全兼容 ASCII,多數情況下,我們並不嚴格區分他們
      LPCSTR p = "Hello,你好"; // 意義同上
      
      const WCHAR * p = L"Hello,你好"; // 使用 UNICODE 字符集
      LPCOLESTR p = L"Hello,你好"; // 意義同上
      
      // 如果預定義了_UNICODE,則表示使用UNICODE字符集;如果定義了_MBCS,則表示使用 MBCS
      const TCHAR * p = _T("Hello,你好"); 
      LPCTSTR p = _T("Hello,你好"); // 意義同上
  在上面的例子中,T是非常有意思的一個符號(TCHAR、LPCTSTR、LPTSTR、_T()、_TEXT()...),它表示使用一種中間類型,既不明確表示使用 MBCS,也不明確表示使用 UNICODE。那到底使用哪種字符集那?嘿嘿......編譯的時候決定吧。設置條件編譯的方式是:VC6中,"Project\Settings...\C/C++卡片 Preprocessor definitions" 中添加或修改 _MBCS、_UNICODE;VC.NET中,"項目\屬性\配置屬性\常規\字符集"然後用組合窗進行選擇。使用 T 類型,是非常好的習慣,嚴重推薦!

四、BSTR
  COM 中除了使用一些簡單標準的數據類型外(注2),字符串類型需要特別重點地說明一下。還記得原則嗎?COM 組件是運行在分佈式環境中的。通俗地說,你不能直接把一個內存指針直接作爲參數傳遞給COM函數。你想想,系統需要把這塊內存的內容傳遞到“地球另一 邊”的計算機上,因此,我至少需要知道你這塊內存的尺寸吧?不然讓我如何傳遞呀?傳遞多少字節呀?!而字符串又是非常常用的一種類型,因此 COM 設計者引入了 BASIC 中字符串類型的表示方式---BSTR。BSTR 其實是一個指針類型,它的內存結構是:(輸入程序片段 BSTR p = ::SysAllocString(L"Hello,你好");斷點執行,然後觀察p的內存)


圖二、BSTR 內存結構

  BSTR 是一個指向 UNICODE 字符串的指針,且 BSTR 向前的4個字節中,使用DWORD保存着這個字符串的字節長度( 沒有含字符串的結束符)。因此係統就能夠正確處理並傳送這個字符串到“地球另一 邊”了。特別需要注意的是,由於BSTR的指針就是指向 UNICODE 串,因此 BSTR 和 LPOLESTR 可以在一定程度上混用,但一定要注意:
  有函數 fun(LPCOLESTR lp),則你調用 BSTR p=...; fun(p); 正確
  有函數 fun(const BSTR bstr),則你調用 LPCOLESTR p=...; fun(p); 錯誤!!!
有關 BSTR 的處理函數:
 
API 函數 說明
SysAllocString() 申請一個 BSTR 指針,並初始化爲一個字符串
SysFreeString() 釋放 BSTR 內存
SysAllocStringLen() 申請一個指定字符長度的 BSTR 指針,並初始化爲一個字符串
SysAllocStringByteLen() 申請一個指定字節長度的 BSTR 指針,並初始化爲一個字符串
SysReAllocStringLen() 重新申請 BSTR 指針

CString 函數

說明

AllocSysString() 從 CString 得到 BSTR
SetSysString() 重新申請 BSTR 指針,並複製到 CString 中

CComBSTR 函數

ATL 的 BSTR 包裝類。在 atlbase.h 中定義

Append()、AppendBSTR()、AppendBytes()、ArrayToBSTR()、BSTRToArray()、AssignBSTR()、Attach()、Detach()、Copy()、CopyTo()、Empty()、Length()、ByteLength()、ReadFromStream()、WriteToStream()、LoadString()、ToLower()、ToUpper()
運算符重載:!,!=,==,<,>,&,+=,+,=,BSTR
    太多了,但從函數名稱不能看出其基本功能。詳細資料,查看MSDN 吧。另外,左側函數,有很多是 ATL 7.0 提供的,VC6.0 下所帶的 ATL 3.0 不支持。
    由於我們將來主要用 ATL 開發組件程序,因此使用 ATL 的 CComBSTR 爲主。VC也提供了其它的包裝類 _bstr_t。


五、各種字符串類型之間的轉換
  1、函數 WideCharToMultiByte(),轉換 UNICODE 到 MBCS。使用範例:

      LPCOLESTR lpw = L"Hello,你好";
      size_t wLen = wcslen( lpw ) + 1;  // 寬字符字符長度,+1表示包含字符串結束符
      
      int aLen=WideCharToMultiByte(  // 第一次調用,計算所需 MBCS 字符串字節長度
		CP_ACP,
		0,
		lpw,  // 寬字符串指針
		wLen, // 字符長度
		NULL,
		0,  // 參數0表示計算轉換後的字符空間
		NULL,
		NULL);
	
      LPSTR lpa = new char [aLen];
	
      WideCharToMultiByte(
		CP_ACP,
		0,
		lpw,
		wLen,
		lpa,  // 轉換後的字符串指針
		aLen, // 給出空間大小
		NULL,
		NULL);

      // 此時,lpa 中保存着轉換後的 MBCS 字符串
      ... ... ... ...
      delete [] lpa;

    2、函數 MultiByteToWideChar(),轉換 MBCS 到 UNICODE。使用範例:
      LPCSTR lpa = "Hello,你好";
      size_t aLen = strlen( lpa ) + 1;
      
      int wLen = MultiByteToWideChar(
		CP_ACP,
		0,
		lpa,
		aLen,
		NULL,
		0);
      
      LPOLESTR lpw = new WCHAR [wLen];
      MultiByteToWideChar(
		CP_ACP,
		0,
		lpa,
		aLen,
		lpw,
		wLen);
      ... ... ... ...
      delete [] lpw;

    3、使用 ATL 提供的轉換宏。
 

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

上表中的宏函數,其實非常容易記憶:
2 好搞笑的縮寫,to 的發音和 2 一樣,所以借用來表示“轉換爲、轉換到”的含義。
A ANSI 字符串,也就是 MBCS。
W、OLE 寬字符串,也就是 UNICODE。
T 中間類型T。如果定義了 _UNICODE,則T表示W;如果定義了 _MBCS,則T表示A
C const 的縮寫

使用範例:

      #include <atlconv.h>
      
      void fun()
      {
          USES_CONVERSION;  // 只需要調用一次,就可以在函數中進行多次轉換
          
          LPCTSTR lp = OLE2CT( L"Hello,你好") );
          ... ... ... ...
          // 不用顯式釋放 lp 的內存,因爲
          // 由於 ATL 轉換宏使用棧作爲臨時空間,函數結束後會自動釋放棧空間。
      }
  使用 ATL 轉換宏,由於不用釋放臨時空間,所以使用起來非常方便。但是考慮到棧空間的尺寸(VC 默認2M),使用時要注意幾點:
    1、只適合於進行短字符串的轉換;
    2、不要試圖在一個次數比較多的循環體內進行轉換;
    3、不要試圖對字符型文件內容進行轉換,因爲文件尺寸一般情況下是比較大的;
    4、對情況 2 和 3,要使用 MultiByteToWideChar() 和 WideCharToMultiByte();
   
六、VARIANT
  C++、BASIC、Java、Pascal、Script......計算機語言多種多樣,而它們各自又都有自己的數據類型,COM 產生目的,其中之一就是要跨語言(注3)。而 VARIANT 數據類型就具有跨語言的特性,同時它可以表示(存儲)任意類型的數據。從C語言的角度來講,VARIANT 其實是一個結構,結構中用一個域(vt)表示------該變量到底表示的是什麼類型數據,同時真正的數據則存貯在 union 空間中。結構的定義太長了(雖然長,但其實很簡單)大家去看 MSDN 的描述吧,這裏給出如何使用的簡單示例:

學生:我想用 VARIANT 表示一個4字節長的整數,如何做?
老師:VARIANT v; v.vt=VT_I4; v.lVal=100;

學生:我想用 VARIANT 表示布爾值“真”,如何做?
老師:VARIANT v; v.vt=VT_BOOL; v.boolVal=VARIANT_TRUE;
學生:這麼麻煩?我能不能 v.boolVal=true; 這樣寫?
老師:不可以!因爲
 
類型 字節長度 假值 真值
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 這樣賦值,那麼將來 if(VARIANT_TRUE==v.boolVal) 的時候會出問題(-1 != 1)。但是你注意觀察,任何布爾類型的“假”都是0,因此作爲一個好習慣,在做布爾判斷的時候,不要和“真值”相比較,而要與“假值”做比較。
學生:謝謝老師,你太牛了。我對老師的敬仰如滔滔江水,連綿不絕......

學生:我想用 VARIANT 保存字符串,如何做?
老師:VARIANT v; v.vt=VT_BSTR; v.bstrVal=SysAllocString(L"Hello,你好");

學生:哦......我明白了。可是這麼操作真夠麻煩的,有沒有簡單一些的方法?
老師:有呀,你可以使用現成的包裝類 CComVariant、COleVariant、_variant_t。比如上面三個問題就可以這樣書寫:CComVariant v1(100),v2(true),v3("Hello,你好"); 簡單了吧?!(注4)

學生:老師,我再問最後一個問題,我如何用 VARIANT 保存一個數組?
老師:這個問題很複雜,我現在不能告訴你,我現在告訴你怕你印象不深......(注5)
學生:~!@#$%^&*()......暈!

七、小結
    以上所介紹的內容,是基本功,必須熟練掌握。先到這裏吧,休息一會兒......更多精彩內容,敬請關注《COM 組件設計與應用(四)》



注1:在後續的 ISupportErrorInfo 接口中介紹。
注2:常見的數據類型,請參考 IDL 文件的說明。(彆着急,還沒寫那......嘿嘿)
注3:跨語言就是各種語言中都能使用COM組件。但啥時候能跨平臺呢?
注4:CComVariant/COlevariant/_variant_t 請參看 MSDN。
注5:關於安全數組 SafeArray 的使用,在後續的文章中討論。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章