深入解析ATL第二版(ATL8.0)筆記--(2.3節)

 
深入解析ATL第二版(ATL8.0)筆記(2.3)
――CComBSTR
整理:賴儀靈
聲明:版權歸原作者所有,轉載時必須保留此部分文字。
 
    CComBSTR是非常有用的ATL工具類,它封裝了BSTR數據類型。CComBSTR類唯一的數據成員是變量m_str
class CComBSTR {
public:
    BSTR    m_str;
};
 
2.3.1   構造函數和析構函數
    CComBSTR的構造函數有八個。默認構造函數把m_str初始化爲NULLNULL也是有效的空BSTR串。析構函數用SysFreeString釋放m_strSysFreeString傳入參數如果是NULL,函數直接返回,這樣能確保空對象的析構不會有問題。
CComBSTR() { m_str = NULL; }
~CComBSTR() { ::SysFreeString(m_str); }
    在後面部分,我們將學習到CComBSTR的許多方法。其中的優點就是析構函數會在適當的時候釋放BSTR,因此我們不必釋放BSTR
    最常用的CComBSTR構造函數之一可能就是用指向NULL終止的OLECHAR字符數組,也就是LPOLECHAR
CComBSTR(LPOLECHAR pSrc) {
    if (pSrc == NULL) m_str = NULL;
    else {
        m_str = ::SysAllocString(pSrc);
        if (m_str == NULL)
            Atlthrow(E_OUTOFMEMORY);
    }
}
下面的類似代碼就將調用上面的構造函數:
CComBSTR str1(OLECHAR(“This is a string of OLECHARs”));
    前面的構造函數會拷貝字符串的內容直到找到字符串的終止字符NULL。如果希望拷貝更少的字符串,比如字符串前綴,或者你希望拷貝嵌有NULL字符的字符串,就必須顯示的指定拷貝的字符個數。此時,調用下面的構造函數:
CComBSTR(int nSize, LPOLESTR sz);
    此構造函數創建nSize大小的BSTR。從sz拷貝指定個數的字符,包括嵌入的NULL,最後再追加一個NULL。當szNULL時,SysAllocStringLen忽略拷貝步驟,創建指定大小未初始化的BSTR字符串。我們可以用如下形式的代碼調用前面的構造函數:
// str2 contains "This is a string"
CComBSTR str2 (16, OLESTR ("This is a string of OLECHARs"));
// Allocates an uninitialized BSTR with room for 64 characters
CComBSTR str3 (64, (LPCOLESTR) NULL);
// Allocates an uninitialized BSTR with room for 64 characters
CComBSTR str4 (64);
    CComBSTR類爲上面的str4例子提供了特殊的構造函數,調用它不需要提供NULL參數,str4例子說明了它的用法。下面是構造函數實現:
CComBSTR(int nSize) {
    m_str = ::SysAllocStringLen(NULL, nSize);
}
    BSTR有一個比較特殊的語義:NULL指針可以表示合法的空BSTR字符串。比如,Visual Basic認爲一個NULL BSTR等於一個指向空字符串的指針,字符串的長度是0,它的第一個字符就是NUL字符。爲了象徵性運用它,Visual Basic認爲IF P=””正確,P是等於NULLBSTRSysStringLen函數適當的實現了檢測,CComBSTR提供了Length方法的包裝:
unsigned int Length() const { return ::SysStringLen(m_str); }
    我們也可以使用下面的拷貝構造函數從一個相等的已經存在並初始化的CComBSTR對象來創建、初始化另一個CComBSTR對象。
CComBSTR(const CComBSTR& src) {
    m_str = src.Copy();
}
在下面的代碼中,創建了str5變量,它調用前面的拷貝構造函數以初始化它的每個對象。
CComBSTR str1(OLESTR(“This is a string of OLECHARs”));
CComBSTR str5 = str1;
    注意前面的拷貝構造函數調用了源CComBSTR對象的Copy方法。Copy方法做了一個字符串的拷貝並返回一個新的BSTR。因爲Copy方法根據已經存在的BSTR字符串的長度分配內存,然後再拷貝指定長度的字符串內容。所以Copy方法可以適當的拷貝嵌有NUL字符的BSTR字符串。
