整理:赖仪灵
出处:
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
。