MFC中CString实现原理简单介绍

MFC里的关于Cstring的类的实现大部分在strcore.cpp中。

Cstring里需要有一个用来存放字符串的缓冲区,并且有一个指针指向该缓冲区,该指针就是LPTSTR m_pchData。但是有些字符串操作会增建或减少字符串的长度,因此为了减少频繁的申请内存或者释放内存,Cstring会先申请一个大的内存块用来存放字符串。当增加后的字符串长度超过预先申请的内存时,Cstring先释放原先的内存,然后再重新申请一个更大的内存块。同样的,当字符串长度减少时,也不释放多出来的内存空间。而是等到积累到一定程度时,才一次性将多余的内存释放。

    还有,当使用一个Cstring对象a来初始化另一个Cstring对象b时,为了节省空间,新对象b并不分配空间,它所要做的只是将自己的指针指向对象a的那块内存空间,只有当需要修改对象a或者b中的字符串时,才会为新对象b申请内存空间,这叫做写入复制技术(CopyBeforeWrite)。

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

这种结构的数据结构的申请方法是这样实现的:

pData = (CStringData*) new BYTE[sizeof(CStringData) + (nLen+1)*sizeof(TCHAR)];

pData->nAllocLength = nLen;

其中nLen是用于说明需要一次性申请的内存空间的大小的。

    LPTSTR CString::GetBuffer(int nMinBufLength),它的实现方法是:

首先通过Cstring::GetData()取得CstringData对象的指针。该指针是通过存放字符串的指针m_pchData先后偏移sizeof(CstringData),从而得到了CstringData的地址。然后根据参数nMinBufLength给定的值重新实例化一个CstringData对象,使得新的对象里的字符串缓冲长度能够满足nMinBufLength。

然后在重新设置一下新的CstringData中的一些描述值。最后将新CstringData对象里的字符串缓冲直接返回给调用者。

 这些过程用C++代码描述就是:  

    if (nMinBufLength > GetData()->nAllocLength)
    {        
        // we have to grow the buffer        
        CStringData* pOldData = GetData();        

        AllocBuffer(nMinBufLength);        
        memcpy(m_pchData->data(), pOldData->data(), (nOldLen+1)*sizeof(TCHAR));
        
        GetData()->nDataLength = nOldLen;
        
        CString::Release(pOldData);        
    }
    
    return m_pchData;

    很多时候,我们经常的对大批量的字符串进行互相拷贝修改等,Cstring 使用了CopyBeforeWrite技术。

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());
    }
}

 存在这些问题的operations介绍。

1.      GetBuffer

对于这个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

   原来在对GetBuffer返回的指针使用之后需要调用ReleaseBuffer,这样才能使用其他Cstring的operations。上面的代码中,我们在4-5处增建一行代码:str1.ReleaseBuffer(),然后再观察nNewLen,发现这个时候已经是我们想要的值8了。

GetBuffer返回的是CstringData对象里的字符串缓冲的首地址

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

 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工作不正常,并且这样也很容易造成缓冲溢出。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值