CString 类是我们经常用到的类,所以有必要对它的内存管理模式分析一下.
CString 内存管理的演变过程如下:
VC5 单纯的使用new delete方法。 因为字符串操作需要频繁调整内存大小.而采用C++操作符 new 与 delete 是没有与realloc相应功能的。结果就是每一次的改变内存大小都需要额外 增加一次拷贝操作。 而 new 与delete 在实现中在进程堆中分配。频繁地在堆上进行小内存分配与释放 必然在堆上产生大量碎片。堆碎片过多直接影响了程序效率。 于是MFC在VC6版本对此进行了改进。 VC6 对于大于512字节的内存和DEBUG模式下,CString仍然使用 new 和 delete来操纵。 在Release模式下不大于512字节的内存分配操作采用了内存池管理。 并将之细分为 <=64, <=128, <=256, <=512 字节4个内存池管理。 这样在不大于512字节的情况下CString有了很好的效率。 但是传说中有解决一个BUG就会产生另外一个BUG的定律。 CString 显然也无法避免它。 于是在VC7中又改了。 VC7 恢复使用C 的内存管理调用方式。即采用 alloc, free, realloc. CString存在的问题 就是由于new与delete没有realloc重新调整内存大小的功能。之前产生的问题导致最终 还是采用了C的管理方法。
在VC6中为了解决CString小内存操纵的性能问题 MFC在Release版本下对于不大于512字节的内存分配 采用的内存池管理来进行优化。其他情况下仍旧使用new 与delete.
Release版本下CString在处理不大于512Byte字串的内存时调用如下 VC6 中CString 分配内存与释放内存调用次序如下
CString::AllocBuffer CFixedAlloc::Alloc CPlex::Create
CString::FreeData CFixedAlloc::Free ========================================================================================= 相关代码引用如下:
FILE:MFC
/
SRC
/
STRCORE.CPP
void
CString::AllocBuffer(
int
nLen)
//
用来分配内存
...
{ ... #ifndef _DEBUG // 在Release 版本并且是不大于512字节 if (nLen <= 64) ...{ pData = (CStringData*)_afxAlloc64.Alloc(); pData->nAllocLength = 64; } else 分别为<= 1128, <=256 , <=512 ...{ ... } else #endif // DEBUG 和Release下大于512的 ...{ pData = (CStringData*) new BYTE[sizeof(CStringData) + (nLen+1)*sizeof(TCHAR)]; pData->nAllocLength = nLen; } ... }
void
FASTCALL CString::FreeData(CStringData
*
pData)
//
释放内存
...
{ #ifndef _DEBUG 在Release 版本并且是不大于512字节 int nLen = pData->nAllocLength; if (nLen == 64) // 根据内存大小分别调用管理器 _afxAlloc64.Free(pData); else if (nLen == 128) _afxAlloc128.Free(pData); else if (nLen == 256) _afxAlloc256.Free(pData); else if (nLen == 512) _afxAlloc512.Free(pData); else ...{ ASSERT(nLen > 512); delete[] (BYTE*)pData; } #else // DEBUG 和Release下大于512的 delete[] (BYTE*)pData; #endif }
_afxAlloc[64,128,256,512] 是CFixedAlloc类的全局对象。
我们分析一下CFixedAlloc是整样进行内存池管理的它在使用中又产生了什么问题?
class
CFixedAlloc
//
定义在 MFC/SRC/FIXALLOC.H文件中
...
{ public: CFixedAlloc(UINT nAllocSize, UINT nBlockSize = 64); UINT GetAllocSize() ...{ return m_nAllocSize; } public: void* Alloc(); //分配 由CString调用 void Free(void* p); //释放 由CString调用 void FreeAll(); //释放所有 被析构函数调用 public: ~CFixedAlloc(); protected: struct CNode...{//这个是用来实现一个单向链表 CNode* pNext; }; UINT m_nAllocSize; // 需要分配对象的大小仅由构造函数传入 UINT m_nBlockSize; // 预分配的数目即池的大小,由构造函数赋予,可知默认为64 CPlex* m_pBlocks; // 池的链表指针。CPlex对象含有一个CPlex* pNext指针对象, CNode* m_pNodeFree; // 被释放块链表的头指针,实际是应看做可用内存块链表 CRITICAL_SECTION m_protect;//临界区对象 }
;
/**/
/* 在Alloc的实现中我们可以看到,当池中没有可用块的时候 调用 CPlex::Create建立一块 m_nAllocSize * m_nBlockSize的内存池 如果有的话则从m_pNodeFree中弹出一块来使用 */
void
*
CFixedAlloc::Alloc()
...
{ if (m_pNodeFree == NULL)...{ //如果没有可用的内存块就进行分配一个池 CPlex* pNewBlock = NULL; TRY...{ // 分配内存块 默认是64个m_nAllocSize. pNewBlock = CPlex::Create(m_pBlocks, m_nBlockSize, m_nAllocSize); }CATCH_ALL(e)...{ ...异常 }END_CATCH_ALL // 下面的代码是将内存块压入m_pNodeFree链表中待用。 CNode* pNode = (CNode*)pNewBlock->data(); (BYTE*&)pNode += (m_nAllocSize * m_nBlockSize) - m_nAllocSize; for (int i = m_nBlockSize-1; i >= 0; i--, (BYTE*&)pNode -= m_nAllocSize) ...{ pNode->pNext = m_pNodeFree; m_pNodeFree = pNode; } } // 这两句是弹出一块内存给调用者使用。 void* pNode = m_pNodeFree; m_pNodeFree = m_pNodeFree->pNext; ... return pNode; }
/**/
/* 当调用者调用Free时,只是将这块内存重新压入m_pNodeFree链表中 并非释放,而是标志为可用块以待后用。 */
void
CFixedAlloc::Free(
void
*
p)
...
{ if (p != NULL) ...{ EnterCriticalSection(&m_protect); CNode* pNode = (CNode*)p; pNode->pNext = m_pNodeFree; m_pNodeFree = pNode; LeaveCriticalSection(&m_protect); } }
void
CFixedAlloc::FreeAll()
...
{ EnterCriticalSection(&m_protect); m_pBlocks->FreeDataChain(); m_pBlocks = NULL; m_pNodeFree = NULL; LeaveCriticalSection(&m_protect); }
/**/
/* 在析构函数中 调用FreeAll进行释放内存 */
CFixedAlloc::
~
CFixedAlloc()
...
{ FreeAll(); DeleteCriticalSection(&m_protect); }
/**/
/* MFC/INCLUDE/AFXPLEX_.H */
struct
CPlex
//
warning variable length structure
...
{ CPlex* pNext; void* data() ...{ return this+1; } static CPlex* PASCAL Create(CPlex*& head, UINT nMax, UINT cbElement); void FreeDataChain(); // free this one and links }
;
/**/
/* MFC/SRC/PLEX.CPP */
CPlex
*
PASCAL CPlex::Create(CPlex
*&
pHead, UINT nMax, UINT cbElement)
...
{ CPlex* p = (CPlex*) new BYTE[sizeof(CPlex) + nMax * cbElement]; p->pNext = pHead; pHead = p; // 加入链表 return p; }
void
CPlex::FreeDataChain()
//
free this one and links
...
{ CPlex* p = this; while (p != NULL)...{ BYTE* bytes = (BYTE*) p; CPlex* pNext = p->pNext; delete[] bytes; p = pNext; } }
============================================================================ 现在我们用一个实例来看一下在Release版本下的实际内存动作
以分配10000个 含有"abcdefghijklmnopqrstuvwxyz"串的CString数组
CString * strArray[10000];
for( int =0;i < 10000; i++ ) strArray[i] = new CString("abcdefghijklmnopqrstuvwxyz");
因为字符串小于64所以调用了_afxAlloc64::Alloc;
---------------------------------------------------------------- _afxAlloc64在STRCORE.CPP中被定义如下: AFX_STATIC CFixedAlloc _afxAlloc64(ROUND4(65*sizeof(TCHAR)+sizeof(CStringData)));
在ANSI版本下 sizeof(TCHAR) = 1 sizeof( CStrginData ) = 12; 65*sizeof(TCHAR)+sizeof(CStringData) = 77;
ROUND4定义用下,将之圆整为4的倍数, #define ROUND(x,y) (((x)+(y-1))&~(y-1)) #define ROUND4(x) ROUND(x, 4) 所以 _afxAlloc64(ROUND4(65*sizeof(TCHAR)+sizeof(CStringData))) 实际上 宏展开最终为 extern CFixedAlloc _afxAlloc64( 80,64); ----------------------------------------------------------------
在CPlex中分配池的大小 sizeof(CPlex) + nMax * cbElement = 4+80*64 = 5124 BYTE.
因为10000不是64的整数倍 = 要分配157个池 实际分配内存 = 157*5124 = 804468 BYTE = 804KB.
释放CString对象 for( int =0;i < 10000; i++ ) delete strArray[i];
此时CString 的调用_afxAlloc64.Free.
由CFixedAlloc::Free的实现可知此时并没有真正释放内存,只是将这该块重新加入m_pNodeFree链表中待用.
因为CFixedAlloc释放内存操作是在析构函数调用,而_afxAlloc64是被定义为全局对象.它的析构函数要到程序退出才能被调用.
所以CFixedAlloc分配的内存在程序结束之前只会增加而不能回收.
而如果我们重新分配10000个 字符串>64 <=128的的CString对象时 _afxAlloc64的内存占用依旧,而_afxAlloc128则重新分配了 157*(4+144*64) = 157*9220=1447540= 1.44754MB 再释放它,此时内存占用则为 1.44754MB+804KB = 2.252008MB.
与使用char*对象做比较:
char* chArray[10000]; 分配 "abcdefghijklmnopqrstuvwxyz" 实际内存是 27*10000 = 270KB 释放后内存即被回收 再分配128字串 10000个 内存是 129*10000 = 1.29MB. 释放后内存即被回收
结论: VC6中的CString采用内存池技术在改进小内存new与delete的性能与堆碎片问题后 又产生了一个不是内存泄露的内存泄露。
其实VC5,VC6中CString产生的问题是因为教条地尊守C++应当采用new与delete来管理内存的规则造成的
最终在VC7中 CString仍旧回到使用C方法上.
|