BSTR Copy() const {
    if ( !*this) { return NULL: }
    return ::SysStringByteLen((char*)m_str, ::SysStringByteLen(m_str));
}
    另兩個構造函數從LPCSTR字符串初始化CComBSTR對象。只有單個參數的構造函數期待一個NUL終止的LPCSTR字符串。帶兩個參數的構造函數允許指定LPCSTR字符串的長度。下面的兩個構造函數期待ANSI字符,創建一個包含相等的OLECHAR字符的BSTR字符串。
CComBSTR (LPCSTR pSrc) {
    m_str = A2WBSTR(pSrc);
}
CComBSTR(int nSize, LPCSTR sz) {
    m_str = A2WBSTR(sz, nSize);
}
    最後一個構造函數也比較奇怪。它期待一個GUID參數,然後產生一個包含描述GUID的字符串。
CComBSTR(REFGUID src);
    在組件註冊建立字符串時此構造函數非常有用。很多時候,我們需要把一個描述GUID的字符串內容寫入註冊表。下面是使用此構造函數的示例代碼:
// Define a GUID as a binary constant
static const GUID GUID_Sample = { 0x8a44e110, 0xf134, 0x11d1,
    { 0x96, 0xb1, 0xBA, 0xDB, 0xAD, 0xBA, 0xDB, 0xAD } };
// Convert the binary GUID to its string representation
CComBSTR str6 (GUID_Sample) ;
// str6 contains "{8A44E110-F134-11d1-96B1-BADBADBADBAD}"
 
2.3.2   賦值運算符
    CComBSTR類定義了三個賦值運算符。第一個用另一個不同的CComBSTR對象來初始化一個CComBSTR對象。第二個用LPCOLESTR指針初始化CComBSTR對象。第三個用LPCSTR指針初始化CComBSTR對象。下面的operator = ()方法用另一個CComBSTR對象來初始化CComBSTR對象。
CComBSTR& operator = (const CComBSTR& src) {
    if (m_str != str.m_str) (
        ::SysFreeString(m_str);
        m_str = src.Copy();
        if (!! src && !*this) { AtlThrow(E_OUTOFMEMORY); }
    }
    return *this;
}
    注意上面的賦值運算符使用Copy方法,對指定的CComBSTR實例做原樣拷貝,本節稍後討論。如下的代碼就會調用此運算符:
CComBSTR str1(OLESTR(“This is a string of OLECHARs”));
CComBSTR str7;
str7 = str1;    // str7 contains “This is a string of OLECHARs”
str7 = str1;    // This is a NOP. Assignment operator detects this case
 
第二個operator = ()方法用指向以NUL字符結束的字符串的LPCOLESTR指針初始化CComBSTR對象。
CComBSTR& operator = (LPCOLESTR pSrc) {
    if (pSrc != m_str) {
        ::SysFreeString(m_str);
        if (pSrc != NULL) {
            m_str = ::SysAllocString(pSrc);
            if (!*this) { AtlThrow(E_OUTOFMEMORY); }
        }
        else { m_str = NULL; }
    }
    return *this;
}
    注意此賦值運算符使用SysAllocString函數來分配指定LPCOLESTR參數的BSTR拷貝。下面的示例代碼可以調用此函數:
CComBSTR str8;
str8 = OLESTR(“This is a string of OLECHARs”);
    我們很容易濫用這個賦值操作符。下面就是一種錯誤的用法:
// BSTR bstrInput contains "This is part one/0and here's part two"
CComBSTR str10 ;
str10 = bstrInput; // str10 now contains "This is part one"
    上面這種情況我們應該用函數AssignBSTR。它的實現與operator=(LPCOLESTR)很相似。除了內部是用SysAllocStringByteLen
