C++字符串完全指引之二 —— 字符串封装类
原著:Michael Dunn 作者:Chengjie Sun
引言
因为
C
语言风格的字符串容易出错且不易管理,黑客们甚至利用可能存在的缓冲区溢出
bug
把
C
语言风格的字符串作为攻击目标,所以出现了很多字符串封装类。不幸的是,在某些场合下我们不知道该使用哪个字符串类,也不知道怎样把一个
C
风格的字符串转换成一个字符串封装类。
这篇文章将介绍所有在
Win32 API, MFC, STL, WTL
和
Visual C++
运行库中出现的字符串类型。我将描述每一个类的用法,告诉大家怎样创建每一个类的对象以及怎样把一个类转换成其他类。受控字符串和
Visual C++ 7
中的类两部分是
Nish
完成的。
Rule #1 of string classes
使用
cast
来实现类型转换是不好的做法,除非有文档明确指出这种转换可以使用。
促使我写这两篇文章的原因是字符串类型转换中经常遇到的一些问题。当我们使用
cast
把字符串从类型
X
转换到类型
Z
的时候,我们不知道为什么代码不能正常工作。各种各样的字符串类型,尤其是
BSTR
,几乎没有在任何一个地方的文档中被明确的指出可以用
cast
来实现类型转换。所以我想一些人可能会使用
cast
来实现类型转换并希望这种转换能够正常工作。
除非源字符串是一个被明确指明支持转换操作符的字符串包装类,否则
cast
不对字符串做任何转换。对常量字符串使用
cast
不会起到任何作用,所以下面的代码:
void SomeFunc ( LPCWSTR widestr );
main()
{
SomeFunc ( (LPCWSTR) "C://foo.txt" ); // WRONG!
}
肯定会失败。它可以被编译,因为
cast
操作会撤消编译器的类型检查。但是,编译可以通过并不能说明代码是正确的。
在下面的例子中,我将会指明
cast
在什么时候使用是合法的。
C-style strings and typedefs
正如我在第一部分中提到的,
windows APIs
是用
TCHARs
来定义的,在编译时,它可以根据你是否定义
_MBCS
或者
_UNICODE
被编译成
MBCS
或者
Unicode
字符。你可以参看第一部分中对
TCHAR
的完整描述,这里为了方便,我列出了字符的
typedefs
Type
|
Meaning
|
WCHAR
|
Unicode character (wchar_t)
|
TCHAR
|
MBCS or Unicode character, depending on preprocessor settings
|
LPSTR
|
string of char (char*)
|
LPCSTR
|
constant string of char (const char*)
|
LPWSTR
|
string of WCHAR (WCHAR*)
|
LPCWSTR
|
constant string of WCHAR (const WCHAR*)
|
LPTSTR
|
string of TCHAR (TCHAR*)
|
LPCTSTR
|
constant string of TCHAR (const TCHAR*)
|
一个增加的字符类型是
OLETYPE
。它表示自动化接口(如
word
提供的可以使你操作文档的接口)中使用的字符类型。这种类型一般被定义成
wchar_t
,然而如果你定义了
OLE2ANSI
预处理标记,
OLECHAR
将会被定义成
char
类型。我知道现在已经没有理由定义
OLE2ANSI
(从
MFC3
以后,微软已经不使用它了),所以从现在起我将把
OLECHAR
当作
Unicode
字符。
这里给出你将会看到的一些
OLECHAR
相关的
typedefs
:
Type
|
Meaning
|
OLECHAR
|
Unicode character (wchar_t)
|
LPOLESTR
|
string of OLECHAR (OLECHAR*)
|
LPCOLESTR
|
constant string of OLECHAR (const OLECHAR*)
|
还有两个用于包围字符串和字符常量的宏定义,它们可以使同样的代码被用于
MBCS
和
Unicode builds
:
Type
|
Meaning
|
_T(x)
|
Prepends L to the literal in Unicode builds.
|
OLESTR(x)
|
Prepends L to the literal to make it an LPCOLESTR.
|
在文档或例程中,你还会看到好多
_T
的变体。有四个等价的宏定义,它们是
TEXT, _TEXT, __TEXT
和
__T
,它们都起同样的做用。
COM
中的字符串
—— BSTR
和
VARIANT
很多自动化和
COM
接口使用
BSTR
来定义字符串。
BSTRs
中有几个
"
陷阱
"
,所以这里我用单独的部分来说明它。
BSTR
是
Pascal-style
字符串(字符串长度被明确指出)和
C-style
字符串(字符串的长度要通过寻找结束符来计算)的混合产物。一个
BSTR
是一个
Unicode
字符串,它的长度是预先考虑的,并且它还有一个
0
字符作为结束标记。下面是一个
BSTR
的示例:
06 00 00 00
|
42 00
|
6F
00
|
62 00
|
00 00
|
--length--
|
B
|
o
|
b
|
EOS
|
注意字符串的长度是如何被加到字符串数据中的。长度是
DWORD
类型的,保存了字符串中包含的字节数,但不包括结束标记。在这个例子中,
"Bob"
包含
3
个
Unicode
字符(不包括结束符),总共
6
个字节。字符串的长度被预先存储好,以便当一个
BSTR
在进程或者计算机之间被传递时,
COM
库知道多少数据需要传送。(另一方面,一个
BSTR
能够存储任意数据块,而不仅仅是字符,它还可以包含嵌入在数据中的
0
字符。然而,由于这篇文章的目的,我将不考虑那些情况)。
在
C++
中,一个
BSTR
实际上就是一个指向字符串中第一个字符的指针。它的定义如下:
BSTR bstr = NULL;
bstr = SysAllocString ( L"Hi Bob!" );
if ( NULL == bstr )
// out of memory error
// Use bstr here...
SysFreeString ( bstr );
自然的,各种各样的
BSTR
封装类为你实现内存管理。
另外一个用在自动化接口中的变量类型是
VARIANT
。它被用来在无类型(
typeless
)语言,如
Jscript
和
VBScript
,来传递数据。一个
VARIANT
可能含有很多不同类型的数据,例如
long
和
IDispatch*
。当一个
VARIANT
包含一个字符串,字符串被存成一个
BSTR
。当我后面讲到
VARIANT
封装类时,我会对
VARIANT
多些介绍。
字符串封装类
到目前为止,我已经介绍了各种各样的字符串。下面,我将说明封装类。对于每个封装类,我将展示怎样创建一个对象及怎样把它转换成一个
C
语言风格的字符串指针。
C
语言风格的字符串指针对于
API
的调用,或者创建一个不同的字符串类对象经常是必需的。我不会介绍字符串类提供的其他操作,比如排序和比较。
重复一遍,除非你确切的明白结果代码将会做什么,否则不要盲目地使用
cast
来实现类型转换。
CRT
提供的类
_bstr_t
_bstr_t
是一个对
BSTR
的完整封装类,实际上它隐藏了底层的
BSTR
。它提供各种构造函数和操作符来访问底层的
C
语言风格的字符串。然而,
_bstr_t
却没有访问
BSTR
本身的操作符,所以一个
_bstr_t
类型的字符串不能被作为输出参数传给一个
COM
方法。如果你需要一个
BSTR*
参数,使用
ATL
类
CComBSTR
是比较容易的方式。
一个
_bstr_t
字符串能够传给一个接收参数类型为
BSTR
的函数,只是因为下列
3
个条件同时满足。首先,
_bstr_t
有一个向
wchar_t*
转换的转换函数;其次,对编译器而言,因为
BSTR
的定义,
wchar_t*
和
BSTR
有同样的含义;第三,
_bstr_t
内部含有的
wchar_t*
指向一片按
BSTR
的形式存储数据的内存。所以,即使没有文档说明,
_bstr_t
可以转换成
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
类型,所以传递一个
_variant_t
来代替
VARIANT
变量是
C++
语言所允许的。
STL
类
STL
只有一个字符串类,
basic_string
。一个
basic_string
管理一个以
0
做结束符的字符串数组。字符的类型是
basic_string
模般的参数。总的来说,一个
basic_string
类型的变量应该被当作不透明的对象。你可以得到一个指向内部缓冲区的只读指针,但是任何写操作必须使用
basic_string
的操作符和方法。
basic_string
有两个预定义的类型:包含
char
的
string
类型和包含
wchar_t
的
wstring
类型。这里没有内置的包含
TCHAR
的类型,但是你可以使用下面列出的代码来实现。
// Specializations
typedef basic_string 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
有一个操作符
--BSTR
方法,所以它能直接被传给
SetText()
函数。还有另外一个操作
--&
,这个操作符返回一个
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()
的原因。
做一个补充说明:重载的
&
操作符意味着在一些
STL
容器中你不能直接使用
CComBSTR
变量,比如
list
。容器要求
&
操作符返回一个指向容器包含的类的指针,但是对
CComBSTR
变量使用
&
操作符返回的是
BSTR*
,而不是
CComBSTR*
。然而,有一个
ATL
类可以解决这个问题,这个类是
CAdapt
。例如,你可以这样声明一个
CComBSTR
的
list
:
std::list< CAdapt<CComBSTR> > bstr_list;
CAdapt
提供容器所需要的操作符,但这些操作符对你的代码是透明的。你可以把一个
bstr_list
当作一个
CComBSTR
的
list
来使用。
CComVariant
CComVariant
是
VARIANT
的封装类。然而,不像
_variant_t
,在
CComVariant
中
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
一样,
CComVariant
也没有提供向
MBCS
字符串转换的转换操作。你需要创建一个
_bstr_t
类型的中间变量,使用提供从
Unicode
到
MBCS
转换的另一个字符串类,或者使用一个
ATL
的转换宏。
ATL
转换宏
ATL
:转换宏是各种字符编码之间进行转换的一种很方便的方式,在函数调用时,它们显得非常有用。
ATL
转换宏的名称是根据下面的模式来命名的
[
源类型
]2[
新类型
]
或者
[
源类型
]2C[
新类型
]
。据有第二种形式的名字的宏的转换结果是常量指针(对应名字中的
"C"
)。各种类型的简称如下:
A: MBCS string, char* (A for ANSI)
W: Unicode string, wchar_t* (W for wide)
T: TCHAR string, TCHAR*
OLE: OLECHAR string, OLECHAR* (in practice, equivalent to W)
BSTR: BSTR (used as the destination type only)
所以,
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
对象。
CString
有一个
string
所不具备的优点:
CString
具有接收
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
(非常量指针)是错误的。养成把一个
CString
变量转换成
LPTSTR
的习惯将会给你带来伤害,因为当你的程序后来崩溃时,你可能不知道为什么,因为你到处都使用同样的代码而那时它们都恰巧正常工作。正确的得到一个指向缓冲区的非常量指针的方法是调用
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
变量,一个非常量指针,因此你需要对
str
调用
GetBuffer()
。
GetBuffer()
的参数是你需要
CString
为缓冲区分配的最小长度。如果因为某些原因,你需要一个可修改的缓冲区来存放
1K TCHARs
,你需要调用
GetBuffer(1024)
。把
0
作为参数时,
GetBuffer()
返回的是指向字符串当前内容的指针。
上面划线的语句可以被编译,在这种情况下,甚至可以正常起作用。但这并不意味着这行代码是正确的。通过使用非常量转换,你已经破坏了面向对象的封装,并对
CString
的内部实现作了某些假定。如果你有这样的转换习惯,你终将会陷入代码崩溃的境地。你会想代码为什么不能正常工作了,因为你到处都使用同样的代码而那些代码看起来是正确的。
你知道人们总是抱怨现在的软件的
bug
是多么的多吗?软件中的
bug
是因为程序员写了不正确的代码。难道你真的想写一些你知道是错误的代码来为所有的软件都满是
bug
这种认识做贡献吗?花些时间来学习使用
CString
的正确方法让你的代码在任何时间都正常工作把。
CString
有两个函数来从一个
CString
创建一个
BSTR
。它们是
AllocSysString()
和
SetSysString()
。
// 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
的构造函数。在大多数情况下这不是一个问题,因为不管怎样你的字符串很可能是
LPCTSTRs
,但这是一个需要意识到的问题。
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
对象包含相同的字符序列,它们实际上是指向相同的对象的。相对于
C++
的使用扩展是增加了一个新的字符串常量前缀
S
,
S
用来代表一个受控的字符串常量(
a managed string literal
)。
// 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
,
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-style
格式函数中使用字符串类
当你在
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
语言风格的字符串指针的构造函数。
Class
|
string type
|
convert to char*?
|
convert to const char*?
|
convert to wchar_t*?
|
convert to const wchar_t*?
|
convert to BSTR?
|
construct from char*?
|
construct from wchar_t*?
|
_bstr_t
|
BSTR
|
yes cast1
|
yes cast
|
yes cast1
|
yes cast
|
yes2
|
yes
|
yes
|
_variant_t
|
BSTR
|
no
|
no
|
no
|
cast to
_bstr_t3
|
cast to
_bstr_t3
|
yes
|
yes
|
string
|
MBCS
|
no
|
yes c_str() method
|
no
|
no
|
no
|
yes
|
no
|
wstring
|
Unicode
|
no
|
no
|
no
|
yes c_str() method
|
no
|
no
|
yes
|
CComBSTR
|
BSTR
|
no
|
no
|
no
|
yes cast to BSTR
|
yes cast
|
yes
|
yes
|
CComVariant
|
BSTR
|
no
|
no
|
no
|
yes4
|
yes4
|
yes
|
yes
|
CString
|
TCHAR
|
no6
|
in MBCS
builds, cast
|
no6
|
in Unicode
builds, cast
|
no5
|
yes
|
yes
|
COleVariant
|
BSTR
|
no
|
no
|
no
|
yes4
|
yes4
|
in MBCS
builds
|
in Unicode
builds
|
·
1
、即使
_bstr_t
提供了向非常量指针的转换操作符,修改底层的缓冲区也会已引起
GPF
如果你溢出了缓冲区或者造成内存泄漏。
·
2
、
_bstr_t
在内部用一个
wchar_t*
来保存
BSTR
,所以你可以使用
const wchar_t*
来访问
BSTR
。这是一个实现细节,你可以小心的使用它,将来这个细节也许会改变。
·
3
、如果数据不能转换成
BSTR
会抛出一个异常。
·
4
、使用
ChangeType()
,然后访问
VARIANT
的
bstrVal
成员。在
MFC
中,如果数据转换不成功将会抛出异常。
·
5
、这里没有转换
BSTR
函数,然而
AllocSysString()
返回一个新的
BSTR
。
·
6
、使用
GetBuffer()
方法,你可以暂时地得到一个非常量的
TCHAR
指针。
|
作者简介
Michael Dunn
:
Michael Dunn
居住在阳光城市洛杉矶。他是如此的喜欢这里的天气以致于想一生都住在这里。他在
4
年级时开始编程,那时用的电脑是
Apple //e
。
1995
年,在
UCLA
获得数学学士学位,随后在
Symantec
公司做
QA
工程师,在
Norton AntiVirus
组工作。他自学了
Windows
和
MFC
编程。
1999-2000
年,他设计并实现了
Norton AntiVirus
的新界面。
Michael
现在在
Napster
(一个提供在线订阅音乐服务的公司)做开发工作,他还开发了
UltraBar
,一个
IE
工具栏插件,它可以使网络搜索更加容易,给了
googlebar
以沉重打击;他还开发了
CodeProject SearchBar
;与人共同创建了
Zabersoft
公司,该公司在洛杉矶和丹麦的
Odense
都设有办事处。
他喜欢玩游戏。爱玩的游戏有
pinball, bike riding
,偶尔还玩
PS, Dreamcasth
和
MAME
游戏。他因忘了自己曾经学过的语言:法语、汉语、日语而感到悲哀。
Nishant S(Nish)
:
Nish
是来自印度
Trivandrum,
的
Microsoft Visual C++ MVP
。他从
1990
年开始编码。现在,
Nish
为作为合同雇员在家里为
CodeProject
工作。
他还写了一部浪漫戏剧《
Summer Love and Some more Cricket
》和一本编程书籍《
Extending MFC applications with the .NET Framework
》。他还管理者
MVP
的一个网站
http://www.voidnish.com/
。在这个网站上,你可以看到他的很多关于编程方面的思想和文章。
Nish
还计划好了旅游,他希望自一生中能够到达地球上尽可能多的地方。