深入解析ATL第二版(ATL8.0)笔记(2.3节)

整理:赖仪灵
出处: http://blog.csdn.net/laiyiling, http://www.cppblog.com/techlab
声明:版权归原作者所有,转载时必须保留此部分文字。
 
    CComBSTR 是非常有用的 ATL 工具类,它封装了 BSTR 数据类型。 CComBSTR 类唯一的数据成员是变量 m_str
class CComBSTR {
public:
    BSTR    m_str;
};
 
2.3.1   构造函数和析构函数
    CComBSTR 的构造函数有八个。默认构造函数把 m_str 初始化为 NULL NULL 也是有效的空 BSTR 串。析构函数用 SysFreeString 释放 m_str SysFreeString 传入参数如果是 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 。当 sz NULL 时, 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 是等于 NULL BSTR SysStringLen 函数适当的实现了检测, 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 还提供了另外两个方法实现 BSTR SAFEARRAY 的转化。
HRESULT BSTRToArray(LPSAFEARRAY * ppArray) {
    Return VectorFromBstr(m_str, ppArray);
}
HRESULT ArrayToBSTR(const SAFEARRAY * pSrc) {
    ::SysStringFree(m_str);
    return BstrFromVector((LPSAFEARRAY)pSrc, &m_str);
}
    其实这两个方法只是 WIN32 函数 BstrFromVector VectorFromBstr 的封装。 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 并不执行从 ANSI UNICODE 的转换。而是用 SysAllocStringByteLen 分配 nLen 字节大小的 BSTR 。然后把结果追加到 CComBSTR 对象。
 
2.3.5   字符大小写转换
    大小写的转换可由方法 ToLower ToUpper 完成。在 Unicode 编译时,内部用 CharLowerBuff API 函数。 ANSI 编译时,字符串先转为 MBCS 然后调用 CharLowerBuff. 结果被转回 UNICODE 存储在新分配的 BSTR m_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 字符的字符串、
    编译器认为 BSTR OLECHAR* 同义。事实上, BSTR OLECHAR* 的一种 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 获取字符串长度放入请求包。这个长度值通常非常大(例子中是字符串开始四个字节的字面数值),当尝试拷贝字符串时常常引起崩溃。
    因为编译器不知道 BSTR OLECHAR* 的区别。我们很容易因无意中用一个内嵌 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 的赋值操作最简单的方法就是用 Empty AppendBSTR 方法:
str10.Empty();                // Insure object is initially empty
str10.AppendBSTR(bstrInput);    // This works!
    在实际中,虽然 BSTR 可以包含内嵌的 NUL 字符,但是多数时候都不包含。当然,这也表示多数时候我们不能轻易发现由错误使用 BSTR 引起的潜在 BUG

你可能感兴趣的:(深入解析ATL第二版(ATL8.0)笔记(2.3节))