HRESULT AssignBSTR(const BSTR bstrSrc){
    HRESULT hr = S_OK;
    if (m_str != bstrSrc){
        ::SysFreeString(m_str);
        if (bstrSrc != NULL){
            m_str = ::SysAllocStringByteLen((char*)bstrSrc,
                ::SysStringByteLen(bstrSrc));
            if (!*this) { hr = E_OUTOFMEMORY; }
        } else {
            m_str = NULL;
        }
    }
    return hr;
}
所以我們的代碼就可以修改爲
CComBSTR str10;
str10.AssignBSTR(bstrInput);
 
第三個operator=操作符以LPCSTR作參數。指向NUL結束的字符串。函數把ANSI字符轉爲Unicode。然後再創建一個BSTR包含UNICODE字符串。
CComBSTR& operator=(LPCSTR pSrc) {
    ::SysFreeString(m_str);
    m_str = A2WBSTR(pSrc);
    if (!*this && pSrc != NULL) { AtlThrow(E_OUTOFMEMORY); }
    return *this;
}
    最後兩個賦值方法是重載的LoadString.
bool LoadString(HINSTANCE hInst, UINT nID);
bool LoadString(UINT nID);
    前一個從指定的模塊加載字符串資源。後者是從當前的全局變量模塊_AtlBaseModule加載。
 
2.3.3   CComBSTR操作符
 
    訪問封裝在CComBSTR類的BSTR字符串有四種方法。操作符BSTR()允許把CComBSTR當原始指針使用。需要把CComBSTR對象顯式、隱式轉化爲BSTR都可調用此方法。
operator BSTR() const { return m_str; }
    通常,我們可以把一個CComBSTR對象傳遞給一個需要BSTR參數的函數。
    當我們取CComBSTR對象的地址時,operator&()方法返回m_str變量的地址。使用CComBSTR對象地址時要小心。因爲返回的是內部BSTR變量的地址。我們可以覆蓋原來的值而不需要釋放。這樣會內存泄漏。如果我們在工程中定義宏ATL_CCOMBSTR_ADDRESS_OF_ASSERT,這種錯誤就會拋出異常。利用這個操作符,在需要BSTR指針的地方我們可以傳遞CComBSTR對象。
    CopyTo函數拷貝內部的BSTR到指定位置。我們必須顯式的調用SysStringFree來釋放字符串。我們需要返回字符串拷貝時可使用此方法:
STDMETHODIMP SomeClass::get_Name (/* [out] */ BSTR* pName) {
    // Name is maintained in variable m_strName of type CComBSTR
    return m_strName.CopyTo (pName);
}
    Detach函數返回CComBSTR內部的BSTR內容。並把對象清空。這樣可防止析構函數釋放字符串。我們也需要在外部調用SysStringFree釋放字符串。
BSTR Detach() { BSTR s = m_str; m_str = NULL; return s; }
    Attach方法執行與Detach相反的工作。它把BSTR依附在空CComBSTR對象上,如果對象不空,先進行釋放:
void Attach(BSTR src) {
    if (m_str != src) {
        ::SysFreeString(m_str);
        m_str = src;
    }
}
    調用Attach函數要小心,我們必須擁有BSTR的所有權,因爲CComBSTR最後會釋放這個BSTR。比如下面的代碼就是錯誤的:
STDMETHODIMP SomeClass::put_Name (/* [in] */ BSTR bstrName) {
    // Name is maintained in variable m_strName of type CComBSTR
    m_strName.Attach (bstrName); // Wrong! We don't own bstrName
    return E_BONEHEAD;
}
    Attach函數通常在我們被給予BSTR所有權,並且我們希望用CComBSTR來管理BSTR時使用:
BSTR bstrName;
pObj->get_Name (&bstrName); // We own and must free the raw BSTR
CComBSTR strName;
strName.Attach(bstrName); // Attach raw BSTR to the object
    我們可調用Empty函數顯式釋放CComBSTR對象的字符串。因爲內部是調用SysStringFree函數,所以我們可以對空對象調用Empty函數:
void Empty() { ::SysStringFree(m_str); m_str = NULL; }
    CComBSTR還提供了另外兩個方法實現BSTRSAFEARRAY的轉化。
