CString的工作原理介绍

 
看了很多人写的程序 , 包括我自己写的一些代码,发现很大的一部分 bug 是关于 MFC 类中的 CString 的错误用法的 . 出现这种错误的原因主要是对 CString 的实现机制不是太了解。
    CString 是对于原来标准 c 中字符串类型的一种的包装。因为,通过很长时间的编程,我们发现 , 很多程序的 bug 多和字符串有关 , 典型的有:缓冲溢出、内存泄漏等。而且这些 bug 都是致命的,会造成系统的瘫痪。因此 c++ 里就专门的做了一个类用来维护字符串指针。标准 c++ 里的字符串类是 string ,在 microsoft MFC 类库中使用的是 CString 类。通过字符串类,可以大大的避免 c 中的关于字符串指针的那些问题。
这里我们简单的看看 Microsoft MFC 中的 CString 是如何实现的。当然,要看原理,直接把它的代码拿过来分析是最好的。 MFC 里的关于 CString 的类的实现大部分在 strcore.cpp 中。
    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));
 }

}

 

 

当多个对象共享同一块内存时,这块内存就属于多个对象,而不在属于原来的申请这块内存的那个对象了。但是,每个对象在其生命结束时,都首先将这块内存的引用减一,然后再判断这个引用值,如果小于等于零时,就将其释放,否则,将之交给另外的正在引用这块内存的对象控制。
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 functions.
  原来在对 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] 
=   '
 
其中 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();

 

 

的确,这种情况是存在的,但是,我还是建议尽量避免这种用法,如果确实需要使用,请不要使用一个专门的指针来保存 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

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) )
{
}

但是有时候确实需要,比如:
我们需要将一个CString对象中的字符串进行一些转换,这个转换是通过调用一个dll里的函数Translate来完成的,但是要命的是,不知道什么原因,这个函数的参数使用的是char*型的:
DWORD Translate( char* pSrc, char *pDest, int nSrcLen, int nDestLen );
这个时候我们可能就需要这个方法了:

void  CString::Release()
{
 
if (GetData() != _afxDataNil)
 
{
  
if (InterlockedDecrement(&GetData()->nRefs) <= 0)
   FreeData(GetData());
 }

}

其中 Release 就是用来判断该内存的被引用情况的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值