C++ 字符串完整指南,第二部分 - 字符串包装类






4.95/5 (154投票s)
2002年10月7日
19分钟阅读

1588561
关于 Visual C++ 和类库提供的字符串包装器类的指南
引言
由于 C 风格字符串容易出错且难以管理,更不用说成为寻找缓冲区溢出漏洞的黑客的目标,因此有许多字符串包装类。不幸的是,在某些情况下,并非总是清楚应该使用哪个类,也不知道如何将 C 风格字符串转换为包装类。
本文涵盖了 Win32 API、MFC、STL、WTL 和 Visual C++ 运行时库中的所有字符串类型。我将描述每个类的用法、如何构造对象以及如何转换为其他类。Nish 还贡献了 Visual C++ 7 中托管字符串和类的部分。
为了充分利用本文,你**必须**理解我在第一部分中介绍的不同字符类型和编码。
字符串类的规则 #1
**强制转换是糟糕的**,除非它们有明确的文档说明。
促使我写这两篇文章的原因是关于如何将字符串类型 X 转换为类型 Z 的常见问题,发帖人使用了强制转换却不明白为什么代码不起作用。各种字符串类型,尤其是`BSTR`,在任何一个地方都没有简洁的文档,所以我想有些人会随意使用强制转换,并希望它能起作用。
强制转换**不会**对字符串进行任何转换,**除非**源字符串是一个具有明确文档说明的转换运算符的包装类。对字符串字面量进行强制转换对字符串没有任何作用,所以写出类似这样的代码
void SomeFunc ( LPCWSTR widestr ); main() { SomeFunc ( (LPCWSTR) "C:\\foo.txt" ); // WRONG! }
将百分之百失败。它会编译,因为强制转换会覆盖编译器的类型检查。但仅仅因为它编译了,并不意味着代码是正确的。
在下面的示例中,我将指出何时强制转换是合法的。
C 风格字符串和 typedef
正如我在第一部分中介绍的,Windows API 是根据`TCHAR`定义的,它可以是 MBCS 或 Unicode 字符,具体取决于你在编译时是否定义了`_MBCS`或`_UNICODE`符号。你应该参考第一部分以获取`TCHAR`的完整描述,但为了方便,我将在此处列出字符 typedef。
类型 |
含义 |
---|---|
|
Unicode 字符(`wchar_t`) |
|
MBCS 或 Unicode 字符,取决于预处理器设置 |
|
`char`字符串(`char*`) |
|
`char`常量字符串(`const char*`) |
|
`WCHAR`字符串(`WCHAR*`) |
|
`WCHAR`常量字符串(`const WCHAR*`) |
|
`TCHAR`字符串(`TCHAR*`) |
|
`TCHAR`常量字符串(`const TCHAR*`) |
另外一种字符类型是`OLECHAR`。它代表了自动化接口中使用的字符类型(例如 Word 公开的接口,以便你可以操作文档)。这种类型通常定义为`wchar_t`,但是如果你定义了`OLE2ANSI`预处理器符号,`OLECHAR`将被定义为`char`类型。我知道现在没有理由定义`OLE2ANSI`(自 MFC 3 时代以来,微软就没有使用过它),所以从现在开始我将把`OLECHAR`视为 Unicode 字符。
以下是你将看到的与`OLECHAR`相关的 typedef
类型 |
含义 |
---|---|
|
Unicode 字符(`wchar_t`) |
|
LPOLESTR |
|
LPCOLESTR |
还有两个宏用于字符串和字符字面量,以便同一代码可以用于 MBCS 和 Unicode 构建
类型 |
含义 |
---|---|
|
在 Unicode 构建中,在字面量前加上`L`。 |
|
在字面量前加上`L`,使其成为`LPCOLESTR`。 |
你可能在文档或示例代码中遇到`_T`的变体。有*四个*等效宏——`TEXT`、`_TEXT`、`__TEXT`和`__T`——它们都做同样的事情。
COM 中的字符串 - BSTR 和 VARIANT
许多自动化和其他 COM 接口使用 `BSTR` 来处理字符串,并且 `BSTR` 有一些陷阱,因此我将在这里专门介绍 `BSTR`。
`BSTR` 是 Pascal 风格字符串(长度与数据一起明确存储)和 C 风格字符串(字符串长度必须通过查找终止零字符来计算)的混合体。`BSTR` 是一个 Unicode 字符串,其长度前置,并且也以零字符终止。以下是“Bob”作为 `BSTR` 的示例
|
|
|
|
|
|
|
|
|
|
请注意字符串的长度是如何前置于字符串数据的。它是一个`DWORD`,保存字符串中的*字节数*,*不包括*终止零。在这种情况下,“Bob”包含 3 个 Unicode 字符(不包括终止零),总共 6 个字节。长度字段的存在是为了当`BSTR`在进程或计算机之间封送时,COM 库知道要传输多少数据。(顺便说一句,`BSTR`可以包含任意数据块,而不仅仅是字符,并且可以包含嵌入的零字符。但是,出于本文的目的,我将不考虑这种情况。)
C++ 中的 `BSTR` 变量实际上是指向字符串第一个字符的指针。事实上,类型 `BSTR` 是这样定义的
typedef OLECHAR* BSTR;
这是非常不幸的,因为实际上`BSTR`与 Unicode 字符串**不同**。该 typedef 禁用了类型检查,允许你随意混合`LPOLESTR`和`BSTR`。将`BSTR`传递给期望`LPCOLESTR`(或`LPCWSTR`)的函数是安全的,但是反之则不然。因此,重要的是要了解函数期望的字符串的确切类型,并传递正确类型的字符串。
要理解为什么将`LPCWSTR`传递给期望`BSTR`的函数是不安全的,请记住字符串前面的四个字节必须存储其长度。`LPCWSTR`没有这样的长度。如果`BSTR`需要封送到另一个进程(例如,你正在控制的 Word 实例),COM 库将查找该长度并找到垃圾,或者堆栈上的其他变量,或者其他随机数据。这可能会导致方法失败,甚至在感知到的长度太长时导致崩溃。
有几个 API 可以操作`BSTR`,但其中最重要的两个是创建和销毁`BSTR`的函数。它们是`SysAllocString()`和`SysFreeString()`。`SysAllocString()`将 Unicode 字符串复制到`BSTR`中,而`SysFreeString()`释放`BSTR`使用的内存。
BSTR bstr = NULL; bstr = SysAllocString ( L"Hi Bob!" ); if ( NULL == bstr ) // out of memory error // Use bstr here... SysFreeString ( bstr );
当然,各种`BSTR`包装类会为你处理内存管理。
自动化接口中使用的另一种类型是 `VARIANT`。它用于在 JScript 和 VBScript 等无类型语言以及某些情况下的 Visual Basic 之间发送数据。`VARIANT` 可以包含多种不同类型的数据,例如 `long` 和 `IDispatch*`。当 `VARIANT` 包含字符串时,它以 `BSTR` 形式存储。稍后在介绍 `VARIANT` 包装类时,我将详细讨论 `VARIANT`。
字符串包装类
既然我已经介绍了各种字符串类型,我将演示包装类。对于每个类,我将展示如何构造一个对象以及如何将其转换为 C 风格的字符串指针。C 风格指针通常是 API 调用或构造不同字符串类的对象所必需的。我将不涉及类提供的其他运算符,例如排序或比较。
再次强调,**不要**盲目地转换对象,除非你完全理解所生成的代码将做什么。
CRT 提供的类
_bstr_t
`_bstr_t`是`BSTR`的完整包装,实际上它隐藏了底层`BSTR`。它提供了各种构造函数,以及访问底层 C 风格字符串的运算符。但是,没有运算符可以直接访问`BSTR`本身,因此`_bstr_t`不能作为`[out]`参数传递给 COM 方法。如果你需要一个`BSTR*`作为参数,使用 ATL 类`CComBSTR`更容易。
一个`_bstr_t`*可以*传递给一个接受`BSTR`的函数,但这只是由于三个巧合。首先,`_bstr_t`有一个到`wchar_t*`的转换函数;其次,`wchar_t*`和`BSTR`在编译器看来是相同的,因为`BSTR`的定义;第三,`_bstr_t`内部保留的`wchar_t*`指向的内存块遵循`BSTR`格式。所以即使没有文档化的`BSTR`转换,它也恰好有效。
// Constructing _bstr_t bs1 = "char string"; // construct from a LPCSTR _bstr_t bs2 = L"wide char string"; // construct from a LPCWSTR _bstr_t bs3 = bs1; // copy from another _bstr_t _variant_t v = "Bob"; _bstr_t bs4 = v; // construct from a _variant_t that has a string // Extracting data LPCSTR psz1 = bs1; // automatically converts to MBCS string LPCSTR psz2 = (LPCSTR) bs1; // cast OK, same as previous line LPCWSTR pwsz1 = bs1; // returns the internal Unicode string LPCWSTR pwsz2 = (LPCWSTR) bs1; // cast OK, same as previous line BSTR bstr = bs1.copy(); // copies bs1, returns it as a BSTR // ... SysFreeString ( bstr );
请注意,`_bstr_t`也有用于`char*`和`wchar_t*`的转换运算符。这是一种有问题的设计,因为即使这些是非常量字符串指针,你也不得使用这些指针修改缓冲区,因为这可能会破坏内部`BSTR`结构。
_variant_t
`_variant_t`是`VARIANT`的完整包装,它提供了许多构造函数和转换函数,以操作`VARIANT`可以包含的众多类型。我将仅在此处介绍与字符串相关的操作。
// Constructing _variant_t v1 = "char string"; // construct from a LPCSTR _variant_t v2 = L"wide char string"; // construct from a LPCWSTR _bstr_t bs1 = "Bob"; _variant_t v3 = bs1; // copy from a _bstr_t object // Extracting data _bstr_t bs2 = v1; // extract BSTR from the VARIANT _bstr_t bs3 = (_bstr_t) v1; // cast OK, same as previous line
请注意,如果无法进行类型转换,`_variant_t`方法可能会抛出异常,因此请准备好捕获`_com_error`异常。
另请注意,没有从`_variant_t`到 MBCS 字符串的直接转换。你需要创建一个临时的`_bstr_t`变量,使用另一个提供 Unicode 到 MBCS 转换的字符串类,或者使用 ATL 转换宏。
与`_bstr_t`不同,`_variant_t`可以直接作为参数传递给 COM 方法。`_variant_t`派生自`VARIANT`类型,因此根据 C++ 语言规则,允许将`_variant_t`替换`VARIANT`进行传递。
STL 类
STL 只有一个字符串类,`basic_string`。`basic_string`管理一个以零结尾的字符数组。字符类型在`basic_string`模板参数中给出。通常,`basic_string`应被视为不透明对象。你可以获取内部缓冲区的只读指针,但任何写入操作都必须使用`basic_string`的运算符和方法。
有`basic_string`的两种预定义特化:`string`,它包含`char`,和`wstring`,它包含`wchar_t`。没有内置的`TCHAR`特化,但你可以使用下面列出的那个。
// Specializations typedef basic_string<TCHAR> tstring; // string of TCHARs // Constructing string str = "char string"; // construct from a LPCSTR wstring wstr = L"wide char string"; // construct from a LPCWSTR tstring tstr = _T("TCHAR string"); // construct from a LPCTSTR // Extracting data LPCSTR psz = str.c_str(); // read-only pointer to str's buffer LPCWSTR pwsz = wstr.c_str(); // read-only pointer to wstr's buffer LPCTSTR ptsz = tstr.c_str(); // read-only pointer to tstr's buffer
与`_bstr_t`不同,`basic_string`无法直接在字符集之间进行转换。但是,如果构造函数接受字符类型,你可以将`c_str()`返回的指针传递给另一个类的构造函数,例如
// Example, construct _bstr_t from basic_string _bstr_t bs1 = str.c_str(); // construct a _bstr_t from a LPCSTR _bstr_t bs2 = wstr.c_str(); // construct a _bstr_t from a LPCWSTR
ATL 类
CComBSTR
`CComBSTR`是 ATL 的`BSTR`包装器,在某些情况下比`_bstr_t`更有用。最值得注意的是,`CComBSTR`允许访问底层`BSTR`,这意味着你可以将`CComBSTR`对象传递给 COM 方法,并且`CComBSTR`对象会自动为你管理`BSTR`内存。例如,假设你想调用此接口的方法
// Sample interface: struct IStuff : public IUnknown { // Boilerplate COM stuff omitted... STDMETHOD(SetText)(BSTR bsText); STDMETHOD(GetText)(BSTR* pbsText); };
`CComBSTR`有一个`operator BSTR`方法,因此可以直接传递给`SetText()`。还有一个`operator &`,它返回一个`BSTR*`,因此你可以在`CComBSTR`对象上使用`&`运算符将其传递给接受`BSTR*`的函数。
CComBSTR bs1; CComBSTR bs2 = "new text"; pStuff->GetText ( &bs1 ); // ok, takes address of internal BSTR pStuff->SetText ( bs2 ); // ok, calls BSTR converter pStuff->SetText ( (BSTR) bs2 ); // cast ok, same as previous line
`CComBSTR`具有与`_bstr_t`类似的构造函数,但是没有内置的 MBCS 字符串转换器。为此,你可以使用 ATL 转换宏。
// Constructing CComBSTR bs1 = "char string"; // construct from a LPCSTR CComBSTR bs2 = L"wide char string"; // construct from a LPCWSTR CComBSTR bs3 = bs1; // copy from another CComBSTR CComBSTR bs4; bs4.LoadString ( IDS_SOME_STR ); // load string from string table // Extracting data BSTR bstr1 = bs1; // returns internal BSTR, but don't modify it! BSTR bstr2 = (BSTR) bs1; // cast ok, same as previous line BSTR bstr3 = bs1.Copy(); // copies bs1, returns it as a BSTR BSTR bstr4; bstr4 = bs1.Detach(); // bs1 no longer manages its BSTR // ... SysFreeString ( bstr3 ); SysFreeString ( bstr4 );
请注意,在最后一个例子中使用了`Detach()`方法。调用该方法后,`CComBSTR`对象不再管理其`BSTR`或相关内存。这就是为什么`bstr4`上需要调用`SysFreeString()`。
附注:`operator &`重载意味着你不能直接在某些 STL 集合(例如`list`)中使用`CComBSTR`。集合要求`&`运算符返回一个指向所包含类的指针,但对`CComBSTR`应用`&`会返回`BSTR*`,而不是`CComBSTR*`。但是,有一个 ATL 类可以解决这个问题,`CAdapt`。例如,要创建一个`CComBSTR`列表,可以这样声明
std::list< CAdapt<CComBSTR> > bstr_list;
`CAdapt`提供了集合所需的运算符,但它对你的代码是不可见的;你可以像使用`CComBSTR`列表一样使用`bstr_list`。
CComVariant
`CComVariant`是`VARIANT`的包装。然而,与`_variant_t`不同,`VARIANT`并未隐藏,事实上你需要直接访问`VARIANT`的成员。`CComVariant`提供了许多构造函数来操作`VARIANT`可以包含的众多类型。我将仅在此处介绍与字符串相关的操作。
// Constructing CComVariant v1 = "char string"; // construct from a LPCSTR CComVariant v2 = L"wide char string"; // construct from a LPCWSTR CComBSTR bs1 = "BSTR bob"; CComVariant v3 = (BSTR) bs1; // copy from a BSTR // Extracting data CComBSTR bs2 = v1.bstrVal; // extract BSTR from the VARIANT
与`_variant_t`不同,没有到各种`VARIANT`类型的转换运算符。如上所示,你必须直接访问`VARIANT`成员并确保`VARIANT`包含你期望类型的数据。如果需要将`CComVariant`的数据转换为`BSTR`,可以调用`ChangeType()`方法。
CComVariant v4 = ... // Init v4 from somewhere CComBSTR bs3; if ( SUCCEEDED( v4.ChangeType ( VT_BSTR ) )) bs3 = v4.bstrVal;
与`_variant_t`一样,没有直接转换为 MBCS 字符串的方法。你需要创建一个临时的`_bstr_t`变量,使用另一个提供 Unicode 到 MBCS 转换的字符串类,或者使用 ATL 转换宏。
ATL 转换宏
ATL 的字符串转换宏是字符编码之间转换的非常便捷的方式,在函数调用中特别有用。它们根据`[源类型]2[新类型]`或`[源类型]2C[新类型]`的方案命名。使用第二种形式命名的宏转换为常量指针(因此名称中带有“C”)。类型缩写是
A:MBCS 字符串,`char*`(A 代表 ANSI)
W:Unicode 字符串,`wchar_t*`(W 代表宽)
T:`TCHAR`字符串,`TCHAR*`
OLE:`OLECHAR`字符串,`OLECHAR*`(实际上等同于 W)
BSTR:`BSTR`(仅用作目标类型)
因此,例如,`W2A()`将 Unicode 字符串转换为 MBCS 字符串,而`T2CW()`将`TCHAR`字符串转换为常量 Unicode 字符串。
要使用宏,首先包含 atlconv.h 头文件。即使在非 ATL 项目中也可以这样做,因为该头文件不依赖于 ATL 的其他部分,也不需要`_Module`全局变量。然后,当你在函数中使用转换宏时,在函数开头放置`USES_CONVERSION`宏。这定义了一些宏使用的局部变量。
当目标类型不是`BSTR`时,转换后的字符串存储在堆栈上,因此如果希望字符串比当前函数存在更长时间,则需要将字符串复制到另一个字符串类中。当目标类型*是*`BSTR`时,内存不会自动释放,因此必须将返回值分配给`BSTR`变量或`BSTR`包装类,以避免内存泄漏。
以下是一些显示各种转换宏的示例
// Functions taking various strings: void Foo ( LPCWSTR wstr ); void Bar ( BSTR bstr ); // Functions returning strings: void Baz ( BSTR* pbstr ); #include <atlconv.h> main() { using std::string; USES_CONVERSION; // declare locals used by the ATL macros // Example 1: Send an MBCS string to Foo() LPCSTR psz1 = "Bob"; string str1 = "Bob"; Foo ( A2CW(psz1) ); Foo ( A2CW(str1.c_str()) ); // Example 2: Send a MBCS and Unicode string to Bar() LPCSTR psz2 = "Bob"; LPCWSTR wsz = L"Bob"; BSTR bs1; CComBSTR bs2; bs1 = A2BSTR(psz2); // create a BSTR bs2.Attach ( W2BSTR(wsz) ); // ditto, assign to a CComBSTR Bar ( bs1 ); Bar ( bs2 ); SysFreeString ( bs1 ); // free bs1 memory // No need to free bs2 since CComBSTR will do it for us. // Example 3: Convert the BSTR returned by Baz() BSTR bs3 = NULL; string str2; Baz ( &bs3 ); // Baz() fills in bs3 str2 = W2CA(bs3); // convert to an MBCS string SysFreeString ( bs3 ); // free bs3 memory }
如你所见,当你在一种格式中拥有字符串而函数需要不同的格式时,这些宏在向函数传递参数时非常方便。
MFC 类
CString
MFC `CString` 保存 `TCHAR`,因此确切的字符类型取决于你定义的预处理器符号。通常,`CString` 类似于 STL `string`,你应将其视为不透明对象,并仅使用 `CString` 方法修改它。`CString` 相对于 STL `string` 的一个优点是它具有接受 MBCS 和 Unicode 字符串的构造函数,并且它具有到 `LPCTSTR` 的转换器,因此你可以直接将 `CString` 对象传递给接受 `LPCTSTR` 的函数;无需调用 `c_str()` 方法。
// Constructing CString s1 = "char string"; // construct from a LPCSTR CString s2 = L"wide char string"; // construct from a LPCWSTR CString s3 ( ' ', 100 ); // pre-allocate a 100-byte buffer, fill with spaces CString s4 = "New window text"; // You can pass a CString in place of an LPCTSTR: SetWindowText ( hwndSomeWindow, s4 ); // Or, equivalently, explicitly cast the CString: SetWindowText ( hwndSomeWindow, (LPCTSTR) s4 );
你还可以从字符串表中加载字符串。有一个`CString`构造函数可以做到这一点,以及`LoadString()`。`Format()`方法也可以选择从字符串表中读取格式字符串。
// Constructing/loading from string table CString s5 ( (LPCTSTR) IDS_SOME_STR ); // load from string table CString s6, s7; // Load from string table. s6.LoadString ( IDS_SOME_STR ); // Load printf-style format string from the string table: s7.Format ( IDS_SOME_FORMAT, "bob", nSomeStuff, ... );
那个第一个构造函数看起来很奇怪,但它实际上就是这样记录的加载字符串的方式。
请注意,你可以应用于`CString`的**唯一**合法转换是转换为`LPCTSTR`。转换为`LPTSTR`(即非`const`指针)是错误的。养成将`CString`转换为`LPTSTR`的习惯只会伤害自己,因为当代码稍后确实崩溃时,你可能不明白为什么,因为你在其他地方使用了相同的代码并且它恰好起作用了。获取缓冲区的非`const`指针的正确方法是`GetBuffer()`方法。
以正确用法为例,考虑在列表控件中设置项目文本的情况
CString str = _T("new text"); LVITEM item = {0}; item.mask = LVIF_TEXT; item.iItem = 1;item.pszText = (LPTSTR)(LPCTSTR) str;// WRONG! item.pszText = str.GetBuffer(0); // correct ListView_SetItem ( &item ); str.ReleaseBuffer(); // return control of the buffer to str
`pszText`成员是一个`LPTSTR`,一个非`const`指针,因此你需要在`str`上调用`GetBuffer()`。`GetBuffer()`的参数是你希望`CString`为缓冲区分配的最小长度。如果出于某种原因你想要一个足够大以容纳 1K `TCHAR`的可修改缓冲区,你将调用`GetBuffer(1024)`。将 0 作为长度传递只是返回一个指向字符串当前内容的指针。
上面划掉的那一行会编译,甚至会在*这种情况下*工作。但这并不意味着代码是正确的。通过使用非`const`强制转换,你打破了面向对象的封装,并对`CString`的内部实现做出了假设。如果你养成这种强制转换的习惯,你最终会遇到代码中断的情况,你会想知道为什么它不起作用,因为你在其他地方都使用了相同的代码并且它(似乎)有效。
你知道人们总是抱怨现在的软件多么多 bug 吗?Bug 是由程序员编写不正确的代码引起的。你真的想编写你*知道*是错误的代码,从而助长“所有软件都有 bug”这种观念吗?花时间学习使用`CString`的正确方法,让你的代码百分之百有效。
`CString`还有两个函数可以从`CString`内容创建`BSTR`,必要时转换为 Unicode。它们是`AllocSysString()`和`SetSysString()`。除了`SetSysString()`接受的`BSTR*`参数外,它们的工作方式相同。
// Converting to BSTR CString s5 = "Bob!"; BSTR bs1 = NULL, bs2 = NULL; bs1 = s5.AllocSysString(); s5.SetSysString ( &bs2 ); // ... SysFreeString ( bs1 ); SysFreeString ( bs2 );
COleVariant
`COleVariant`与`CComVariant`非常相似。`COleVariant`派生自`VARIANT`,因此可以传递给接受`VARIANT`的函数。然而,与`CComVariant`不同,`COleVariant`只有一个`LPCTSTR`构造函数。没有单独的`LPCSTR`和`LPCWSTR`构造函数。在大多数情况下这不是问题,因为你的字符串很可能都是`LPCTSTR`,但这是一个需要注意的地方。`COleVariant`还有一个接受`CString`的构造函数。
// Constructing CString s1 = _T("tchar string"); COleVariant v1 = _T("Bob"); // construct from an LPCTSTR COleVariant v2 = s1; // copy from a CString
与`CComVariant`一样,你必须直接访问`VARIANT`成员,如果需要将`VARIANT`转换为字符串,可以使用`ChangeType()`方法。但是,`COleVariant::ChangeType()`如果失败,会抛出异常,而不是返回失败的`HRESULT`代码。
// Extracting data COleVariant v3 = ...; // fill in v3 from somewhere BSTR bs = NULL; try { v3.ChangeType ( VT_BSTR ); bs = v3.bstrVal; } catch ( COleException* e ) { // error, couldn't convert } SysFreeString ( bs );
WTL 类
CString
WTL 的`CString`行为与 MFC 的`CString`完全相同,因此请参阅上面 MFC`CString`的描述。
CLR 和 VC 7 类
`System::String`是 .NET 中用于处理字符串的类。在内部,`String`对象包含一个不可变的字符序列。任何声称操作`String`对象的`String`方法实际上都会返回一个新的`String`对象,因为原始`String`是不可变的。`String`的一个特殊之处在于,如果你有多个包含相同字符序列的`String`,它们实际上都指向同一个对象。C++ 的托管扩展有一个新的字符串字面量前缀`S`,用于表示托管字符串字面量。
// Constructing String* ms = S"This is a nice managed string";
你可以通过传递非托管字符串来构造`String`对象,但这比通过传递托管字符串来构造`String`对象的效率稍低。这是因为所有相同的`S`前缀字符串实例都代表同一个对象,但对于非托管字符串则不是这样。以下代码将阐明这一点
String* ms1 = S"this is nice"; String* ms2 = S"this is nice"; String* ms3 = L"this is nice"; Console::WriteLine ( ms1 == ms2 ); // prints true Console::WriteLine ( ms1 == ms3); // prints false
比较可能未使用`S`前缀字符串创建的字符串的正确方法是使用`String::CompareTo()`方法,如下所示
Console::WriteLine ( ms1->CompareTo(ms2) ); Console::WriteLine ( ms1->CompareTo(ms3) );
以上两行都将打印 0,这意味着字符串相等。
在`String`和 MFC 7 `CString`之间转换很容易。`CString`有一个到`LPCTSTR`的转换器,`String`有两个接受`char*`和`wchar_t*`的构造函数,因此你可以直接将`CString`传递给`String`构造函数。
CString s1 ( "hello world" ); String* s2 ( s1 ); // copy from a CString
反向转换类似
String* s1 = S"Three cats";
CString s2 ( s1 );
这可能会让你有点困惑,但它之所以有效,是因为从 VS.NET 开始,`CString`有一个接受`String`对象的构造函数
CStringT ( System::String* pString );
为了进行一些快速操作,你可能有时想访问底层字符串
String* s1 = S"Three cats"; Console::WriteLine ( s1 ); const __wchar_t __pin* pstr = PtrToStringChars(s1); for ( int i = 0; i < wcslen(pstr); i++ ) (*const_cast<__wchar_t*>(pstr+i))++; Console::WriteLine ( s1 );
`PtrToStringChars()`返回一个`const __wchar_t*`到底层字符串,我们需要将其固定下来,否则垃圾回收器在操作其内容时可能会在内存中移动字符串。
将字符串类与 printf 风格的格式化函数一起使用
在使用字符串包装类与`printf()`或任何以`printf()`方式工作的函数时,必须仔细注意。这包括`sprintf()`及其变体,以及`TRACE`和`ATLTRACE`宏。由于没有对函数的附加参数进行类型检查,因此必须小心只传递 C 风格字符串指针,而不是完整的字符串对象。
因此,例如,要将`_bstr_t`中的字符串传递给`ATLTRACE()`,你**必须**明确地写出`(LPCSTR)`或`(LPCWSTR)`强制转换
_bstr_t bs = L"Bob!";
ATLTRACE("The string is: %s in line %d\n", (LPCSTR) bs, nLine);
如果你忘记强制转换并传递整个`_bstr_t`对象,则跟踪消息将显示无意义的输出,因为推入堆栈的将是`_bstr_t`变量保留的任何内部数据。
所有类摘要
两种字符串类之间转换的通常方法是,获取源字符串,将其转换为 C 风格字符串指针,然后将该指针传递给目标类型中的构造函数。因此,这里有一个图表,显示了如何将字符串转换为 C 风格指针,以及哪些类可以从 C 风格指针构造。
类 |
字符串 |
转换 |
转换为 |
转换为 |
转换为 |
转换 |
构造 |
构造 |
---|---|---|---|---|---|---|---|---|
|
|
是,强制转换1 |
是,强制转换 |
是,强制转换1 |
是,强制转换 |
是2 |
是 |
是 |
|
|
否 |
否 |
否 |
强制转换为 |
强制转换为 |
是 |
是 |
|
MBCS |
否 |
是,`c_str()` |
否 |
否 |
否 |
是 |
否 |
|
Unicode |
否 |
否 |
否 |
是,`c_str()` |
否 |
否 |
是 |
|
|
否 |
否 |
否 |
是,强制转换 |
是,强制转换 |
是 |
是 |
|
|
否 |
否 |
否 |
是4 |
是4 |
是 |
是 |
|
|
否6 |
在 MBCS |
否6 |
在 Unicode |
否5 |
是 |
是 |
|
|
否 |
否 |
否 |
是4 |
是4 |
在 MBCS 构建中 |
在 Unicode 构建中 |
1尽管`_bstr_t`提供了到非`const`指针的转换运算符,但如果超出缓冲区,修改底层缓冲区可能会导致 GPF,或者在释放`BSTR`内存时导致内存泄漏。 |