HRESULT BSTRToArray(LPSAFEARRAY * ppArray) {
    Return VectorFromBstr(m_str, ppArray);
}
HRESULT ArrayToBSTR(const SAFEARRAY * pSrc) {
    ::SysStringFree(m_str);
    return BstrFromVector((LPSAFEARRAY)pSrc, &m_str);
}
    其實這兩個方法只是WIN32函數BstrFromVectorVectorFromBstr的封裝。BSTRToArray把字符串的每個字符賦值爲調用者提供的一維SAFEARRAY的元素。注意釋放SAFEARRAY是調用者的責任。ArrayToBSTR相反:接受一個一維SAFEARRAY指針建立一個BSTR。注意SAFEARRAY裏只能是char類型元素。否則函數返回類型匹配錯誤。
 
2.3.4   CComBSTR的字符串連接
 
    連接CComBSTR對象和指定字符串的方法有八個。六個重載的Append,一個AppendBSTR和一個operator+=()方法。
HRESULT Append(LPCOLESTR lpsz, int nLen);
HRESULT Append(LPCOLESTR lpsz);
HRESULT Append(LPCSTR);
HRESULT Append(char ch);
HRESULT Append(wchar_t ch);
HRESULT Append(const CComBSTR& bstrSrc);
CComBSTR& operator+=(const CComBSTR& bstrSrc);
HRESULT AppendBSTR(BSTR p);
    Append(LPCOLESTR lpsz, int nLen);方法計算原來的字符串加上nLen的長度,並分配空間。把原來的字符串內容拷貝到新空間,然後從lpsz指定的字符串拷貝nLen個字符到分配的空間裏。並釋放原來的字符串。用新BSTR對象賦值。
    其他的重載Append函數在內部都是調用這個函數的。區別只是在於獲取字符串和長度值的方法不同。
    注意,當我們用BSTR作參數調用Append函數時,實際上調用的是Append(LPCOLESTR). 因爲編譯器把BSTR當作OLECHAR *參數。因爲函數從BSTR拷貝字符串直到第一個NUL字符。當我們希望拷貝整個BSTR時,應該使用AppendBSTR方法。
    另一個附加方法可以追加包含二進制數據的數組:
HRESULT AppendBytes(const char * lpsz, int nLen);
    AppendBytes並不執行從ANSIUNICODE的轉換。而是用SysAllocStringByteLen分配nLen字節大小的BSTR。然後把結果追加到CComBSTR對象。
 
2.3.5   字符大小寫轉換
    大小寫的轉換可由方法ToLowerToUpper完成。在Unicode編譯時,內部用CharLowerBuff API函數。ANSI編譯時,字符串先轉爲MBCS然後調用CharLowerBuff. 結果被轉回UNICODE存儲在新分配的BSTRm_str的任何內容在覆蓋前都被SysStringFree釋放。
HRESULT ToLower(){
    if (m_str != NULL) {
#ifdef _UNICODE
        // Convert in place
            CharLowerBuff(m_str, Length());
#else
        UINT _acp = _AtlGetConversionACP();
         int nRet = WideCharToMultiByte(_acp, 0, m_str, Length(),
             pszA, _convert, NULL, NULL);
         CharLowerBuff(pszA, nRet);
         nRet = MultiByteToWideChar(_acp, 0, pszA, nRet, pszW, _convert);
         BSTR b = ::SysAllocStringByteLen((LPCSTR)(LPWSTR)pszW,
            nRet * sizeof(OLECHAR));
            if (b == NULL)
                    return E_OUTOFMEMORY;
        SysFreeString(m_str);
        m_str = b;
#endif
    }
    return S_OK;
}
    注意這些方法可以完全實現大小寫轉換,即使原始字符串有內嵌NUL字符。但是同樣也要注意轉換也可能有損,如果本地代碼頁不包括與原始UNICODE字符相等的字符。它就不能被轉換。
 
