MFC的CString的内部实现分析

MFC的CString是字符串管理类,其为了实现高效率的缓冲管理,使用了引用记数及CopyBeforeWrite技术。这在一定程度上加大了其神秘感和理解难度。好在他的代码是公开的,所以可以给我们研究它的内部实现提供条件。下面就来看看到底是如何实现的。由于不同版本的MSVC其CString实现有些许差别,下面是针对VS2003来说明的。

 

由于它的基础类使用了模板技术,可以实现内存管理的定制,我们只研究MFC的默认内存管理方式。即CAfxStringMgr类提供的方式。MFC在strcore.cpp定义了它的一个全局实例:CAfxStringMgr afxStringManager。在特性模板参数中通过

AfxGetStringManager() 将 afxStringManager作为默认的管理对象指针。

 

 

下面从具体的代码来查看其执行机制:

 

CString str( _T("abc") );

 

这个语句定义了字串对象str,并初始化为"abc"。

 

看它到底是如何执行的吧。

 

首先CString是 CStringT 的TCHAR字符类型版本(自适应Unicode及Ascii型)。

typedef ATL::CStringT< TCHAR, StrTraitMFC< TCHAR > > CString;

 

 

所以它的构造函数是调用了

CStringT::CStringT( const XCHAR* pszSrc ) : CThisSimpleString( StringTraits::GetDefaultManager() ) { if( !CheckImplicitLoad( pszSrc ) ) { // nDestLength is in XCHARs *this = pszSrc; } }

 

可见在构造函数使用CThisSimpleString( StringTraits::GetDefaultManager() ) 指定了内存管理对象为afxStringManager。记住这个MFC默认的内存管理返回的字串内存为空,但带了一个内存分配器,为以后的内存分配做准备。因为字串内存为空,所以后面的赋值操作将激发内存分配操作。

 

然后调用赋值语句

CStringT& operator=( PCXSTR pszSrc ) { CThisSimpleString::operator=( pszSrc ); return( *this ); }

这里又将处理转向了基类CSimpleString的赋值语句

CSimpleStringT& operator=( PCXSTR pszSrc ) { SetString( pszSrc ); return( *this ); }

 

下面就进入了字串赋值的实际代码

void SetString( PCXSTR pszSrc ) { SetString( pszSrc, StringLength( pszSrc ) ); } void SetString( PCXSTR pszSrc, int nLength ) { if( nLength == 0 ) { Empty(); } else { // It is possible that pszSrc points to a location inside of our // buffer. GetBuffer() might change m_pszData if (1) the buffer // is shared or (2) the buffer is too small to hold the new // string. We detect this aliasing, and modify pszSrc to point // into the newly allocated buffer instead. if(pszSrc == NULL) AtlThrow(E_INVALIDARG); UINT nOldLength = GetLength(); UINT_PTR nOffset = pszSrc-GetString(); // If 0 <= nOffset <= nOldLength, then pszSrc points into our // buffer PXSTR pszBuffer = GetBuffer( nLength ); if( nOffset <= nOldLength ) { CopyCharsOverlapped( pszBuffer, pszBuffer+nOffset, nLength ); } else { CopyChars( pszBuffer, pszSrc, nLength ); } ReleaseBufferSetLength( nLength ); } }

 

可以看出它考虑了新旧字串地址的重叠问题,并做了不同处理。

其中最关键的函数是GetBuffer(nLength);

PXSTR GetBuffer( int nMinBufferLength ) { return( PrepareWrite( nMinBufferLength ) ); } PXSTR PrepareWrite( int nLength ) { CStringData* pOldData = GetData(); int nShared = 1-pOldData->nRefs; // nShared < 0 means true, >= 0 means false int nTooShort = pOldData->nAllocLength-nLength; // nTooShort < 0 means true, >= 0 means false if( (nShared|nTooShort) < 0 ) // If either sign bit is set (i.e. either is less than zero), we need to copy data { PrepareWrite2( nLength ); } return( m_pszData ); } ATL_NOINLINE void PrepareWrite2( int nLength ) { CStringData* pOldData = GetData(); if( pOldData->nDataLength > nLength ) { nLength = pOldData->nDataLength; } if( pOldData->IsShared() ) { Fork( nLength ); } else if( pOldData->nAllocLength < nLength ) { // Grow exponentially, until we hit 1K. int nNewLength = pOldData->nAllocLength; if( nNewLength > 1024 ) { nNewLength += 1024; } else { nNewLength *= 2; } if( nNewLength < nLength ) { nNewLength = nLength; } Reallocate( nNewLength ); } }

 

 

其中

if( (nShared|nTooShort) < 0 )  // If either sign bit is set (i.e. either is less than zero), we need to copy data
  {
   PrepareWrite2( nLength );
  }

 

说明假设字串缓冲是共享的(也就是多个CString对象共用),将激发Fork( nLength );,假如内存太小,将进行内存扩大操作。

 

在本例中,也许大家觉得这个初始时内存为0,肯定进行内存扩大操作。但实际上进入的是Fork(nLength);,因为原来初始化的默认内存管理器所返回的内存天生属于共享的。

class CNilStringData : public CStringData { public: CNilStringData() throw() { pStringMgr = NULL; nRefs = 2; // Never gets freed by IAtlStringMgr nDataLength = 0; nAllocLength = 0; achNil[0] = 0; achNil[1] = 0; } void SetManager( IAtlStringMgr* pMgr ) throw() { ATLASSERT( pStringMgr == NULL ); pStringMgr = pMgr; } public: wchar_t achNil[2]; };

 

 

nRefs = 2;  // Never gets freed by IAtlStringMgr

 

这里将默认返回的内存的nRefs设置为2,就是初始的字串都共享原始的那个长度为0的缓冲。

 

下面看看Fork函数如何处理:

ATL_NOINLINE void Fork( int nLength ) { CStringData* pOldData = GetData(); int nOldLength = pOldData->nDataLength; CStringData* pNewData = pOldData->pStringMgr->Clone()->Allocate( nLength, sizeof( XCHAR ) ); if( pNewData == NULL ) { ThrowMemoryException(); } int nCharsToCopy = ((nOldLength < nLength) ? nOldLength : nLength)+1; // Copy '/0' CopyChars( PXSTR( pNewData->data() ), PCXSTR( pOldData->data() ), nCharsToCopy ); pNewData->nDataLength = nOldLength; pOldData->Release(); Attach( pNewData ); }

 

 

我们看到这里对缓冲内存进行了重新分配。同时对旧的缓冲调用Release。

 

其中用到了默认的内存管理器进行内存分配操作

pOldData->pStringMgr->Clone()->Allocate( nLength, sizeof( XCHAR ) );

 

这个分配操作就是内部引用计数的关键部分。

CStringData* CAfxStringMgr::Allocate( int nChars, int nCharSize ) { size_t nTotalSize; CStringData* pData; size_t nDataBytes; ASSERT(nCharSize > 0); if(nChars < 0) { ASSERT(FALSE); return NULL; } nDataBytes = (nChars+1)*nCharSize; nTotalSize = sizeof( CStringData )+nDataBytes; pData = (CStringData*)malloc( nTotalSize ); if (pData == NULL) return NULL; pData->pStringMgr = this; pData->nRefs = 1; pData->nAllocLength = nChars; pData->nDataLength = 0; return pData; }

 

 

我们看到这里的分配操作额外分配了CStringData大小的数据在缓冲的前面,该数据用于对缓冲区进行引用计数。从而实现了缓冲的共享。

 

然后其他操作时只要涉及数据Clone或释放操作时都将对计数进行增加或减少,从而达到缓冲共享,并在不使用时能够及时回收。

 

 

你可能感兴趣的:(C++语言)