Posted on 2009-01-07 13:56
天之骄子 阅读(11585)
评论(1) 编辑 收藏 引用
先来看看CString的使用:
先定义几个以后会用到的变量:
CString str1, str2, str3;
概括说明:
MFC对CString类的封装可能的确不如std::string完善,但是也的确不错,功能也足够强大,使用上还很体贴。其基本特征为:
CString类没有基类。
CString类和LPCTSTR的关系:MSDN上说“CString objects follow "value semantics." Think of a CString object as an actual string, not as a pointer to a string.”也就是说用的时候除了考虑使用它的成员函数的方式,就把它当作普通的c-style字符串来使用就行了。你可以在构造函数中使用LPCTSTR:
CString str("Hello World!");
可以:
str1 = str2; 或者 str1 = “Hello”;
也可以:
str3 = str1 + str2; 或者 str3 = str1 + “Hello”;
当然也可以:
str1 += str2; 或者 str1 += “Hello”;
实际上下列的这些操作符都可以用在CString对象之间或者CString和LPCTSTR对象之间:
==、!=、<、>、<=、>=
自然,将CString对象强制转换到LPCTSTR类型也就应该在情理之中:
LPCTSTR string = (LPCTSTR) str1;
CString支持UNICODE和多字节字符集(MBCS)。因为其本身是基于TCHAR的——当然你不要忘了定义编码方式,如:#define _UNICODE。
CString支持引用计数。可以通过其成员函数LockBuffe/和UnLockBuffer来禁用/启用引用计数。
对于CString类的成员函数的定义、说明、返回值等形式在此并不赘述,如有此疑问请参阅:http://msdn.microsoft.com/library/en-us/vcmfc98/html/_mfc_cstring_class_members.asp中的相关链接。
常用函数和范例:
改变大小写:
CString::MakeUpper和CString::MakeLower两个成员函数(不带参数)能使整个字符串变成大/小写字母。
例: str1 = “hello”;
str1.MakeUpper();
afxDump << str1; // 输出结果是”HELLO”;
反转:void CString::MakeReverse();
从.rc文件读入字符串:
CString::LoadString函数把传入的字符串资源ID对应的字符串读入到CString对象中。如果成功就返回非零值。
BOOL bResult = LoadString(IDS_FILENOTFOUND);
子串操作
→去掉字符串左边空格:str1.TrimLeft();
→去掉字符串右边空格:str1.TrimRight();
→获得指定位置字符:char a = str1.GetAt(3); 相应的有CString::SetAt函数,修改指定位置字符。
→删除字符串中所有指定字符:
str1 = “Hello test”;
str1.Remove(‘t’);
afxDump << str1; //输出”Hello es”;
→删除指定位置指定长度的子串:
str1 = “Hello test”;
str1.Delete(3, 2); //第一个参数为index(从零开始)
//第二个参数是长度
afxDump << str1; //输出”Hel test”;
→清空对象的内容:
void CString::Empty();
→查找子串:
※CString::Find函数有四个重载,可以进行字符和字串的搜索,同时还可以指定搜索的起始位置,并返回第一次查找到的index。
int Find( TCHAR ch ) const;
int Find( LPCTSTR lpszSub ) const;
int Find( TCHAR ch, int nStart ) const;
int Find( LPCTSTR pstr, int nStart ) const;
※CString::ReverseFind是返回字符串中最后一个匹配字符的index,与Find函数查找方向正好相反,可惜只有一种重载:
int ReverseFind( TCHAR ch ) const;
※CString::FindOneof查找的是第一个与指定字符串中任意一个匹配字符的index。(好像有点绕口,看看例子就明白了)
str1 = “Hello test”;
int j = str1.Find(“el”);
afxDump << “j=” << j << “\n”;
int k = str1.Find(‘e’, 3);
afxDump << “k=” << k << “\n”;
int l = str1.ReverseFind(‘t’);
afxDump << “l=” << l << “\n”;
int m = str1.ReverseFind(‘t’);
afxDump << “m=” << m << “\n”;
int n = str1. FindOneOf(“stuv”);
afxDump << “n=” << n << “\n”;
输出结果:
j=1
k=7
l=9
m=9
n=6
→字串截断:CString::Left、CString::Right函数都只带一个参数,并且都返回一个CString对象,作用是截取左/右边指定长度的子串。CString::Mid函数第一个参数指定位置,第二个参数指定长度。这几个都是常用的函数,就不写例子了
→获得Buffer
经常有人问到CString对象和char *的转换问题,除了前面说到的强制转化,就是用这个了
LPTSTR GetBufferSetLength( int nNewLength );使用返回的指针可以直接修改CString对象的内容,不过有两点要注意,一是如果指定长度比原CString长度短(截断)请记得在后面补’\0’,二是在调用CString对象的任何其它成员函数前请一定记得ReleaseBuffer,也许不用似乎并没有出错,但是说不定就是大隐患的根源。
→CString::SpanExcluding函数
以前回答过一个把CString对象分离子串的问题,现在想想,如果当时用这个函数的话,将使多么的方便。函数原型:
CString SpanExcluding( LPCTSTR lpszCharSet ) const;
它查找CString对象中与lpszCharSet串中任意匹配的第一个字符,并返回一个CString对象,该对象的内容是原来对象从起始位置到查找到字符的前一个字符的部分。这在分离用分割符(逗号空格之类)隔开的子串的时候将十分方便:
str1 = “Hello test”;
str2 = str1.SpanExcluding(“ ,”);
afxDump << str2; //输出”Hello”
同时,还有一个很方便的函数:CString::SpanIncluding,函数原型:
CString SpanIncluding( LPCTSTR lpszCharSet ) const;
它返回对象中前若干个字符,这些字符都必须在lpszCharSet之中:
str1 = “Hello test”;
str2 = str1.SpanIncluding(“ABCDEFGHIJK”);
afxDump << str2; //输出”H”
→插入子串:用CString::Insert可以插入字符或者字串到指定位置
str1 = “Hello test”;
str1.Insert(2,“ABCD”);
afxDump << str1; //输出”HeABCDllo test”
→替换:CString::Replace的作用是将原来对象中的所有匹配相替换指定字符/子串。有两个重载原型:
int Replace( TCHAR chOld, TCHAR chNew );
int Replace( LPCTSTR lpszOld, LPCTSTR lpszNew );
☆CString对象的属性操作:这些都很常用了,简要说明之
int GetLength( ) const; //获得buffer的长度
BOOL IsEmpty( ) const; //判断CString对象内容是否为空
int Compare( LPCTSTR lpsz ) const; //与lpsz按ASCII码比较
int CompareNoCase( LPCTSTR lpsz ) const; //与lpsz按ASCII码比较,忽略大小写
CString::Format /*用来格式化对象。切记不要把对象本身放到Format函数的参数中去了*/
原理:
Cstring就是对一个用来存放字符串的缓冲区和对施加于这个字符串的操作封装。也就是说,Cstring里需要有一个用来存放字符串的缓冲区,并且有一个指针指向该缓冲区,该指针就是LPTSTR m_pchData。但是有些字符串操作会增建或减少字符串的长度,因此为了减少频繁的申请内存或者释放内存,Cstring会先申请一个大的内存块用来存放字符串。这样,以后当字符串长度增长时,如果增加的总长度不超过预先申请的内存块的长度,就不用再申请内存。当增加后的字符串长度超过预先申请的内存时,Cstring先释放原先的内存,然后再重新申请一个更大的内存块。同样的,当字符串长度减少时,也不释放多出来的内存空间。而是等到积累到一定程度时,才一次性将多余的内存释放。
还有,当使用一个Cstring对象a来初始化另一个Cstring对象b时,为了节省空间,新对象b并不分配空间,它所要做的只是将自己的指针指向对象a的那块内存空间,只有当需要修改对象a或者b中的字符串时,才会为新对象b申请内存空间,这叫做写入复制技术(CopyBeforeWrite)。
这样,仅仅通过一个指针就不能完整的描述这块内存的具体情况,需要更多的信息来描述。
首先,需要有一个变量来描述当前内存块的总的大小。
其次,需要一个变量来描述当前内存块已经使用的情况。也就是当前字符串的长度
另外,还需要一个变量来描述该内存块被其他Cstring引用的情况。有一个对象引用该内存块,就将该数值加一。
Cstring中专门定义了一个结构体来描述这些信息:
struct CStringData
{
long nRefs; // reference count
int nDataLength; // length of data (including terminator)
int nAllocLength; // length of allocation
// TCHAR data[nAllocLength]
TCHAR* data() // TCHAR* to managed data
{ return (TCHAR*)(this+1); }
};
实际使用时,该结构体的所占用的内存块大小是不固定的,在Cstring内部的内存块头部,放置的是该结构体。从该内存块头部开始的sizeof(CstringData)个BYTE后才是真正的用于存放字符串的内存空间。这种结构的数据结构的申请方法是这样实现的:
pData = (CStringData*) new BYTE[sizeof(CStringData) + (nLen+1)*sizeof(TCHAR)];
pData->nAllocLength = nLen;
其中nLen是用于说明需要一次性申请的内存空间的大小的。
从代码中可以很容易的看出,如果想申请一个256个TCHAR的内存块用于存放字符串,实际申请的大小是:
sizeof(CstringData)个BYTE + (nLen+1)个TCHAR
其中前面sizeof(CstringData)个BYTE是用来存放CstringData信息的。后面的nLen+1个TCHAR才是真正用来存放字符串的,多出来的一个用来存放’\0’。
Cstring中所有的operations的都是针对这个缓冲区的。比如LPTSTR CString::GetBuffer(int nMinBufLength),它的实现方法是:
首先通过Cstring::GetData()取得CstringData对象的指针。该指针是通过存放字符串的指针m_pchData先后偏移sizeof(CstringData),从而得到了CstringData的地址。
然后根据参数nMinBufLength给定的值重新实例化一个CstringData对象,使得新的对象里的字符串缓冲长度能够满足nMinBufLength。
然后在重新设置一下新的CstringData中的一些描述值。
最后将新CstringData对象里的字符串缓冲直接返回给调用者。
这些过程用C++代码描述就是:
if (GetData()->nRefs > 1 || nMinBufLength > GetData()->nAllocLength)
{
// we have to grow the buffer
CStringData* pOldData = GetData();
int nOldLen = GetData()->nDataLength; // AllocBuffer will tromp it
if (nMinBufLength < nOldLen)
nMinBufLength = nOldLen;
AllocBuffer(nMinBufLength);
memcpy(m_pchData, pOldData->data(), (nOldLen+1)*sizeof(TCHAR));
GetData()->nDataLength = nOldLen;
CString::Release(pOldData);
}
ASSERT(GetData()->nRefs <= 1);
// return a pointer to the character storage for this string
ASSERT(m_pchData != NULL);
return m_pchData;
很多时候,我们经常的对大批量的字符串进行互相拷贝修改等,Cstring 使用了CopyBeforeWrite技术。使用这种方法,当利用一个Cstring对象a实例化另一个对象b的时候,其实两个对象的数值是完全相同的,但是如果简单的给两个对象都申请内存的话,对于只有几个、几十个字节的字符串还没有什么,如果是一个几K甚至几M的数据量来说,是一个很大的浪费。
因此Cstring 在这个时候只是简单的将新对象b的字符串地址m_pchData直接指向另一个对象a的字符串地址m_pchData。所做的额外工作是将对象a的内存应用CstringData:: nRefs加一。
CString::CString(const CString& stringSrc)
{
m_pchData = stringSrc.m_pchData;
InterlockedIncrement(&GetData()->nRefs);
}
这样当修改对象a或对象b的字符串内容时,首先检查CstringData:: nRefs的值,如果大于一(等于一,说明只有自己一个应用该内存空间),说明该对象引用了别的对象内存或者自己的内存被别人应用,该对象首先将该应用值减一,然后将该内存交给其他的对象管理,自己重新申请一块内存,并将原来内存的内容拷贝过来。
其实现的简单代码是:
void CString::CopyBeforeWrite()
{
if (GetData()->nRefs > 1)
{
CStringData* pData = GetData();
Release();
AllocBuffer(pData->nDataLength);
memcpy(m_pchData, pData->data(),
(pData- >nDataLength+1)*sizeof(TCHAR));
}
}
其中Release 就是用来判断该内存的被引用情况的。
void CString::Release()
{
if (GetData() != _afxDataNil)
{
if (InterlockedDecrement(&GetData()->nRefs) <= 0)
FreeData(GetData());
}
}
当多个对象共享同一块内存时,这块内存就属于多个对象,而不在属于原来的申请这块内存的那个对象了。但是,每个对象在其生命结束时,都首先将这块内存的引用减一,然后再判断这个引用值,如果小于等于零时,就将其释放,否则,将之交给另外的正在引用这块内存的对象控制。
Cstring使用这种数据结构,对于大数据量的字符串操作,可以节省很多频繁申请释放内存的时间,有助于提升系统性能。
通过上面的分析,我们已经对Cstring的内部机制已经有了一个大致的了解了。总的说来MFC中的Cstring是比较成功的。但是,由于数据结构比较复杂(使用CstringData),所以在使用的时候就出现了很多的问题,最典型的一个就是用来描述内存块属性的属性值和实际的值不一致。出现这个问题的原因就是Cstring为了方便某些应用,提供了一些operations,这些operation可以直接返回内存块中的字符串的地址值,用户可以通过对这个地址值指向的地址进行修改,但是,修改后又没有调用相应的operations1使CstringData中的值来保持一致。比如,用户可以首先通过operations得到字符串地址,然后将一些新的字符增加到这个字符串中,使得字符串的长度增加,但是,由于是直接通过指针修改的,所以描述该字符串长度的CstringData中的nDataLength却还是原来的长度,因此当通过GetLength获取字符串长度时,返回的必然是不正确的。
存在这些问题的operations下面一一介绍。
1. GetBuffer
很多错误用法中最典型的一个就是Cstring:: GetBuffer ()了.查了MSDN,里面对这个operation的描述是:
Returns a pointer to the internal character buffer for the CString object. The returned LPTSTR is not const and thus allows direct modification of CString contents。
这段很清楚的说明,对于这个operation返回的字符串指针,我们可以直接修改其中的值:
CString str1("This is the string 1");――――――――――――――――1
int nOldLen = str1.GetLength();―――――――――――――――――2
char* pstr1 = str1.GetBuffer( nOldLen );――――――――――――――3
strcpy( pstr1, "modified" );――――――――――――――――――――4
int nNewLen = str1.GetLength();―――――――――――――――――5
通过设置断点,我们来运行并跟踪这段代码可以看出,当运行到三处时,str1的值是”This is the string 1”,并且nOldLen的值是20。当运行到5处时,发现,str1的值变成了”modified”。也就是说,对GetBuffer返回的字符串指针,我们将它做为参数传递给strcpy,试图来修改这个字符串指针指向的地址,结果是修改成功,并且Cstring对象str1的值也响应的变成了” modified”。但是,我们接着再调用str1.GetLength()时却意外的发现其返回值仍然是20,但是实际上此时str1中的字符串已经变成了” modified”,也就是说这个时候返回的值应该是字符串” modified”的长度8!而不是20。现在Cstring工作已经不正常了!这是怎么回事?
很显然,str1工作不正常是在对通过GetBuffer返回的指针进行一个字符串拷贝之后的。
再看MSDN上的关于这个operation的说明,可以看到里面有这么一段话:
If you use the pointer returned by GetBuffer to change the string contents, you must call ReleaseBuffer before using any other CString member s.
原来在对GetBuffer返回的指针使用之后需要调用ReleaseBuffer,这样才能使用其他Cstring的operations。上面的代码中,我们在4-5处增建一行代码:str2.ReleaseBuffer(),然后再观察nNewLen,发现这个时候已经是我们想要的值8了。
从Cstring的机理上也可以看出:GetBuffer返回的是CstringData对象里的字符串缓冲的首地址。根据这个地址,我们对这个地址里的值进行的修改,改变的只是CstringData里的字符串缓冲中的值, CstringData中的其他用来描述字符串缓冲的属性的值已经不是正确的了。比如此时CstringData:: nDataLength很显然还是原来的值20,但是现在实际上字符串的长度已经是8了。也就是说我们还需要对CstringData中的其他值进行修改。这也就是需要调用ReleaseBuffer()的原因了。
正如我们所预料的,ReleaseBuffer源代码中显示的正是我们所猜想的:
CopyBeforeWrite(); // just in case GetBuffer was not called
if (nNewLength == -1)
nNewLength = lstrlen(m_pchData); // zero terminated
ASSERT(nNewLength <= GetData()->nAllocLength);
GetData()->nDataLength = nNewLength;
m_pchData[nNewLength] = ''''\0'''';
其中CopyBeforeWrite是实现写拷贝技术的,这里不管它。
下面的代码就是重新设置CstringData对象中描述字符串长度的那个属性值的。首先取得当前字符串的长度,然后通过GetData()取得CstringData的对象指针,并修改里面的nDataLength成员值。
但是,现在的问题是,我们虽然知道了错误的原因,知道了当修改了GetBuffer返回的指针所指向的值之后需要调用ReleaseBuffer才能使用Cstring的其他operations时,我们就能避免不在犯这个错误了。答案是否定的。这就像虽然每一个懂一点编程知识的人都知道通过new申请的内存在使用完以后需要通过delete来释放一样,道理虽然很简单,但是,最后实际的结果还是有由于忘记调用delete而出现了内存泄漏。
实际工作中,常常是对GetBuffer返回的值进行了修改,但是最后却忘记调用ReleaseBuffer来释放。而且,由于这个错误不象new和delete人人都知道的并重视的,因此也没有一个检查机制来专门检查,所以最终程序中由于忘记调用ReleaseBuffer而引起的错误被带到了发行版本中。
要避免这个错误,方法很多。但是最简单也是最有效的就是避免这种用法。很多时候,我们并不需要这种用法,我们完全可以通过其他的安全方法来实现。
比如上面的代码,我们完全可以这样写:
CString str1("This is the string 1");
int nOldLen = str1.GetLength();
str1 = "modified";
int nNewLen = str1.GetLength();
但是有时候确实需要,比如:
我们需要将一个Cstring对象中的字符串进行一些转换,这个转换是通过调用一个dll里的函数Translate来完成的,但是要命的是,不知道什么原因,这个函数的参数使用的是char*型的:
DWORD Translate( char* pSrc, char *pDest, int nSrcLen, int nDestLen );
这个时候我们可能就需要这个方法了:
Cstring strDest;
Int nDestLen = 100;
DWORD dwRet = Translate( _strSrc.GetBuffer( _strSrc.GetLength() ),
strDest.GetBuffer(nDestLen),
_strSrc.GetLength(), nDestlen );
_strSrc.ReleaseBuffer();
strDest.ReleaseBuffer();
if ( SUCCESSCALL(dwRet) )
{
}
if ( FAILEDCALL(dwRet) )
{
}
的确,这种情况是存在的,但是,我还是建议尽量避免这种用法,如果确实需要使用,请不要使用一个专门的指针来保存GetBuffer返回的值,因为这样常常会让我们忘记调用ReleaseBuffer。就像上面的代码,我们可以在调用GetBuffer之后马上就调用ReleaseBuffer来调整Cstring对象。
2. LPCTSTR
关于LPCTSTR的错误常常发生在初学者身上。
例如在调用函数
DWORD Translate( char* pSrc, char *pDest, int nSrcLen, int nDestLen );
时,初学者常常使用的方法就是:
int nLen = _strSrc.GetLength();
DWORD dwRet = Translate( (char*)(LPCTSTR)_strSrc),
(char*)(LPCTSTR)_strSrc),
nLen,
nLen);
if ( SUCCESSCALL(dwRet) )
{
}
if ( FAILEDCALL(dwRet) )
{
}
他原本的初衷是将转换后的字符串仍然放在_strSrc中,但是,当调用完Translate以后之后再使用_strSrc时,却发现_strSrc已经工作不正常了。检查代码却又找不到问题到底出在哪里。
其实这个问题和第一个问题是一样的。Cstring类已经将LPCTST重载了。在Cstring中LPCTST实际上已经是一个operation了。对LPCTST的调用实际上和GetBuffer是类似的,直接返回CstringData对象中的字符串缓冲的首地址。
其C++代码实现是:
_AFX_INLINE CString::operator LPCTSTR() const
{ return m_pchData; }
因此在使用完以后同样需要调用ReleaseBuffer()。
但是,这个谁又能看出来呢?
其实这个问题的本质原因出在类型转换上。LPCTSTR返回的是一个const char*类型,因此使用这个指针来调用Translate编译是不能通过的。对于一个初学者,或者一个有很长编程经验的人都会再通过强行类型转换将const char*转换为char*。最终造成了Cstring工作不正常,并且这样也很容易造成缓冲溢出。
通过上面对于Cstring机制和一些容易出现的使用错误的描述,可以使我们更好的使用Cstring