2.3.6   CComBSTR比較操作
    CComBSTR對象是空時operator!()返回true,否則返回false.
bool operator !() const { return (m_str == NULL); }
    有四個operator<()方法,四個operator>(),五個operator==()operator!=()方法。另外還有一個重載operator==()方法處理與NULL比較的特殊情形。在這些方法中的代碼大同小異,因爲我們現在只討論operator<()方法,這些說明也同樣適用於operator>()operator==()方法。
    這些操作內部調用了VarBstrCmp函數,這與前一版本的ATL不同,舊版本不能正確比較包含有內嵌NUL字符的CComBSTR對象,這些新的操作法大部分時候能正確處理這種比較。因此,下面的代碼可以按期望的正常工作。本節稍後我們會討論用內嵌NULs初始化CComBSTR對象。
BSTR bstrIn1 = SysAllocStringLen(OLESTR("Here's part 1/0and here's part 2"), 35);
BSTR bstrIn2 = SysAllocStringLen(OLESTR("Here's part 1/0and here’s part 2"), 35);
CComBSTR bstr1(::SysStringLen(bstrIn1), bstrIn1);
CComBSTR bstr2(::SysStringLen(bstrIn2), bstrIn2);
bool b = bstr1 == bstr2; // correctly returns false
    operator<()方法的第一個重載版本里,比較與一個CComBSTR參數進行。
bool operator<(const CComBSTR& bstrSrc) const {
    return VarBstrCmp(m_str, bstrSrc.m_str, LOCALE_USER_DEFAULT,0) == VARCMP_LT;
}
    operator<()方法的第二個重載版本里,比較與一個LPCSTR參數進行。LPCSTR與內部的寬字符BSTR類型不同。因此,實現時通過構造一個臨時CComBSTR對象後再調用第一個版本的operator<()
bool operator>(LPCSTR pszSrc) const {
    CComBSTR bstr2(pszSrc);
    return operator>(bstr2);
}
    第三個版本的參數是LPCOLESTR,實現與前一版本類似:
bool operator>(LPCOLESTR pszSrc) const {
    CComBSTR bstr2(pszSrc);
    return operator>(bstr2);
}
    第四個版本的參數是LPOLESTR,它也是簡單調用前一種實現:
bool operator>(LPOLESTR pszSrc) const {
    return operator>((LPCOLESTR)(pszSrc));
}
 
2.3.6   CComBSTR的持續化支持
    CComBSTR類的最後兩個方法實現BSTR字符串與流之間的讀寫。WriteToStream方法首先寫入一個ULONG類型的值表示BSTR字符串的個數。然後接着就寫入BSTR字符串。注意方法沒沒有寫入標籤值說明字節順序。因此,與流數據的大多數情況一樣,CComBSTR通常以特定硬件結構格式把字符串寫入到流。
HRESULT WriteToStream(IStream* pStream) {
    ATLASSERT(pStream != NULL);
    if(pStream == NULL)
        return E_INVALIDARG;
    ULONG cb;
    ULONG cbStrLen = ULONG(m_str ? SysStringByteLen(m_str)+sizeof(OLECHAR) : 0);
    HRESULT hr = pStream->Write((void*) &cbStrLen, sizeof(cbStrLen), &cb);
    if (FAILED(hr))
        return hr;
    return cbStrLen ? pStream->Write((void*) m_str, cbStrLen, &cb) : S_OK;                                        
}
    ReadFromStream方法從指定流讀取一個ULONG值。然後分配適當的空間,再讀取BSTR字符串。調用ReadFromStream方法CComBSTR對象必須爲空。否則,DEBUG時會有ASSERT錯誤,Release時發生內存泄漏。
