MFC的CString(VC6) 内存管理分析

原创 2007年09月29日 14:38:00

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::CreateCString::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方法上.

CString 内存分配机制

      CString比起STL的string来说,有很多方便的地方。许多有经验的作者在他们的文章里都写过,string是一个很好用的类型,但是往往MFC程序里的许多BUG就是它引起的,典型的漏洞...

Cstring 内存分配机制

CString比起STL的string来说,有很多方便的地方。许多有经验的作者在他们的文章里都写过,string是一个很好用的类型,但是往往MFC程序里的许多BUG就是它引起的,典型的漏洞有:缓冲溢出...

WinSock完成端口I/O模型

关于重叠I/O,参考《WinSock重叠I/O模型》;关于完成端口的概念及内部机制,参考译文《深度探索I/O完成端口》。 完成端口对象取代了WSAAsyncSelect中的消息驱动和WSAEvent...

WinSock完成端口I/O模型

关于重叠I/O,参考《WinSock重叠I/O模型》;关于完成端口的概念及内部机制,参考译文《深度探索I/O完成端口》。 完成端口对象取代了WSAAsyncSelect中的消息驱动和WSAEvent...

MFC基于CPlex结构的内存池化管理

CPlex,CFixedAlloc,Memory Pool
  • phunxm
  • phunxm
  • 2010年06月17日 22:16
  • 4506

内存池技术的应用和详细说明

为了控制CE的串口反复不断的分配内存,出现内存碎片,防止出现内存泄露,于是把从MFC上学到的那个内存池简化了一下,直接用到了程序上 ,虽然很简单,但是如果只要稍加二次封装,即可写出类似于MFC中...
  • joji_h
  • joji_h
  • 2014年06月06日 11:11
  • 1276

用js在前台及后台生成随机字符串

通过javascript 在 web 前端和后端各生成随机字符串。可能起到密码、验证码、随机数、任意字串等作用....
  • cuckoo1
  • cuckoo1
  • 2016年06月02日 12:51
  • 523

java网络编程之下载文件通过多线程分块下载(二)

import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.RandomAc...

CString 内存管理解析

在程序开发中,字符串是我们经常使用的一个东西.在C语言中,我们经常使用char*来操作字符串.char*虽然在使用上比较直观,但在内存管理上面却不怎么方便.手动开辟和释放内存也是一项比较麻烦的操作.而...
  • liyafu
  • liyafu
  • 2011年05月15日 10:45
  • 951

xv6源码分析(四):内存管理

xv6通过页表机制实现了对内存空间的控制。页表使得 xv6 能够让不同进程各自的地址空间映射到相同的物理内存上,还能够为不同进程的内存提供保护。 除此之外,我们还能够通过使用页表来间接地实现一些特殊功...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:MFC的CString(VC6) 内存管理分析
举报原因:
原因补充:

(最多只允许输入30个字)