[Z]深入解析MFC -- CString的内存结构

VC6的时候记得看过CString的源代码,并不复杂,应该是从VC7开始,MFC和ATL共用一个CString了,新的CString使用了模板技术和其它技术,值得一提。

先看CString的定义:

typedef CAtlString CString;

如果想明确使用ANSI和UNICODE版本,可以使用CStringA和CStringW,看它们的定义:

typedef CAtlStringW CStringW;

typedef CAtlStringA CStringA;

以上三个Atl版本的String,其定义为:

typedef CStringT< wchar_t, StrTraitATL< wchar_t > > CAtlStringW;

typedef CStringT< char, StrTraitATL< char > > CAtlStringA;

typedef CStringT< TCHAR, StrTraitATL< TCHAR > > CAtlString;

因此,CStringT才是真实的CString类。

template< typename BaseType, class StringTraits >
class CStringT :
public CSimpleStringT< BaseType, _CSTRING_IMPL_::_MFCDLLTraitsCheck<BaseType, StringTraits>::c_bIsMFCDLLTraits >
{
};


CStringT有两个模板参数,第一个表明字符类型,第二个参数从源代码中知道是StrTraitATL:

template< typename _BaseType = char, class StringIterator = ChTraitsOS< _BaseType > >
class StrTraitATL : public StringIterator
{
public:
static HINSTANCE FindStringResourceInstance(__in UINT nID) throw()
{
return( AtlFindStringResourceInstance( nID ) );
}

static IAtlStringMgr* GetDefaultManager() throw()
{
return( &g_strmgr );
}
};


从类声明看到他提供了一个字符串管理器,字符迭代器和从资源中获得字符串的功能。字符串管理器比较重要,后面会提到。

CStringT没有成员变量,封装了很多好用的字符串函数,数据在基类CSimpleStringT中。

CSimpleStringT只有一个成员变量m_pszData,不要小看这个变量,在它身上,有着很多秘密,绝对不是他的声明那么朴实:

PXSTR m_pszData;

PXSTR就是char或者wchar_t,只不过被模板的特化技术封装了一下。CSimpleStringT没有直接操作m_pszData,而是通过成员函数GetData来获得,看一下这个函数:

CStringData* GetData() const throw()
{
return( reinterpret_cast< CStringData* >( m_pszData )-1 );
}

 

这个函数将m_pszData指向的内存转成CStringData类型,然后往前移动sizeof(CStringData)的长度,指向了一个CStringData对象。m_pszData的秘密就在此处,实际上,每次分配内存时,都会多分配一段sizeof(CStringData)长度的内存,最前面这段数据格式化为CStringData对象,然后m_pszData指向其后的数据,这才是字符串。
|_______________|___________________________________________________|
CStringData     m_pszData

再看CStringData的声明:

struct CStringData
{
IAtlStringMgr* pStringMgr; // String manager for this CStringData
int nDataLength; // Length of currently used data in XCHARs (not including terminating null)
int nAllocLength; // Length of allocated data in XCHARs (not including terminating null)
long nRefs; // Reference count: negative == locked
// XCHAR data[nAllocLength+1] // A CStringData is always followed in memory by the actual array of character data
};


CStringData包含了这个字符串的所有信息,包括字符串长度,内存长度和引用计数,另外还有一个字符串管理器指针,这个指针从前面所提到的模板参数StrTraitATL中得到。再看看接口IAtlStringMgr的声明:

__interface IAtlStringMgr
{
public:
// Allocate a new CStringData
CStringData* Allocate( int nAllocLength, int nCharSize ) throw();
// Free an existing CStringData
void Free( CStringData* pData ) throw();
// Change the size of an existing CStringData
CStringData* Reallocate( CStringData* pData, int nAllocLength, int nCharSize ) throw();
// Get the CStringData for a Nil string
CStringData* GetNilString() throw();
IAtlStringMgr* Clone() throw();
};


IAtlStringMgr提供了字符串内存的分配和销毁。具体实现参考类CAfxStringMgr。

我们还是先看看一个字符串是如何赋值的吧,给下面第二行代码加上断点,调试进入:

CString s;
s = L"Hello world";

一直跟踪到wmemcpy_s函数,才找到了拷贝的函数,以下是堆栈:

s = L"Hello world";
CStringT::operator=()                                              // a
CSimpleStringT::operator=()                                        // b
SetString(const wchar_t * pszSrc)                                  // c
SetString(const wchar_t * pszSrc, int nLength)                     // d
CopyChars                                                          // e
一行行来分析:
a,调用基类CSimpleStringT的操作符=
b,调用SetString
c,求字符串pszSrc长度,调用两个参数的SetString
d,调用GetBuffer分配内存空间,然后判断新字符串是否与现有字符串重叠,重叠调用CopyCharsOverlapped拷贝新字符串,不重叠调用CopyChars
e,CopyChars就是wmemcpy_s

在上面的步骤中,值得关注的GetBuffer函数。我们继续进入这个函数看看发生了什么:

SetString
GetBuffer                                          // a
PrepareWrite                                       // b
PrepareWrite2                                      // c
Fork                                               // d
CAfxStringMgr::Allocate                            // e
_malloc_dbg                                        // f

a,调用PrepareWrite
b,判断引用计数是否大于1或者长度不够,满足任意一个条件,都需要调用PrepareWrite重新分配内存,否则直接返回m_pszData
c,首先判断新申请的长度是否小于已经内存长度,如果小于,则将长度设置为已有内存长度。然后判断引用计数是否大于1,是则调用Fork重新申请内存。如果引用计数不大于1,则看是否需要申请内存,是则重新分配。重新分配有一个简单的原则,以下这段比较清楚:

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


d,取出旧的CStringData,克隆一个IAtlStringMgr,并分配nLength长度的内存。格式化新分配的内存,并释放旧内存。注意释放的方式,并不是直接free内存,而是调用了CStringData的Release:

void Release() throw()
{
ATLASSERT( nRefs != 0 );

if( _AtlInterlockedDecrement( &nRefs ) <= 0 )
{
pStringMgr->Free( this );
}
}


可以看到只有引用计数小于或等于0了,才会直接free内存。具体看代码吧:

CStringData* CAfxStringMgr::Allocate( int nChars, int nCharSize ) throw()
{
size_t nTotalSize;
CStringData* pData;
size_t nDataBytes;

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


可以看到实际分配的内存大小是字符串长度+1个NULL+sizeof(CStringData)
 

原文:http://blog.csdn.net/huxin1/article/details/4310964

 

附:

CString不象其他数据类型,它似乎是没有长度限制的,为什么呢?因为CString并非是你赋予多大长度的字符串就占有多大的内存空间。它的空间分配机制是事先申请一个比较大的内存空间来存放字符串,在以后的操作中如果字符串超出了这个内存区域,它才会先释放原先的内存区域,重新申请一个更大的内存空间。同样,如果字符串变短了,它也不是立即释放多余空间,而是累积到了一定程度才释放。这样实现了“无长度限制”,又避免了频繁的申请、释放内存的操作。 CString的另外一个特色就是“写入复制技术(CopyBeforeWrite)”。当使用一个CString对象A来初始化另外一个CString对象B时,B并不会被分配空间,而是将自己的指针指向对象A的存储空间。除非对两个中的某个做修改时,才会为对象B申请内存。如此看来,这些特性又是如何实现的呢?可以肯定的是,这需要很多附加信息的支持,而非仅仅只有一个指针这么简单。

文章出处:飞诺网(www.diybl.com):http://www.diybl.com/course/3_program/c++/cppxl/2008930/147214.html

你可能感兴趣的:(String)