HRESULT ReadFromStream(IStream* pStream) {
    ATLASSERT(pStream != NULL);
    ATLASSERT(!*this); // should be empty
    ULONG cbStrLen = 0;
    HRESULT hr = pStream->Read((void*) &cbStrLen, sizeof(cbStrLen), NULL);                               
    if ((hr == S_OK) && (cbStrLen != 0)) {
        //subtract size for terminating NULL which we wrote out
        //since SysAllocStringByteLen overallocates for the NULL
        m_str = SysAllocStringByteLen(NULL, cbStrLen-sizeof(OLECHAR));
        if (!*this) hr = E_OUTOFMEMORY;
        else hr = pStream->Read((void*) m_str, cbStrLen, NULL);
        ...
    }
    if (hr == S_FALSE) hr = E_FAIL;
    return hr;
}
 
2.3.7   BSTR的注意點、嵌入NUL字符的字符串、
    編譯器認爲BSTROLECHAR*同義。事實上,BSTROLECHAR*的一種typedef定義。看wtypes.h的定義:
typedef /* [wire_marshal] */ OLECHAR __RPC_FAR * BSTR;
    這是非常頭痛的。並不是所有的BSTR都是OLECHAR*,不是所有的OLECHAR*BSTR。正因爲大多數情況下BSTR能和OLECHAR*一樣使用,這方面非常容易誤導我們。
STDMETHODIMP SomeClass::put_Name(LPCOLESTR pName);
 
BSTR bstrInput = ...
pObj->put_Name(bstrInput); // This works just fine... usually
SysFreeString(bstrInput);
    在上面的例子中,因爲bstrInput參數是BSTR類型,在字符串內可能包含NUL字符。put_Name方法的期望參數是LPCOLESTR(NUL字符結束的字符串),可能會只保存第一個NUL字符前面部分的字符。也就是可能切斷字符串。
    當要求[out] OLECHAR*參數時也不能使用BSTR。比如:
STDMETHODIMP SomeClass::get_Name(OLECHAR** ppName) {
    BSTR bstrOutput =...    // Produce BSTR string to return
    *ppName = bstrOutput;   // This compiles just fine
    return S_OK;            // but leaks memory as caller 
                            // doesn't release BSTR
}
    相反地,需要BSTR時也不能使用OLECHAR*。即使碰巧能運作,也是一個潛在的BUG。比如,下面的代碼就是不正確的:
STDMETHODIMP SomeClass::put_Name(BSTR bstrName);
// Wrong! Wrong! Wrong!
pObj->put_Name(OLECHAR("This is not a BSTR!"))
    如果put_Name方法用SysStringLen函數獲取BSTR的長度,它會嘗試從整型前綴讀取長度,但是字符串並沒有這樣的整數。即使put_Name方法間接那樣做也會有問題,存在進程外訪問。在此情景,列集代碼調用SysStringLen獲取字符串長度放入請求包。這個長度值通常非常大(例子中是字符串開始四個字節的字面數值),當嘗試拷貝字符串時常常引起崩潰。
    因爲編譯器不知道BSTROLECHAR*的區別。我們很容易因無意中用一個內嵌NUL字符的BSTR字符串代替CComBSTR而引起非法運行。下面討論在這些情況下應該使用哪種方法。
    構造一個CComBSTR對象,必須指定字符串的長度:
BSTR bstrInput=SysAllocStringLen(OLESTR("Part one/0and Part two"), 36);
CComBSTR str8 (bstrInput);      // Wrong! Unexpected behavior here
                                // Note: str8 contains only “Part one"
CComBSTR str9 (::SysStringLen (bstrInput), bstrInput); // Correct!
// str9 contains "This is part one/0and here's part two"
    把一個內嵌NUL字符的BSTR賦值給CComBSTR對象將肯定錯誤。比如:
// BSTR bstrInput contains "This is part one/0and here's part two"
CComBSTR str10;
str10 = bstrInput; // Wrong! Unexpected behavior here
                    // str10 now contains "This is part one"
    執行BSTR的賦值操作最簡單的方法就是用EmptyAppendBSTR方法:
str10.Empty();                // Insure object is initially empty
str10.AppendBSTR(bstrInput);    // This works!
    在實際中,雖然BSTR可以包含內嵌的NUL字符,但是多數時候都不包含。當然,這也表示多數時候我們不能輕易發現由錯誤使用BSTR引起的潛在BUG
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章