引言
由于C风格字符串容易出错并且管理困难,又容易成为寻找缓冲区溢出漏洞的黑客们的攻击目标,因此有许多字符串的封装类。但不幸之处在于在什么场合使用什么类往往不太明确,也不知道如何把一个C风格字符串转换为这些封装类。
这篇文章将涵盖所有在Win32 API、MFC、STL、WTL和Visual C++运行时库中涉及到的字符串类型。我将会叙述每个类的用法以及如何构造对象,如何向其他类转化。Nish在字符串管理和Visual C++ 7中的类这部分内容上也有所贡献。
为了更好的从这篇文章获益,你必须理解了不同字符类型和编码的相关知识,就是我第一部分介绍的那些内容。
字符串类的使用的规则1
类型转换是不好的,除非有明确的文档说明(explicitly documented)。
促使我写这两篇文章的原因就是一些经常被问起的关于如何从字符串类型X向类型Z转换的问题,在一些发帖者使用了类型转换而不理解代码为何不能正常运行的地方。众多的字符串类型,尤其是BSTR,都没有在任何地方准确的说明过,因此我想有些人使用了类型转换并且希望代码能正常运行。
一个类型转换并不会对字符串起到真正转换的作用,除非源字符串的封装类有明确的文档说明了转换操作符。类型转换对字符串字面值不会起任何作用,像下面的代码:
将会100%失败。代码将被编译,因为类型转换绕过了编译器的类型检查。但是不能仅仅因为编译了这段代码就意味了代码是正确的。
在下面的例子中,我将会指出什么时候类型转换是合法的。
C风格字符串和typedef
正如我第一部分介绍的那样,Windows API 是以TCHAR来定义和形成文档的,可以为MBCS或者是Unicode字符,这取决于你编译时是定义了_MBCS还是_UNICODE字符。你应该参考第一部分来全面了解TCHAR,但是为了方便我也将字符的定义列在下边。
一个新增加的字符类型是OLECHAR,它代表的是使用在自动化接口中的字符(例如Word暴露的一些用来操作文档的接口)。这个类型通常定义为wchar_t,但是如果你定义了OLE2ANSI预编译符号时,OLECHAR将被定义为char类型。
这些天,我也不知为何定义OLE2ANSI(从MFC 3开始微软就没有再使用了),因此从现在起我将把OLECHAR当做Unicode字符处理.
以下是一些OLECHAR相关的类型定义:
也有两个围绕着字符串和字符字面值(string and character literals)的宏,因此同一份代码可用在MBCS和Unicode工程中使用:
也有一些你在文档后者或者代码中将会遇到的_T宏的变体,它们是四个等同的宏--TEXT,_TEXT,__TEXT和__T--它们的作用全都相同。
COM 中的字符串BSTR 和 VARIANT
很多的自动化和COM接口中使用BSTR作为字符串,BSTR有一些缺点,我单独做一部分说明。
BSTR是介于Pascal风格字符串(长度和数据一同存储的类型)和C风格字符串(字符串长度是通过查看结束标志0字符来计算的类型)之间的混合体。BSTR是一种预先存有长度(prepended)的Unicode字符串,同时又是一结束标志0字符来结尾的字符串。
下面是BSTR字符串"Bob"的示例:
请注意,其长度是如何存储到字符串数据中的。它是一个双字(DWORD),包含了字符串的字节数目,而不是通过计算结束标志0字符。这种情况下,"Bob"包含3个Unicode字符(不包含末尾的0),总共有6个字节。由于显示了长度域,因此当BSTR在不同过程和计算机中排列时,COM库就知道要转换多少数据了。(作为附注,一个BSTR类型可以包含任意的数据块,不仅仅指字符,而且能包含内嵌的0字符。但是限于本文的目的,我不会考虑这方面内容。)
在C++中BSTR实际上是指向字符串中第一个字符的指针。事实上,BSTR类型的定义如下:
这是很不幸的,因为在现实中一个BSTR并不完全是Unicode字符串。这种定义使类型检查失效而允许你自由的混合LPOLESTR和BSTR。向一个需要LPOLESTR(或者是LPCWSTR)的函数传递一个BSTR是安全的。因此意识到一个函数需要准确的字符串类型并且传递正确类型的字符串是很重要的。
要明白为什么向需要一个BSTR的函数传递一个LPCWSTR是不安全的,你要记住BSTR字符串的头四个字节必须字符串的长度,而LPCWSTR并没有这样的长度。如果BSTR需要由另外一个过程来处理(If the BSTR needs to be marshaled to another process)(比如,你控制的一个Word的实例),COM库会查看它的长度并且寻找垃圾,或者是栈中的一些变量,或者是其他随机数据。这样将导致方法调用失败甚至崩溃如果被当做长度太长。
有许多操作BSTR的API,但最重要的两个就是BSTR的构造函数和析构函数。它们就是SysAllocString() 和SysFreeString().SysAllocString()将一个Unicode字符串拷贝到BSTR,而SysFreeString()则释放BSTR使用的内存空间。
自然而然地,众多的BSTR封装类将会替你留意内存管理。
在自动化接口中使用的另一种类型就是VARIANT。这种类型用于在像JScript和VBScript这类无类型的语言之间发送数据,当然也包括在Visual Basic的一些情形。一个VARIANT可以包含很多不同类型的数据,像long和IDispath*。当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的函数,仅仅因为三个巧合(three coincidences)。第一,_bstr_t有一个转换为wchar_t*的转换函数;第二,由于BSTR的定义,wchar_t和BSTR对于编译是等同的;第三,_bstr_t内部持有的wchar_t*指针指向一个允许BSTR格式的内存区域。因此即使没有文档明确的说明向BSTR的转换,但是仍然可以运行。
请注意,_bstr_t也有char*和wchar_t*转换操作符。这是一个颇有质疑的设计,因为尽管它们不是常量字符串指针(non-constant string pointers),但是为了避免破坏BSTR内部结构,你绝对不要使用这些指针来修改buffer。
_variant_t
_variant_t是VARIANT的完全封装,它提供了很多的构造函数和转换函数用来操作VARIANT可以包含的众多的数据类型。我在这里只讲述字符串相关的操作。
请注意,_variant_t 在无法转换时将抛出异常,因此准备接受_com_error异常。
还要注意的是,没有_variant_t向MBCS字符串直接转换的方法。_variant_t 从VARIANT类型派生而来,因此在C++规则中允许在传递VARIANT时用_variant_t类型替代。
STL中的类
STL只含有一个字符串类basic_string。一个basic_string管理了一个以0字符结尾的字符数组。这个字符类型是在basic_string的模板参数中给定的。大体上说,一个basic_string对象应该视为不透明的对象。你可以获取一个内部缓冲区的只读指针,但是任何的写操作都得通过basic_string提供的操作符和方法。
basic_string有两种预定义的specializations。一种包含char和wstring的字符串,一种包含wchar_t的字符串。没有TCHAR的内置((built-in))specializations,但是你可以使用列在下面的这一个。
和_bstr_t不同的是,一个basic_string对象不能直接在字符集之间转换。但是你可以向其他接受该字符类型的类的构造函数传递c_str()返回的指针,例如:
CComBSTR
CComBSTR是ATL中BSTR的封装类,在某些场合比_bstr_t更有用。更值得注意的是,CComBSTR允许访问底层的BSTR,这意味着你可以向COM方法传递一个CComBSTR对象,而这个CComBSTR对象会自动替你管理BSTR的内存。例如,你想调用接口的方法如下:
CComBSTR有一个operator BSTR方法,因此你可以直接将其传递给SetText()。也有一个
&操作符,它返回BSTR*,因此你可以将&操作符作用于CComBSTR对象,将其传递给一个需要BSTR*参数的函数。
CComBSTR有一个和_bstr_t类似的构造函数,然而并没有内置的向MBCS转换的转换函数。对于这一点,你可以使用ATL中的转换宏。
注意到在最后一个例子中Detach()使用了方法。在调用这个方法后,CComBSTR对象将不再管理BSTR本身或者相关的内存,这也就是SysFreeString()方法需要作用在bstr4上的原因了。
补充说明一点,&操作符被重载了意味着你在某些STL的集合类中不能直接使用CComBSTR,例如在list中。集合类需要&操作符返回包含类的指针,而&作用于CComBSTR将返回BSTR*,而不是CComBSTR*。但是,CAdapt这个ATL类解决了这一点。例如,声明一个CComBSTR的list采用如下方式:
CAdapt类提供了集合类所需的操作符,但是对你的代码而言是不可见的。你可以把bstr_list当做CComBSTR的list来使用。
CComVariant
CComVariant是VARIANT的封装类。然而,不像_variant_t,CComVariant中VARIANT没有被屏蔽,事实上,你需要直接访问VARIANT的成员。CComVariant提供了很多的构造函数来操作VARIANT所包含的众多的数据类型。我在这里只叙述与字符串相关的操作。
和_variant_t不同的是,CComVariant没有转换到VARIANT众多类型的操作符。正如上文所讲,你必须直接访问VARIANT的成员并且确保VARIANT包含了你想要的类型的数据。如果你需要将CComVariant数据转换为BSTR,你可以调用ChangeType()方法。
和_variant_t一样,CComVariant 也没有直接转换为MBCS字符串的函数。你需要一个_bstr_t中间变量,使用其他的字符串类提供的Unicode向MBCS转换的函数,或者使用ATL中的转换宏。
ATL转换宏
ATL字符串转换宏是一种相当方便的转换字符编码的方式,尤其在函数调用中十分有用。它们按照[source type]2[new type] 或者是[source type]2C[new type]的组合来命名。以第二种方式命名的宏转换为常量指针(名字中"C"所指)。类型缩写如下:
A: MBCS string, char* (A for ANSI)
W: Unicode string, wchar_t* (W for wide)
T: TCHAR string, TCHAR*
OLE: OLECHAR string, OLECHAR* (实际上等同于W)
BSTR: BSTR (仅作为目的类型)
那么,例如W2A()将Unicode字符串转换为MBCS字符串,T2W()将TCHAR字符串转换为Unicode字符串。
要使用这些宏,你首先要包含atlconv.h头文件。在非ATL工程中你也可以使用,因为这个头文件不依赖于ATL中的其他部分,不需要一个_Module全局变量。那么在函数中使用这些宏时,请将USES_CONVERSION 宏置于函数的开始位置。这样为使用这个宏而定义了一些局部变量。
当目的类型是除了BSTR外的任何类型,转换后的字符串都存储在栈中,因此如果你想在超出函数范围的地方继续持有这个字符串,你需要将其拷贝到另外的一个字符串类中。当目的类型是BSTR时,内存并没有被自动释放,因此你必须将返回值赋给BSTR变量或者是BSTR的封装类来避免内存泄露。
下面列出了很多的转换宏:
就像你看到的这样,当有一种字符串类型但调用函数却需要另一种类型的字符串时,这些宏在函数传参时是如此的方便。
MFC中的类
CString
一个MFC中的CString对象包含的是TCHAR类型的字符,因此准确的说字符类型取决于你定义的预编译符号。大体上看,一个CString对象像STL中的string,这样说来的话,你应该把它看做不透明的对象,你只能通过CString类提供的方法来修改它的内容。但CString类优于STL string的一点是,CString有既可以接受MBCS又可以接受Unicode字符串的构造函数,并且它有一个向LPCTSTR的转换函数,因此你可以直接向接受LPCTSTR的函数传递一个CString对象。没有你需要调用的c_str()方法。
你也可以从字符串资源表中加载一个字符串,有一个CString构造函数来完成此功能,那就是LoadString()。Format()方法也可以有选择的从字符串资源表中读取有格式的字符串。
第一种构造函数看起来很奇怪,但是技术文档中确实是这样来加载一个字符串的。
请注意,唯一合法的作用于CString的类型转换是转换为LPCTSTR。转换为LPTSRT(非常量指针)是错误的。陷入将CString转换为LPTSTR的习惯之中,只会自寻烦恼,当你的代码以后无法运行时,你可能不明白其中缘由,因为同样的代码在其他地方它又碰巧正常运行了。正确的获取一个buffer的非常量指针的方法是调用GetBuffer()方法。
作为一个正确的使用示例,考虑设置列表控件上文字的小案例:
pszText是一个LPTSTR,一个非常量指针,因此你调用了str的GetBuffer()方法。传递给GetBuffer()的参数是CString分配给buffer的最小内存长度。如果由于某些原因,你需要一个能容纳1K TCHAR的可改动的buffer,你需要调用GetBuffer(1024)。传递0作为长度的话,仅仅返回指向当前字符串内容的指针。
上面删除画线部分(WRONG所指部分)的代码会通过编译,在这个例子中甚至可以运行。但这并不意味着代码是正确的。使用了非常量指针,你就破坏了面向对象的封装性,试图做了一些CString类内部的工作。如果你养成了这种类型转换的习惯,迟早你的代码会陷入无法运行的僵局,你可能还在想为什么不能运行了,因为代码在其它地方看起来正常运行了。
你知道人们最近为什么一直抱怨有漏洞的(buggy)软件吗?漏洞源于程序员写出的不正确的代码。你真想写出连你自己都知道是错误的代码,来支持所有软件都有漏洞这种观念吗?还是花些时间学习学习如何正确使用CString类来确保你的代码100%正常运行吧。
CString也有两个函数来从CString内容创建一个BSTR,如果需要转换为Unicode字符串。
它们就是AllocSysString() 和SetSysString()。除了SetSysString()所需的BSTR*参数外,它们运作是相同的。
COleVariant
COleVariant和CComVariant十分相似。COleVariant派生自VARIANT,因此可以将其传递给需要VARIANT的函数。然而,与CComVariant不同之处是,COleVariant只有一个LPCTSTR的构造函数。没有LPCSTR和LPCWSTR分开来的构造函数。在大多数情况下,这不是什么问题,因为你的字符串很可能一直是LPCTSTR,但是仍然要意识到这一点。COleVariant也有一个接受CString的构造函数。
和CComVariant一样,你必须访问直接访问VARIANT的成员,如果需要将VARIANT转换为字符串就使用ChangeType()方法。但是,COleVariant::ChangeType()在失败时将抛出异常而不是通过HRESULT返回失败代码。
CString
WTL中的CString和MFC中的CString极为相似,请参考上述关于MFC CString的内容。
CLR和VC 7中的类System::String 是.NET中处理字符串的类。在内部,一个String对象包含了不可变的字符序列。任何操作String对象的String方法实际上都返回一个新的String对象,因为原始的String是不可变的。String的一个特质就是,如果你有多个包含相同字符序列的String,那么它们实际上都指向同一个对象。C++托管扩展(The Managed Extensions to C++)有一个新的字符串字面值前缀S,代表这是一个托管的字符串字面值。
你可以传递一个非托管的字符串来构造一个String对象,但是这样效率比传递一个托管的字符串来构造String对象稍微低些。这是因为所有相同的以S前缀标明的字符串实例代表的是相同的对象,而非托管字符串则不然。下面的代码将清楚的说明这一点:
对于没有以S前缀来创建的字符串,正确的比较是使用下面所示的String::CompareTo()方法。
上面两行代码均打印0,表示两个字符串相等。
在String和MFC 7中的CString之间转换是容易的。CString有一个转换为LPCTSTR的函数,而String有两个需要char*和wchar_t*的构造函数,因此你可以直接向String的构造函数传递一个CString对象。
相反方向的转换也类似如下:
这或许令你困惑,但是自从VS.NET起,CString的构造函数可以接受一个String对象。
CStringT ( System::String* pString );
对于一些快速的操作,某些时候你可能想要访问底层的字符串:
PtrToStringChars()返回一个我们需要向下遍历(pin down)的指向底层字符串的const __wchar_t*,否则垃圾回收器可能会在我们操纵字符串内容时释放字符串的内存。
使用字符串类的printf风格的格式化函数
当使用封装类的printf()或者有类似作用的函数时,你必须留心。这些函数包括sprintf()和它的一些变体,包括TRACE和ATLTRACE宏。因为对于这些函数的附加参数没有类型检查,你必须小心确保只传递C风格字符串指针而不是一个完完全全的字符串对象。
例如,向ATLTRACE()传递_bstr_t中的字符串时,你必须显式的写出(LPCSTR)或者(LPCWSTR)以表明类型转换:
如果你忘记了类型转换而将整个_bstr_t对象传递给它,那么将输出一些无意义的信息,因为压入栈中的内容将是_bstr_t变量持有的任何内部数据。
所有类的总结
通常在两个字符串类之间的转换就是取源字符串,将其转换为C风格字符串指针,然后将指针传递给目的类型的构造函数。那么我把如何将字符串转化为C风格字符串指针和哪些类可以由C风格字符串指针构造的内容列成下表:
说明:
1.尽管_bstr_t提供了转换为非常量指针的操作符,但是修改底层Buffer的数据时如果buffer越界或者BSTR释放内存时内存泄露则可能引起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的非常量指针。
原文地址:
http://www.codeproject.com/Articles/3004/The-Complete-Guide-to-C-Strings-Part-II-String-Wra
第一部分原文地址:
http://www.codeproject.com/Articles/2995/The-Complete-Guide-to-C-Strings-Part-I-Win32-Chara
第一部分译文地址:
http://blog.csdn.net/ziyuanxiazai123/article/details/7482360