深入解析ATL第二版(ATL8.0)笔记(2.3节)
――CComBSTR类
整理:赖仪灵
声明:版权归原作者所有,转载时必须保留此部分文字。
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。