C++字符串完全指引之二 —— 字符串封装类

30 篇文章 0 订阅
26 篇文章 0 订阅
引言

  因为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

TypeMeaning
WCHARUnicode character (wchar_t)
TCHARMBCS or Unicode character, depending on preprocessor settings
LPSTR string of char (char*)
LPCSTRconstant 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:

TypeMeaning
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 0042 006F 0062 0000 00
--length--BobEOS

  注意字符串的长度是如何被加到字符串数据中的。长度是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 dataLPCSTR psz1 = bs1;              // automatically converts to MBCS stringLPCSTR psz2 = (LPCSTR) bs1;     // cast OK, same as previous lineLPCWSTR pwsz1 = bs1;            // returns the internal Unicode stringLPCWSTR pwsz2 = (LPCWSTR) bs1;  // cast OK, same as previous lineBSTR    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的类型,但是你可以使用下面列出的代码来实现。 

// Specializationstypedef basic_string
  
   tstring; // string of TCHARs // Constructingstring str = "char string";         // construct from a LPCSTRwstring wstr = L"wide char string"; // construct from a LPCWSTRtstring tstr = _T("TCHAR string");  // construct from a LPCTSTR // Extracting dataLPCSTR psz = str.c_str();    // read-only pointer to str''s bufferLPCWSTR pwsz = wstr.c_str(); // read-only pointer to wstr''s bufferLPCTSTR 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转换宏。 

// ConstructingCComBSTR bs1 = "char string";       // construct from a LPCSTRCComBSTR bs2 = L"wide char string"; // construct from a LPCWSTRCComBSTR bs3 = bs1;                 // copy from another CComBSTRCComBSTR bs4;  bs4.LoadString ( IDS_SOME_STR );  // load string from string table// Extracting dataBSTR bstr1 = bs1;        // returns internal BSTR, but don''t modify it!BSTR bstr2 = (BSTR) bs1; // cast ok, same as previous lineBSTR bstr3 = bs1.Copy(); // copies bs1, returns it as a BSTRBSTR 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能够包含的多种类型进行处理。这里,我将只介绍和字符串相关的操作。 

// ConstructingCComVariant v1 = "char string";       // construct from a LPCSTRCComVariant v2 = L"wide char string"; // construct from a LPCWSTRCComBSTR bs1 = "BSTR bob";CComVariant v3 = (BSTR) bs1;          // copy from a BSTR // Extracting dataCComBSTR bs2 = v1.bstrVal;            // extract BSTR from the VARIANT      

  不像_variant_t,这里没有提供针对VARIANT包含的各种类型的转换操作符。正如上面介绍的,你必须直接访问VARIANT的成员并且确保这个VARIANT变量保存着你期望的类型。如果你需要把一个CComVariant类型的数据转换成一个BSTR类型的数据,你可以调用ChangeType()方法。 

CComVariant v4 = ... // Init v4 from somewhereCComBSTR 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}      

  正如你所看见的,当你有一个和函数所需的参数类型不同的字符串时,使用这些转换宏是非常方便的。

 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值