MFC的CString的内部实现分析
2011-03-24 19:14
363 查看
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;
所以它的构造函数是调用了
view plaincopy to clipboardprint?
CStringT::CStringT( const XCHAR* pszSrc ) :
CThisSimpleString( StringTraits::GetDefaultManager() )
{
if( !CheckImplicitLoad( pszSrc ) )
{
// nDestLength is in XCHARs
*this = pszSrc;
}
}
可见在构造函数使用CThisSimpleString( StringTraits::GetDefaultManager() ) 指定了内存管理对象为afxStringManager。记住这个MFC默认的内存管理返回的字串内存为空,但带了一个内存分配器,为以后的内存分配做准备。因为字串内存为空,所以后面的赋值操作将激发内存分配操作。
然后调用赋值语句
view plaincopy to clipboardprint?
CStringT& operator=( PCXSTR pszSrc )
{
CThisSimpleString::operator=( pszSrc );
return( *this );
}
这里又将处理转向了基类CSimpleString的赋值语句
view plaincopy to clipboardprint?
CSimpleStringT& operator=( PCXSTR pszSrc )
{
SetString( pszSrc );
return( *this );
}
下面就进入了字串赋值的实际代码
view plaincopy to clipboardprint?
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);
view plaincopy to clipboardprint?
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);,因为原来初始化的默认内存管理器所返回的内存天生属于共享的。
view plaincopy to clipboardprint?
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函数如何处理:
view plaincopy to clipboardprint?
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 ) );
这个分配操作就是内部引用计数的关键部分。
view plaincopy to clipboardprint?
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或释放操作时都将对计数进行增加或减少,从而达到缓冲共享,并在不使用时能够及时回收。
由于它的基础类使用了模板技术,可以实现内存管理的定制,我们只研究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;
所以它的构造函数是调用了
view plaincopy to clipboardprint?
CStringT::CStringT( const XCHAR* pszSrc ) :
CThisSimpleString( StringTraits::GetDefaultManager() )
{
if( !CheckImplicitLoad( pszSrc ) )
{
// nDestLength is in XCHARs
*this = pszSrc;
}
}
可见在构造函数使用CThisSimpleString( StringTraits::GetDefaultManager() ) 指定了内存管理对象为afxStringManager。记住这个MFC默认的内存管理返回的字串内存为空,但带了一个内存分配器,为以后的内存分配做准备。因为字串内存为空,所以后面的赋值操作将激发内存分配操作。
然后调用赋值语句
view plaincopy to clipboardprint?
CStringT& operator=( PCXSTR pszSrc )
{
CThisSimpleString::operator=( pszSrc );
return( *this );
}
这里又将处理转向了基类CSimpleString的赋值语句
view plaincopy to clipboardprint?
CSimpleStringT& operator=( PCXSTR pszSrc )
{
SetString( pszSrc );
return( *this );
}
下面就进入了字串赋值的实际代码
view plaincopy to clipboardprint?
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);
view plaincopy to clipboardprint?
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);,因为原来初始化的默认内存管理器所返回的内存天生属于共享的。
view plaincopy to clipboardprint?
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函数如何处理:
view plaincopy to clipboardprint?
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 ) );
这个分配操作就是内部引用计数的关键部分。
view plaincopy to clipboardprint?
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或释放操作时都将对计数进行增加或减少,从而达到缓冲共享,并在不使用时能够及时回收。
相关文章推荐
- MFC的CString的内部实现分析
- MFC工具条和状态栏,内部实现原理详细分析
- ENode 2.0 - 深入分析ENode的内部实现流程和关键地方的幂等设计
- [转]MFC的CString(VC6) 内存管理分析
- Delphi.NET 内部实现分析(1)
- 从MFC消息映射宏分析MFC消息映射的实现
- MFC Ribbon UI 弹出菜单实现分析
- MFC的窗口分割的设计与实现以及CSplitterWnd 类分析
- BUG_ON内部实现分析
- Android AsyncTask 分析内部实现
- Delphi.NET 内部实现分析(3.3)
- mysql内核分析--innodb哈希表的内部实现(上)
- MFC的CString(VC6) 内存管理分析
- redis源码分析 dict字典的实现和内部应用
- C++的string实现MFC的CString::GetBuffer
- Task运行过程分析3——Map Task内部实现
- Delphi.NET 内部实现分析(3.2)
- Delphi.NET 内部实现分析(1,2,3)(转贴)
- mysql内核分析--innodb动态数组内部实现(上) (摘自老杨)
- MFC的CString(VC6) 内存管理分析