學習是一個探索的過程,但是在探索的過程中,我們要學會去總結,如果總是去探索,而不去歸納總結探索的成果物,那麼你的探索很可能就得不到別人的認可,也可能使自己進入一種迷茫的狀態中。最近的一階段,做的項目都是關於COM的,在與COM打交道時,各種類型的各種轉換是最讓人頭疼的,今天這篇博文主要就是總結在COM中各種頭疼的類型之間的關系與轉換。
BSTR、_bstr_t和CComBSTR
首先從定義的角度看看BSTR:
typedef /* [wire_marshal] */ OLECHAR *BSTR;
typedef WCHAR OLECHAR;
typedef wchar_t WCHAR;
我們能很清楚地看到,BSTR到底是什麼。
BSTR是COM中的數據類型,而_bstr_t是C++對BSTR進行的封裝。而在COM編程時,接口中定義的字符串類型都是BSTR類型,而使用BSTR類型是極其容易出錯的,同時,一不小心就有可能造成內存洩露。所以,在我們的這個項目開始之前,我就向項目組中的成員有如下建議:
1.在對接口進行實現時,從接口參數中傳遞過來的BSTR類型的變量,一定要在第一時刻將BSTR類型轉變成_bstr_t類型的變量,就是因為BSTR有隱藏的危險,同時當使用BSTR出現bug時,而這種bug而不一定好找;
2.在對接口進行實現時,接口中的BSTR字符串參數是out時,在接口實現的內部不要定義BSTR類型變量,而是定義_bstr_t類型的變量,進行操作,操作完成以後,然後在轉為BSTR類型的變量傳出去。
既然是這樣,就有人要問了,那麼BSTR存在的必要是什麼?
這個是由COM決定的,由於COM是跨系統及不同開發語言間實現互操作的技術,常規以NULL結尾的簡單字符串在COM組件間傳遞不太方便。所以,BSTR就這麼出現了。BSTR作為指針類型,標准的BSTR是一個有長度前綴和NULL結束符的OLECHAR數組。BSTR的前4個字節是一個表示字符串長度的前綴。BSTR長度域的值是字符串的字節數,但不包括字符串結束符。BSTR實際上包含的是Unicode串,所以字符數是字節數的一半。
所以,在能不使用BSTR的情況下,就盡量不要使用BSTR類型,而是使用對應的_bstr_t類型。為了處理BSTR,Microsoft提供了以下API供使用:
BSTR SysAllocString(constOLECHAR * psz);
INT SysReAllocString(BSTR* pbstr,constOLECHAR* psz);
BSTR SysAllocStringLen(constOLECHAR * strIn,UINTui);
INT SysReAllocStringLen(BSTR* pbstr,constOLECHAR* psz,unsignedintlen);
voidSysFreeString(BSTR bstrString);
UINTSysStringLen(BSTR);
UINTSysStringByteLen(BSTR bstr);
BSTR SysAllocStringByteLen(LPCSTRpsz,UINTlen);
現在對以上的API逐一的進行講解和應用,大家可以結合著MSDN,看這篇博文,至少MSDN講的比我這裡更詳細。
SysAllocString分配內存,並創建BSTR字符串;
SysReAllocString重新分配內存,並將第二個參數指定的OLECHAR同時放入新開辟的的內存中;
SysAllocStringLen分配內存,將第一個參數指定的字符串的前ui(第二個參數指定的字符個數)個數放入開辟的內存中;
SysReAllocStringLen,經過上面兩個API的講解,這個API的就不需要更多的講解了;
SysFreeString釋放由以上的API函數開辟的內存;
SysStringLen表示的是BSTR的字符個數;
SysStringByteLen獲得BSTR字符串表示的字節數,也就是BSTR的前4個字節表示的內容;
SysAllocStringByteLen使用的是ANSI string進行創建BSTR對象,盡量少用該API。
_bstr_t對BSTR的封裝
_bstr_t是對BSTR的封裝,封裝的好處就是讓程序員去干更少的事情,同時也較少錯誤的出現。
對於_bstr_t類,其中的成員函數也不是很多,很簡單,但是在使用_bstr_t時,最讓人迷惑的就是_bstr_t中內存的管理了。對於這個問題,下面提供一個簡單的DEMO(注:該DEMO來源自MSDN)
大家把這個例子搞懂,就沒有問題了。
#include <comdef.h>
#include <stdio.h>
int main()
{
// creates a _bstr_t wrapper
_bstr_t bstrWrapper;
// creates BSTR and attaches to it
bstrWrapper = "some text";
wprintf_s(L"bstrWrapper = %s\n",
static_cast<wchar_t*>(bstrWrapper));
// bstrWrapper releases its BSTR
BSTR bstr = bstrWrapper.Detach();
wprintf_s(L"bstrWrapper = %s\n",
static_cast<wchar_t*>(bstrWrapper));
// "some text"
wprintf_s(L"bstr = %s\n", bstr);
bstrWrapper.Attach(SysAllocString(OLESTR("SysAllocedString")));
wprintf_s(L"bstrWrapper = %s\n",
static_cast<wchar_t*>(bstrWrapper));
// assign a BSTR to our _bstr_t
bstrWrapper.Assign(bstr);
wprintf_s(L"bstrWrapper = %s\n",
static_cast<wchar_t*>(bstrWrapper));
// done with BSTR, do manual cleanup
SysFreeString(bstr);
// resuse bstr
bstr= SysAllocString(OLESTR("Yet another string"));
// two wrappers, one BSTR
_bstr_t bstrWrapper2 = bstrWrapper;
*bstrWrapper.GetAddress() = bstr;
// bstrWrapper and bstrWrapper2 do still point to BSTR
bstr = 0;
wprintf_s(L"bstrWrapper = %s\n",
static_cast<wchar_t*>(bstrWrapper));
wprintf_s(L"bstrWrapper2 = %s\n",
static_cast<wchar_t*>(bstrWrapper2));
// new value into BSTR
_snwprintf_s(bstrWrapper.GetBSTR(), 100, bstrWrapper.length(),
L"changing BSTR");
wprintf_s(L"bstrWrapper = %s\n",
static_cast<wchar_t*>(bstrWrapper));
wprintf_s(L"bstrWrapper2 = %s\n",
static_cast<wchar_t*>(bstrWrapper2));
}
CComBSTR對BSTR的封裝
CComBSTR是ATL對BSTR的封裝,可以看到C++,ATL都對BSTR進行了封裝,所以,BSTR使用起來是多麼的麻煩的。雖然CComBSTR對BSTR進行了封裝,但是,CComBSTR使用起來也是很麻煩的,陷阱也是很多的,一不留神,就掉進陷阱了,所以,這就是我一開始就沒有推薦使用CComBSTR的原因。對於MSDN上,關於CComBSTR的講解很詳細,大家可以參考MSDN上的DEMO,這裡就不做講解了。
VARIANT與_variant_t
使用VARIANT來傳遞參數意味著非強類型語言(例如VBScript)能夠調用使用強類型語言(C++)實現的方法。VARIANT是一種特殊的數據類型,除了定長string數據以及用戶定義類型外,可以包含任何種類的數據,當我們在MSDN查詢VARIANT時,我們就會被VARIANT的定義嚇著,確實很嚇人的,不仔細看,你還真的看不明白,這是個什麼東西。
在VARIANT數據結構中包含兩個域(如果不考慮保留的域),vt域描述了第二個域的數據類型。為了使多種類型能夠在第二個域中出現,所以VARIANT定義了一個聯合結構。所以,第二個域的名稱隨著vt域中輸入值的不同而改變。在MSDN中,用於指定vt域值情況的常量在聯合的定義中以每一行的注釋形式給出。在實際編程時,這些我們可能記不住,可以去查看MSDN。
下面是一個簡單的例子:
short iVal = 20;
VARIANT varParm;
varParm.vt = VT_I2;
varParm.iVal = iVal;
上面的例子,簡單明了的說明了VARIANT的簡單使用。雖然有的時候,VARIANT使用起來是很簡單的,但是有些問題,我們還必須要去注意:
1.建立VARIANT變量時,必須使用VariantInit進行初始化;
2.對於VT_UI1, VT_I2, VT_I4, VT_R4, VT_R8, VT_BOOL, VT_ERROR, VT_CY, VT_DECIMAL, 和VT_DATE這些類型,數據的值是直接儲存在VARIANT結構中的,當VARIANT的類型發生變化時,指向這些數據的指針會變得無效。例如: VARIANT varParam;
::VariantInit(&varParam);
short iVal = 20;
varParam.vt = VT_I2;
varParam.iVal = iVal;
short *pVal = &varParam.iVal;
varParam.vt = VT_I4;
varParam.lVal = 200;
此時,*pVal指向的值與你期望的可能是不一樣的。
3.對於VT_BYREF | any type數據類型,而VARIANT只是擁有這些數據指向內存的指針,而內存的釋放需要由函數的調用者負責。例如: VARIANT varParam;
::VariantInit(&varParam);
int *p = new int;
*p = 10;
varParam.vt = VT_BYREF | VT_I4;
varParam.plVal = (long*)p;
delete p; // If you donnot delete the memory, this may cause memory leak
4.對於VT_BSTR類型,在VARIANT中的字符串必須要用SysAllocString進行分配內存,當釋放時,或者VARIANT的類型發生改變時,都需要調用SysFreeString進行內存釋放,否則就會發生內存洩露;
5.對於VT_ARRAY | any type類型,這個規則和VT_BSTR是類似的,在VARIANT中的數組必須使用SafeArrayCreate進行開辟內存空間,然後必須使用SafeArrayDestroy進行內存空間的釋放;
6.對於VT_DISPATCH和VT_UNKNOWN,我們考慮的更多的就是引用計數器的增加與減少了,是的,在進行賦值時需要進行引用計數的增加,釋放時,則需要對應的減少。
_variant_t
_variant_t是對VARIANT的封裝,_variant_t類管理內存,而不用手動的去調用VariantInit和VariantClear。_variant_t方法提供的參數不是很多,由於封裝的比較好,MSDN講解的比較詳細,沒有什麼需要注意的,所以在這裡就不錯講解了。
SAFEARRAY的使用
SAFEARRAY的主要目的是用於automation中的數組型參數的傳遞,我們都知道,在網絡環境中,數組是不能直接傳遞的,所以我們必須將數組封裝成SAFEARRAY類型,這樣才能進行傳遞,在COM編程時,SAFEARRAY類型是可以存放在VARIANT類型中,指定vt為VT_ARRAY|*或者VT_BYREF|VT_ARRAY。對於SAFEARRAY,說白了,就是普通的數組,添加了一些額外的說明,當我第一次遇到這個類型時,也是有點恐懼的,後來,用慣了,也就無所謂了。SAFEARRAY單獨用的時候很少,就像我前面說的,一般都是搭配著VARIANT一起使用,指定vt類型以後,parray成員就是指向SAFEARRAY的指針。SAFEARRAY中元素的類型可以是VARIANT能封裝的任何類型,包括VARIANT類型本身。
1.使用SafeArrayAllocDescriptor在stack上創建SAFEARRAY // Create the SAFEARRAY on the stack
long nData[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
SAFEARRAY *pArray = NULL;
HRESULT hr = SafeArrayAllocDescriptor(1, &pArray);
pArray->cbElements = sizeof(nData[0]);
pArray->cDims = 1;
pArray->rgsabound[0].lLbound = 0;
pArray->rgsabound[0].cElements = 10;
pArray->pvData = nData;
pArray->fFeatures = FADF_AUTO | FADF_FIXEDSIZE;
// Access the SAFEARRAY
long *pValue = NULL;
SafeArrayAccessData(pArray, (void**)&pValue);
long low, high;
low = 0;
high = 0;
hr = SafeArrayGetUBound(pArray, 1, &high);
hr = SafeArrayGetLBound(pArray, 1, &low);
SafeArrayUnaccessData(pArray);
SafeArrayDestroy(pArray);
2.使用SafeArrayAllocDescriptor配合SafeArrayAllocData在Heap上創建一維SAFEARRAY long nData[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
SAFEARRAY *pArray = NULL;
HRESULT hr = SafeArrayAllocDescriptor(1, &pArray);
pArray->cbElements = sizeof(nData[0]);
pArray->cDims = 1;
pArray->rgsabound[0].lLbound = 0;
pArray->rgsabound[0].cElements = 10;
hr = SafeArrayAllocData(pArray);
// Put data
long *pData = NULL;
SafeArrayAccessData(pArray, (void**)&pData);
long low, high;
low = 0;
high = 0;
hr = SafeArrayGetLBound(pArray, 1, &low);
hr = SafeArrayGetUBound(pArray, 1, &high);
long size = high - low + 1;
for (long i = 0; i < size; ++i)
{
pData[i] = nData[i];
}
SafeArrayUnaccessData(pArray);
// Access the SAFEARRAY
long *pValue = NULL;
hr = SafeArrayAccessData(pArray, (void**)&pValue);
hr = SafeArrayGetLBound(pArray, 1, &low);
hr = SafeArrayGetUBound(pArray, 1, &high);
hr = SafeArrayUnaccessData(pArray);
hr = SafeArrayDestroy(pArray);
3.使用SafeArrayAllocDescriptor配合SafeArrayAllocData在Heap上創建二維SAFEARRAY // Create SAFEARRAY
SAFEARRAY *pArray = NULL;
HRESULT hr = SafeArrayAllocDescriptor(2, &pArray);
pArray->rgsabound[0].lLbound = 0;
pArray->rgsabound[0].cElements = 3;
pArray->rgsabound[1].lLbound = 0;
pArray->rgsabound[1].cElements = 3;
pArray->cbElements = sizeof(long);
hr = SafeArrayAllocData(pArray);
// Put data
long lDimension[2];
long x = 1;
for (long i = 0; i < 3; ++i)
{
lDimension[1]=0; // Row
lDimension[0]=i; // Column
SafeArrayPutElement(pArray, lDimension, (void*)&x);
++x;
}
for (long i = 0; i < 3; ++i)
{
lDimension[1]=1;
lDimension[0]=i;
SafeArrayPutElement(pArray, lDimension, (void*)&x);
++x;
}
long y = 0;
lDimension[1]=1;
lDimension[0]=2;
hr = SafeArrayGetElement(pArray, lDimension, (void*)&y);
SafeArrayDestroy(pArray);
二位SAFEARRAY數組使用的時候,下標一定要注意,因為采用的是列主序的方式,即 lDimension[1]代表行,lDimension[0]代表列。
4.使用SafeArrayCreate在堆上創建一維數組 SAFEARRAYBOUND Bound[1];
Bound[0].lLbound = 0;
Bound[0].cElements = 10;
SAFEARRAY *pArray = SafeArrayCreate(VT_I4, 1, Bound);
long *pData = NULL;
HRESULT hr = SafeArrayAccessData(pArray, (void**)&pData);
long low, high;
low = 0;
high = 0;
SafeArrayGetLBound(pArray, 1, &low);
SafeArrayGetUBound(pArray, 1, &high);
long size = high - low + 1;
for (long i = low; i < size; ++i)
{
pData[i] = i;
cout<<pData[i]<<endl;
}
SafeArrayUnaccessData(pArray);
SafeArrayDestroy(pArray);
5.使用SafeArrayCreate在堆上創建二維數組 // Create SAFEARRAY
SAFEARRAYBOUND Bound[2];
Bound[0].lLbound = 0;
Bound[0].cElements = 10;
Bound[1].lLbound = 0;
Bound[1].cElements = 10;
SAFEARRAY *pArray = SafeArrayCreate(VT_I4, 2, Bound);
long Demen[2];
for (int i = 0; i < 2; ++i)
{
for (int j = 0; j < 10; ++j)
{
Demen[1] = i;
Demen[0] = j;
long value = i * j;
SafeArrayPutElement(pArray, Demen, &value);
}
}
// Visit the SAFEARRAY
int iDims = SafeArrayGetDim(pArray);
long low = 0;
long high = 0;
int value = 0;
for (int i = 0; i < iDims; ++i)
{
SafeArrayGetLBound(pArray, i + 1, &low);
SafeArrayGetUBound(pArray, i + 1, &high);
long size = high - low + 1;
for (int j = low; j < size; ++j)
{
Demen[1] = i;
Demen[0] = j;
SafeArrayGetElement(pArray, Demen, &value);
cout<<value<<" ";
}
cout<<endl;
}
SafeArrayDestroy(pArray);
6.使用SafeArrayCreateEx創建包含結構的一維數組
使用SAFEARRAY傳遞自定義結構是一項常用的技術,需要注意的一點就是自定義的結構需要要有自己的GUID,這必須在IDL文件中定義。同時還必須要使用IRecordInfo接口,該接口將和數組一起傳遞出去,IRecordInfo接口內部記錄了自定義結構的描述信息。
訪問SAFEARRAY
訪問SAFEARRAY的方法大體上有兩種:
1.使用SafeArrayAccessData方法;
2.使用SafeArrayGetElement和SafeArrayPutElement方法。
關於這兩種方法,在上面的例子中都有涉及。
總結
這裡總結的數據類型是在進行COM開發時經常使用的,而且還非常容易出錯,這裡對這些數據類型進行了全面的總結,希望對大家有幫助。