以下来源http://www.cnblogs.com/JCSU/articles/1051826.html
程序员们经常编写内存管理程序,往往提心吊胆。如果不想触雷,唯一的解决办法就是发现所有潜伏的地雷并且排除它们,躲是躲不了的。本文的内容比一般教科书的要深入得多,读者需细心阅读,做到真正地通晓内存管理。
内存分配方式
(1)从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
(2)在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
(3) 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。
常见的内存错误及其对策
发生内存错误是件非常麻烦的事情。编译器不能自动发现这些错误,通常是在程序运行时才能捕捉到。而这些错误大多没有明显的症状,时隐时现,增加了改错的难度。有时用户怒气冲冲地把你找来,程序却没有发生任何问题,你一走,错误又发作了。 常见的内存错误及其对策如下:
* 内存分配未成功,却使用了它
编程新手常犯这种错误,因为他们没有意识到内存分配会不成功。常用解决办法是,在使用内存之前检查指针是否为NULL。如果指针p是函数的参数,那么在函数的入口处用assert(p!=NULL)进行检查。如果是用malloc或new来申请内存,应该用if(p==NULL) 或if(p!=NULL)进行防错处理。
* 内存分配虽然成功,但是尚未初始化就引用它
犯这种错误主要有两个起因:一是没有初始化的观念;二是误以为内存的缺省初值全为零,导致引用初值错误(例如数组)。内存的缺省初值究竟是什么并没有统一的标准,尽管有些时候为零值,我们宁可信其无不可信其有。所以无论用何种方式创建数组,都别忘了赋初值,即便是赋零值也不可省略,不要嫌麻烦。
* 内存分配成功并且已经初始化,但操作越过了内存的边界
例如在使用数组时经常发生下标“多1”或者“少1”的操作。特别是在for循环语句中,循环次数很容易搞错,导致数组操作越界。
* 忘记了释放内存,造成内存泄露
含有这种错误的函数每被调用一次就丢失一块内存。刚开始时系统的内存充足,你看不到错误。终有一次程序突然死掉,系统出现提示:内存耗尽。
动态内存的申请与释放必须配对,程序中malloc与free的使用次数一定要相同,否则肯定有错误(new/delete同理)。
* 释放了内存却继续使用它
有三种情况:
(1)程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。
(2)函数的return语句写错了,注意不要返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。
(3)使用free或delete释放了内存后,没有将指针设置为NULL。导致产生“野指针”。
【规则1】用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。
【规则2】不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。
【规则3】避免数组或指针的下标越界,特别要当心发生“多1”或者“少1”操作。
【规则4】动态内存的申请与释放必须配对,防止内存泄漏。
【规则5】用free或delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。
指针与数组的对比
C++/C程序中,指针和数组在不少地方可以相互替换着用,让人产生一种错觉,以为两者是等价的。
数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。数组名对应着(而不是指向)一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。
指针可以随时指向任意类型的内存块,它的特征是“可变”,所以我们常用指针来操作动态内存。指针远比数组灵活,但也更危险。
下面以字符串为例比较指针与数组的特性。
1 修改内容
下例中,字符数组a的容量是6个字符,其内容为hello。a的内容可以改变,如a[0]= 'x'。指针p指向常量字符串"world"(位于静态存储区,内容为world),常量字符串的内容是不可以被修改的。从语法上看,编译器并不觉得语句 p[0]= 'x'有什么不妥,但是该语句企图修改常量字符串的内容而导致运行错误。
![](https://i-blog.csdnimg.cn/blog_migrate/f0cd6c7f9e7ae96feae062cb48f670f0.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/f0cd6c7f9e7ae96feae062cb48f670f0.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/f0cd6c7f9e7ae96feae062cb48f670f0.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/34031c708bfe702fe82d01ff5c6593aa.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/0be121fa5b8988fbabbbc526af3b0fc0.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/b854634c0904529d4018c4c3336be836.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/587e34b10dcf5efbc0859b53470a2db3.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/587e34b10dcf5efbc0859b53470a2db3.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/587e34b10dcf5efbc0859b53470a2db3.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/587e34b10dcf5efbc0859b53470a2db3.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/587e34b10dcf5efbc0859b53470a2db3.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/587e34b10dcf5efbc0859b53470a2db3.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/4fd96b3cf02f4c7b5c8964ac8167f7af.gif)
2 内容复制与比较
不能对数组名进行直接复制与比较。下例中,若想把数组a的内容复制给数组b,不能用语句 b = a ,否则将产生编译错误。应该用标准库函数strcpy进行复制。同理,比较b和a的内容是否相同,不能用if(b==a) 来判断,应该用标准库函数strcmp进行比较。
语句p = a 并不能把a的内容复制指针p,而是把a的地址赋给了p。要想复制a的内容,可以先用库函数malloc为p申请一块容量为strlen(a)+1个字符的内存,再用strcpy进行字符串复制。同理,语句if(p==a) 比较的不是内容而是地址,应该用库函数strcmp来比较。
![](https://i-blog.csdnimg.cn/blog_migrate/f0cd6c7f9e7ae96feae062cb48f670f0.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/f0cd6c7f9e7ae96feae062cb48f670f0.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/f0cd6c7f9e7ae96feae062cb48f670f0.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/f0cd6c7f9e7ae96feae062cb48f670f0.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/f0cd6c7f9e7ae96feae062cb48f670f0.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/f0cd6c7f9e7ae96feae062cb48f670f0.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/f0cd6c7f9e7ae96feae062cb48f670f0.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/f0cd6c7f9e7ae96feae062cb48f670f0.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/f0cd6c7f9e7ae96feae062cb48f670f0.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/f0cd6c7f9e7ae96feae062cb48f670f0.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/f0cd6c7f9e7ae96feae062cb48f670f0.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/f0cd6c7f9e7ae96feae062cb48f670f0.gif)
3 计算内存容量
用运算符sizeof可以计算出数组的容量(字节数)。下例(a)中,sizeof(a)的值是12(注意别忘了' ')。指针p指向a,但是 sizeof(p)的值却是4。这是因为sizeof(p)得到的是一个指针变量的字节数,相当于sizeof(char*),而不是p所指的内存容量。C++/C语言没有办法知道指针所指的内存容量,除非在申请内存时记住它。注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。下例(b)中,不论数组a的容量是多少,sizeof(a)始终等于sizeof(char *)。
示例(a)![](https://i-blog.csdnimg.cn/blog_migrate/f0cd6c7f9e7ae96feae062cb48f670f0.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/f0cd6c7f9e7ae96feae062cb48f670f0.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/f0cd6c7f9e7ae96feae062cb48f670f0.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/f0cd6c7f9e7ae96feae062cb48f670f0.gif)
示例(b)
![](https://i-blog.csdnimg.cn/blog_migrate/f0cd6c7f9e7ae96feae062cb48f670f0.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/34031c708bfe702fe82d01ff5c6593aa.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/0be121fa5b8988fbabbbc526af3b0fc0.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/b854634c0904529d4018c4c3336be836.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/587e34b10dcf5efbc0859b53470a2db3.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/4fd96b3cf02f4c7b5c8964ac8167f7af.gif)
来源http://www.cnblogs.com/growup/archive/2011/06/27/2091101.html
new与malloc
(收集整理from web)
1.malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存
2.对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
3.因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。
4.C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存。
5.new可以认为是malloc加构造函数的执行。new出来的指针是直接带类型信息的。而malloc返回的都是void*指针。
new delete在实现上其实调用了malloc,free函数
6.new建立的对象你可以把它当成一个普通的对象,用成员函数访问,不要直接访问它的地址空间;malloc分配的是一块内存区域,就用指针访问好了,而且还可以在里面移动指针.
7.new 建立的是一个对象;alloc分配的是一块内存.
***************************************
相同点:都可用于申请动态内存和释放内存
不同点:
(1)操作对象有所不同。
malloc与free是C++/C 语言的标准库函数,new/delete 是C++的运算符。对于非内部数据类的对象而言,光用maloc/free 无法满足动态对象的要求。对象在创建的同时要自动执行构造函数, 对象消亡之前要自动执行析构函数。由于malloc/free 是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加malloc/free。
(2)在用法上也有所不同。
函数malloc 的原型如下:
void * malloc(size_t size);
用malloc 申请一块长度为length 的整数类型的内存,程序如下:
int *p = (int *) malloc(sizeof(int) * length);
我们应当把注意力集中在两个要素上:“类型转换”和“sizeof”。
malloc 返回值的类型是void *,所以在调用malloc 时要显式地进行类型转换,将void * 转换成所需要的指针类型。
malloc 函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数。
函数free 的原型如下:
void free( void * memblock );
为什么free 函数不象malloc 函数那样复杂呢?这是因为指针p 的类型以及它所指的内存的容量事先都是知道的,语句free(p)能正确地释放内存。如果p 是NULL 指针,那么free
对p 无论操作多少次都不会出问题。如果p 不是NULL 指针,那么free 对p连续操作两次就会导致程序运行错误。
new/delete 的使用要点
运算符new 使用起来要比函数malloc 简单得多,例如:
int *p1 = (int *)malloc(sizeof(int) * length);
int *p2 = new int[length];
这是因为new 内置了sizeof、类型转换和类型安全检查功能。对于非内部数据类型的对象而言,new 在创建动态对象的同时完成了初始化工作。如果对象有多个构造函数,那么new 的语句也可以有多种形式。
如果用new 创建对象数组,那么只能使用对象的无参数构造函数。例如
Obj *objects = new Obj[100]; // 创建100 个动态对象
不能写成
Obj *objects = new Obj[100](1);// 创建100 个动态对象的同时赋初值1
在用delete 释放对象数组时,留意不要丢了符号‘[]’。例如
delete []objects; // 正确的用法
delete objects; // 错误的用法
后者相当于delete objects[0],漏掉了另外99 个对象。
***************************************
1 new自动计算需要分配的空间,而malloc需要手工计算字节数
2 new是类型安全的,而malloc不是,比如:
int* p = new float[2]; // 编译时指出错误
int* p = malloc(2*sizeof(float)); // 编译时无法指出错误
new operator 由两步构成,分别是 operator new 和 construct
3 operator new对应于malloc,但operator new可以重载,可以自定义内存分配策略,甚至不做内存分配,甚至分配到非内存设备上。而malloc无能为力
4 new将调用constructor,而malloc不能;delete将调用destructor,而free不能。
5 malloc/free要库文件支持,new/delete则不要。
以下来源http://www.cnblogs.com/xinyuyuanm/archive/2013/05/09/3069918.html
内存分配C/C++堆、栈及静态数据区详解
本文纯属个人见解,是对面前学习的总结,如有描述不正确的地方还请高手指正~
五大内存分区
在C++中,内存成分5个区,他们分别是堆、栈、自在存储区、全局/态静存储区和常量存储区。
栈,就是那些由编译器在须要的时候分配,在不须要的时候主动清晰的变量的存储区。面里的变量通常是局部变量、函数数参等。
堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的用应序程去制控,一般一个new就要对应一个delete。如果序程员没有释放掉,那么在序程束结后,操纵系统会主动回收。
自在存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来束结自己的性命的。
全局/态静存储区,全局变量和态静变量被分配到统一块内存中,在之前的C语言中,全局变量又分为初始化的和未初始化的,在C++面里没有这个分区了,他们独特占用统一块内存区。
常量存储区,这是一块比拟殊特的存储区,他们面里放存的是常量,不答应改修(当然,你要通过非合法段手也可以改修,而且方法很多)
确明分区堆与栈
在bbs上,堆与栈的分区问题,似乎是一个恒永的话题,由此可见,初学者对此往往是淆混不清的,所以我定决拿他第一个开刀。
首先,我们举一个例子:
void f() { int* p=new int[5]; }
这条短短的一句话就包含了堆与栈,看到new,我们首先就应该想到,我们分配了一块堆内存,那么指针p呢?他分配的是一块栈内存,所以这句话的意思就是:在栈内存中放存了一个指向一块堆内存的指针p。在序程会先确定在堆中分配内存的巨细,然后调用operator new分配内存,然后返回这块内存的首址地,放入栈中,他在VC6下的汇编代码如下:
00401028 push 14h
0040102A call operator new (00401060)
0040102F add esp,4
00401032 mov dword ptr [ebp-8],eax
00401035 mov eax,dword ptr [ebp-8]
00401038 mov dword ptr [ebp-4],eax
这里,我们为了单简并没有释放内存,那么该怎么去释放呢?是delete p么?澳,错了,应该是delete []p,这是为了诉告编译器:我除删的是一个组数,VC6就会根据应相的Cookie信息去行进释放内存的作工。
好了,我们回到我们的主题:堆和栈究竟有什么区别?
要主的区别由以下几点:
1、管理方法不同;
2、空间巨细不同;
3、是否生产碎片不同;
4、成长方向不同;
5、分配方法不同;
6、分配效率不同;
管理方法:对于栈来讲,是由编译器主动管理,无需我们手工制控;对于堆来讲,释放作工由序程员制控,易容生产memory leak。
空间巨细:一般来讲在32位系统下,堆内存可以到达4G的空间,从这个度角来看堆内存几乎是没有什么制约的。但是对于栈来讲,一般都是有必定的空间巨细的,例如,在VC6面下,默许的栈空间巨细是1M(好像是,记不清晰了)。当然,我们可以改修:
打开工程,次依操纵菜单如下:Project->Setting->Link,在Category 中中选Output,然后在Reserve中设定堆栈的最大值和commit。
注意:reserve最小值为4Byte;commit是保留在虚拟内存的页件文面里,它设置的较大会使栈辟开较大的值,可能加增内存的开销和启动间时。
碎片问题:对于堆来讲,繁频的new/delete势必会形成内存空间的不续连,从而形成量大的碎片,使序程效率下降。对于栈来讲,则不会存在这个问题,因为栈是先进后出的列队,他们是如此的一一对应,以至于永久都可不能有一个内存块从栈间中弹出,在他弹出之前,在他面下的进后的栈内容经已被弹出,详细的可以考参数据结构,这里我们就不再一一探讨了。
成长方向:对于堆来讲,成长方向是向上的,也就是向着内存址地加增的方向;对于栈来讲,它的成长方向是向下的,是向着内存址地减小的方向长增。
分配方法:堆都是动态分配的,没有态静分配的堆。栈有2种分配方法:态静分配和动态分配。态静分配是编译器实现的,比如局部变量的分配。动态分配由alloca函数行进分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器行进释放,无需我们手工现实。
分配效率:栈是器机系统供给的数据结构,计算会机在底层对栈供给支撑:分配专门的寄存器放存栈的址地,压栈出栈都有专门的指令执行,这就定决了栈的效率比拟高。堆则是C/C++函数库供给的,它的机制是很杂复的,例如为了分配一块内存,库函数会按照必定的法算(详细的法算可以考参数据结构/操纵系统)在堆内存中索搜可用的够足巨细的空间,如果没有够足巨细的空间(是能可由于内存碎片太多),就有可能调用系统功能去加增序程数据段的内存空间,这样就有会机分到够足巨细的内存,然后行进返回。然显,堆的效率比栈要低很多。
从这里我们可以看到,堆和栈比相,由于量大new/delete的用使,易容形成量大的内存碎片;由于没有专门的系统支撑,效率很低;由于可能发引用户态和核态心的换切,内存的请求,价值变得更加昂贵。所以栈在序程中是用应最普遍的,就算是函数的调用也利用栈去实现,函数调用过程当中的数参,返回址地,EBP和局部变量都采取栈的方法放存。所以,我们推荐大家尽量用栈,而不是用堆。
虽然栈有如此多众的利益,但是由于和堆比相不是那么活灵,有时候分配量大的内存空间,还是用堆好一些。
无论是堆还是栈,都要止防越界象现的产生(除非你是故意使其越界),因为越界的结果要么是序程崩溃,要么是捣毁序程的堆、栈结构,生产以想不到的结果,就算是在你的序程行运过程当中,没有产生面下的问题,你还是要当心,说不定什么时候就崩掉,那时候debug可是相称难题的:)
对了,还有一件事,如果有人把堆栈合起来讲,那它的意思是栈,可不是堆,呵呵,清晰了?
static用来制控变量的存储方法和可见性
函数外部定义的变量,在序程执行到它的定义处时,编译器为它在栈上分配空间,函数在栈上分配的空间在此函数执行束结时会释放掉,这样就生产了一个问题: 如果想将函数中此变量的值存保至下一次调用时,如何现实? 最易容想到的方法是定义一个全局的变量,但定义为一个全局变量有许多点缺,最显明的点缺是坏破了此变量的问访范围(使得在此函数中定义的变量,不仅仅受此函数制控)。
须要一个数据象对为整个类而非某个象对服务,同时又力图不坏破类的装封性,即要求此成员隐藏在类的外部,对外可不见。
static的外部机制:
态静数据成员要在序程一开始行运时就必须存在。因为函数在序程行运中被调用,所以态静数据成员不能在任何函数内分配空间和初始化。
这样,它的空间分配有三个可能的地方,一是作为类的外部接口的头件文,那里有类声明;二是类定义的外部现实,那里有类的成员函数定义;三是用应序程的main()函数前的全局数据声明和定义处。
态静数据成员要现实地分配空间,故不能在类的声明中定义(只能声明数据成员)。类声明只声明一个类的“尺寸和规格”,不并行进现实的内存分配,所以在类声明中写成定义是错误的。它也不能在头件文中类声明的外部定义,因为那会形成在多个用使该类的源件文中,对其重复定义。
static被引入以知告编译器,将变量存储在序程的态静存储区而非栈上空间,态静
数据成员按定义现出的先后序顺次依初始化,注意态静成员套嵌时,要保障所套嵌的成员经已初始化了。清除时的序顺是初始化的反序顺。
static的优势:
可以节俭内存,因为它是全部象对所私有的,因此,对多个象对来讲,态静数据成员只存储一处,供全部象对共用。态静数据成员的值对每一个象对都是一样,但它的值是可以更新的。只要对态静数据成员的值更新一次,保障全部象对存取更新后的同相的值,这样可以进步间时效率。
引用态静数据成员时,采取如下格式:
<类名>::<态静成员名>
如果态静数据成员的问访权限答应的话(即public的成员),可在序程中,按上述格式
来引用态静数据成员。
PS:
(1)类的态静成员函数是属于整个类而非类的象对,所以它没有this指针,这就致导
了它仅能问访类的态静数据和态静成员函数。
(2)不能将态静成员函数定义为虚函数。
(3)由于态静成员声明于类中,操纵于其外,所以对其取址地操纵,就多少有些殊特
,变量址地是指向其数据类型的指针,函数址地类型是一个“nonmember函数指针”。
(4)由于态静成员函数没有this指针,所以就差多不等同于nonmember函数,结果就
生产了一个意想不到的利益:成为一个callback函数,使得我们得以将C++和C-based X W
indow系统结合,同时也胜利的用应于线程函数身上。
(5)static并没有加增序程的时空开销,相反她还缩短了类子对类父态静成员的问访
间时,节俭了类子的内存空间。
(6)态静数据成员在<定义或说明>时面前加关键字static。
(7)态静数据成员是态静存储的,所以必须对它行进初始化。
(8)态静成员初始化与一般数据成员初始化不同:
初始化在类体外行进,而面前不加static,以免与一般态静变量或象对相淆混;
初始化时不加该成员的问访权制约控符private,public等;
初始化时用使作用域运算符来明标它所属类;
所以我们得出态静数据成员初始化的格式:
<数据类型><类名>::<态静数据成员名>=<值>
(9)为了止防类父的影响,可以在类子定义一个与类父同相的态静变量,以屏蔽类父的影响。这里有一点须要注意:我们说态静成员为类父和类子享共,但我们有重复定义了态静成员,这会不会发引错误呢?不会,我们的编译器采取了一种绝妙的法手:name-mangling 用以成生独一的标记。
内存池技术
http://kulong0105.blog.163.com/blog/static/1744061912011872057574/
以下来源http://blog.csdn.net/realxie/article/details/7228197
C++内存池的实现实
本节分析在某个大型应用程序实际应用到的一个内存池实现,并详细讲解其使用方法与工作原理。这是一个应用于单线程环境且分配单元大小固定的内存池,一般用来为执行时会动态频繁地创建且可能会被多次创建的类对象或者结构体分配内存。
本节首先讲解该内存池的数据结构声明及图示,接着描述其原理及行为特征。然后逐一讲解实现细节,最后介绍如何在实际程序中应用此内存池,并与使用普通内存函数申请内存的程序性能作比较。
内存池类MemoryPool的声明如下:
MemoryBlock为内存池中附着在真正用来为内存请求分配内存的内存块头部的结构体,它描述了与之联系的内存块的使用信息:
此内存池的数据结构如图6-2所示。
此内存池的总体机制如下。
(1)在运行过程中,MemoryPool内存池可能会有多个用来满足内存申请请求的内存块,这些内存块是从进程堆中开辟的一个较大的连续内存区域,它由一个MemoryBlock结构体和多个可供分配的内存单元组成,所有内存块组成了一个内存块链表,MemoryPool的pBlock是这个链表的头。对每个内存块,都可以通过其头部的MemoryBlock结构体的pNext成员访问紧跟在其后面的那个内存块。
(2)每个内存块由两部分组成,即一个MemoryBlock结构体和多个内存分配单元。这些内存分配单元大小固定(由MemoryPool的nUnitSize表示),MemoryBlock结构体并不维护那些已经分配的单元的信息;相反,它只维护没有分配的自由分配单元的信息。它有两个成员比较重要:nFree和nFirst。nFree记录这个内存块中还有多少个自由分配单元,而nFirst则记录下一个可供分配的单元的编号。每一个自由分配单元的头两个字节(即一个USHORT型值)记录了紧跟它之后的下一个自由分配单元的编号,这样,通过利用每个自由分配单元的头两个字节,一个MemoryBlock中的所有自由分配单元被链接起来。
(3)当有新的内存请求到来时,MemoryPool会通过pBlock遍历MemoryBlock链表,直到找到某个MemoryBlock所在的内存块,其中还有自由分配单元(通过检测MemoryBlock结构体的nFree成员是否大于0)。如果找到这样的内存块,取得其MemoryBlock的nFirst值(此为该内存块中第1个可供分配的自由单元的编号)。然后根据这个编号定位到该自由分配单元的起始位置(因为所有分配单元大小固定,因此每个分配单元的起始位置都可以通过编号分配单元大小来偏移定位),这个位置就是用来满足此次内存申请请求的内存的起始地址。但在返回这个地址前,需要首先将该位置开始的头两个字节的值(这两个字节值记录其之后的下一个自由分配单元的编号)赋给本内存块的MemoryBlock的nFirst成员。这样下一次的请求就会用这个编号对应的内存单元来满足,同时将此内存块的MemoryBlock的nFree递减1,然后才将刚才定位到的内存单元的起始位置作为此次内存请求的返回地址返回给调用者。
(4)如果从现有的内存块中找不到一个自由的内存分配单元(当第1次请求内存,以及现有的所有内存块中的所有内存分配单元都已经被分配时会发生这种情形),MemoryPool就会从进程堆中申请一个内存块(这个内存块包括一个MemoryBlock结构体,及紧邻其后的多个内存分配单元,假设内存分配单元的个数为n,n可以取值MemoryPool中的nInitSize或者nGrowSize),申请完后,并不会立刻将其中的一个分配单元分配出去,而是需要首先初始化这个内存块。初始化的操作包括设置MemoryBlock的nSize为所有内存分配单元的大小(注意,并不包括MemoryBlock结构体的大小)、nFree为n-1(注意,这里是n-1而不是n,因为此次新内存块就是为了满足一次新的内存请求而申请的,马上就会分配一块自由存储单元出去,如果设为n-1,分配一个自由存储单元后无须再将n递减1),nFirst为1(已经知道nFirst为下一个可以分配的自由存储单元的编号。为1的原因与nFree为n-1相同,即立即会将编号为0的自由分配单元分配出去。现在设为1,其后不用修改nFirst的值),MemoryBlock的构造需要做更重要的事情,即将编号为0的分配单元之后的所有自由分配单元链接起来。如前所述,每个自由分配单元的头两个字节用来存储下一个自由分配单元的编号。另外,因为每个分配单元大小固定,所以可以通过其编号和单元大小(MemoryPool的nUnitSize成员)的乘积作为偏移值进行定位。现在唯一的问题是定位从哪个地址开始?答案是MemoryBlock的aData[1]成员开始。因为aData[1]实际上是属于MemoryBlock结构体的(MemoryBlock结构体的最后一个字节),所以实质上,MemoryBlock结构体的最后一个字节也用做被分配出去的分配单元的一部分。因为整个内存块由MemoryBlock结构体和整数个分配单元组成,这意味着内存块的最后一个字节会被浪费,这个字节在图6-2中用位于两个内存的最后部分的浓黑背景的小块标识。确定了分配单元的起始位置后,将自由分配单元链接起来的工作就很容易了。即从aData位置开始,每隔nUnitSize大小取其头两个字节,记录其之后的自由分配单元的编号。因为刚开始所有分配单元都是自由的,所以这个编号就是自身编号加1,即位置上紧跟其后的单元的编号。初始化后,将此内存块的第1个分配单元的起始地址返回,已经知道这个地址就是aData。
(5)当某个被分配的单元因为delete需要回收时,该单元并不会返回给进程堆,而是返回给MemoryPool。返回时,MemoryPool能够知道该单元的起始地址。这时,MemoryPool开始遍历其所维护的内存块链表,判断该单元的起始地址是否落在某个内存块的地址范围内。如果不在所有内存地址范围内,则这个被回收的单元不属于这个MemoryPool;如果在某个内存块的地址范围内,那么它会将这个刚刚回收的分配单元加到这个内存块的MemoryBlock所维护的自由分配单元链表的头部,同时将其nFree值递增1。回收后,考虑到资源的有效利用及后续操作的性能,内存池的操作会继续判断:如果此内存块的所有分配单元都是自由的,那么这个内存块就会从MemoryPool中被移出并作为一个整体返回给进程堆;如果该内存块中还有非自由分配单元,这时不能将此内存块返回给进程堆。但是因为刚刚有一个分配单元返回给了这个内存块,即这个内存块有自由分配单元可供下次分配,因此它会被移到MemoryPool维护的内存块的头部。这样下次的内存请求到来,MemoryPool遍历其内存块链表以寻找自由分配单元时,第1次寻找就会找到这个内存块。因为这个内存块确实有自由分配单元,这样可以减少MemoryPool的遍历次数。
综上所述,每个内存池(MemoryPool)维护一个内存块链表(单链表),每个内存块由一个维护该内存块信息的块头结构(MemoryBlock)和多个分配单元组成,块头结构MemoryBlock则进一步维护一个该内存块的所有自由分配单元组成的"链表"。这个链表不是通过"指向下一个自由分配单元的指针"链接起来的,而是通过"下一个自由分配单元的编号"链接起来,这个编号值存储在该自由分配单元的头两个字节中。另外,第1个自由分配单元的起始位置并不是MemoryBlock结构体"后面的"第1个地址位置,而是MemoryBlock结构体"内部"的最后一个字节aData(也可能不是最后一个,因为考虑到字节对齐的问题),即分配单元实际上往前面错了一位。又因为MemoryBlock结构体后面的空间刚好是分配单元的整数倍,这样依次错位下去,内存块的最后一个字节实际没有被利用。这么做的一个原因也是考虑到不同平台的移植问题,因为不同平台的对齐方式可能不尽相同。即当申请MemoryBlock大小内存时,可能会返回比其所有成员大小总和还要大一些的内存。最后的几个字节是为了"补齐",而使得aData成为第1个分配单元的起始位置,这样在对齐方式不同的各种平台上都可以工作。
有了上述的总体印象后,本节来仔细剖析其实现细节。
(1)MemoryPool的构造如下:
从①处可以看出,MemoryPool创建时,并没有立刻创建真正用来满足内存申请的内存块,即内存块链表刚开始时为空。
②处和③处分别设置"第1次创建的内存块所包含的分配单元的个数",及"随后创建的内存块所包含的分配单元的个数",这两个值在MemoryPool创建时通过参数指定,其后在该MemoryPool对象生命周期中一直不变。
后面的代码用来设置nUnitSize,这个值参考传入的_nUnitSize参数。但是还需要考虑两个因素。如前所述,每个分配单元在自由状态时,其头两个字节用来存放"其下一个自由分配单元的编号"。即每个分配单元"最少"有"两个字节",这就是⑤处赋值的原因。④处是将大于4个字节的大小_nUnitSize往上"取整到"大于_nUnitSize的最小的MEMPOOL_ ALIGNMENT的倍数(前提是MEMPOOL_ALIGNMENT为2的倍数)。如_nUnitSize为11时,MEMPOOL_ALIGNMENT为8,nUnitSize为16;MEMPOOL_ALIGNMENT为4,nUnitSize为12;MEMPOOL_ALIGNMENT为2,nUnitSize为12,依次类推。
(2)当向MemoryPool提出内存请求时:
MemoryPool满足内存请求的步骤主要由四步组成。
①处首先判断内存池当前内存块链表是否为空,如果为空,则意味着这是第1次内存申请请求。这时,从进程堆中申请一个分配单元个数为nInitSize的内存块,并初始化该内存块(主要初始化MemoryBlock结构体成员,以及创建初始的自由分配单元链表,下面会详细分析其代码)。如果该内存块申请成功,并初始化完毕,返回第1个分配单元给调用函数。第1个分配单元以MemoryBlock结构体内的最后一个字节为起始地址。
②处的作用是当内存池中已有内存块(即内存块链表不为空)时遍历该内存块链表,寻找还有"自由分配单元"的内存块。
③处检查如果找到还有自由分配单元的内存块,则"定位"到该内存块现在可以用的自由分配单元处。"定位"以MemoryBlock结构体内的最后一个字节位置aData为起始位置,以MemoryPool的nUnitSize为步长来进行。找到后,需要修改MemoryBlock的nFree信息(剩下来的自由分配单元比原来减少了一个),以及修改此内存块的自由存储单元链表的信息。在找到的内存块中,pMyBlock->nFirst为该内存块中自由存储单元链表的表头,其下一个自由存储单元的编号存放在pMyBlock->nFirst指示的自由存储单元(亦即刚才定位到的自由存储单元)的头两个字节。通过刚才定位到的位置,取其头两个字节的值,赋给pMyBlock->nFirst,这就是此内存块的自由存储单元链表的新的表头,即下一次分配出去的自由分配单元的编号(如果nFree大于零的话)。修改维护信息后,就可以将刚才定位到的自由分配单元的地址返回给此次申请的调用函数。注意,因为这个分配单元已经被分配,而内存块无须维护已分配的分配单元,因此该分配单元的头两个字节的信息已经没有用处。换个角度看,这个自由分配单元返回给调用函数后,调用函数如何处置这块内存,内存池无从知晓,也无须知晓。此分配单元在返回给调用函数时,其内容对于调用函数来说是无意义的。因此几乎可以肯定调用函数在用这个单元的内存时会覆盖其原来的内容,即头两个字节的内容也会被抹去。因此每个存储单元并没有因为需要链接而引入多余的维护信息,而是直接利用单元内的头两个字节,当其分配后,头两个字节也可以被调用函数利用。而在自由状态时,则用来存放维护信息,即下一个自由分配单元的编号,这是一个有效利用内存的好例子。
④处表示在②处遍历时,没有找到还有自由分配单元的内存块,这时,需要重新向进程堆申请一个内存块。因为不是第一次申请内存块,所以申请的内存块包含的分配单元个数为nGrowSize,而不再是nInitSize。与①处相同,先做这个新申请内存块的初始化工作,然后将此内存块插入MemoryPool的内存块链表的头部,再将此内存块的第1个分配单元返回给调用函数。将此新内存块插入内存块链表的头部的原因是该内存块还有很多可供分配的自由分配单元(除非nGrowSize等于1,这应该不太可能。因为内存池的含义就是一次性地从进程堆中申请一大块内存,以供后续的多次申请),放在头部可以使得在下次收到内存申请时,减少②处对内存块的遍历时间。
可以用图6-2的MemoryPool来展示MemoryPool::Alloc的过程。图6-3是某个时刻MemoryPool的内部状态。
因为MemoryPool的内存块链表不为空,因此会遍历其内存块链表。又因为第1个内存块里有自由的分配单元,所以会从第1个内存块中分配。检查nFirst,其值为m,这时pBlock->aData+(pBlock->nFirst*nUnitSize)定位到编号为m的自由分配单元的起始位置(用pFree表示)。在返回pFree之前,需要修改此内存块的维护信息。首先将nFree递减1,然后取得pFree处开始的头两个字节的值(需要说明的是,这里aData处值为k。其实不是这一个字节。而是以aData和紧跟其后的另外一个字节合在一起构成的一个USHORT的值,不可误会)。发现为k,这时修改pBlock的nFirst为k。然后,返回pFree。此时MemoryPool的结构如图6-4所示。
可以看到,原来的第1个可供分配的单元(m编号处)已经显示为被分配的状态。而pBlock的nFirst已经指向原来m单元下一个自由分配单元的编号,即k。
(3)MemoryPool回收内存时:
如前所述,回收分配单元时,可能会将整个内存块返回给进程堆,也可能将被回收分配单元所属的内存块移至内存池的内存块链表的头部。这两个操作都需要修改链表结构。这时需要知道该内存块在链表中前一个位置的内存块。
①处遍历内存池的内存块链表,确定该待回收分配单元(pFree)落在哪一个内存块的指针范围内,通过比较指针值来确定。
运行到②处,pMyBlock即找到的包含pFree所指向的待回收分配单元的内存块(当然,这时应该还需要检查pMyBlock为NULL时的情形,即pFree不属于此内存池的范围,因此不能返回给此内存池,读者可以自行加上)。这时将pMyBlock的nFree递增1,表示此内存块的自由分配单元多了一个。
③处用来修改该内存块的自由分配单元链表的信息,它将这个待回收分配单元的头两个字节的值指向该内存块原来的第一个可分配的自由分配单元的编号。
④处将pMyBlock的nFirst值改变为指向这个待回收分配单元的编号,其编号通过计算此单元的起始位置相对pMyBlock的aData位置的差值,然后除以步长(nUnitSize)得到。
实质上,③和④两步的作用就是将此待回收分配单元"真正回收"。值得注意的是,这两步实际上是使得此回收单元成为此内存块的下一个可分配的自由分配单元,即将它放在了自由分配单元链表的头部。注意,其内存地址并没有发生改变。实际上,一个分配单元的内存地址无论是在分配后,还是处于自由状态时,一直都不会变化。变化的只是其状态(已分配/自由),以及当其处于自由状态时在自由分配单元链表中的位置。
⑤处检查当回收完毕后,包含此回收单元的内存块的所有单元是否都处于自由状态,且此内存是否处于内存块链表的头部。如果是,将此内存块整个的返回给进程堆,同时修改内存块链表结构。
注意,这里在判断一个内存块的所有单元是否都处于自由状态时,并没有遍历其所有单元,而是判断nFree乘以nUnitSize是否等于nSize。nSize是内存块中所有分配单元的大小,而不包括头部MemoryBlock结构体的大小。这里可以看到其用意,即用来快速检查某个内存块中所有分配单元是否全部处于自由状态。因为只需结合nFree和nUnitSize来计算得出结论,而无须遍历和计算所有自由状态的分配单元的个数。
另外还需注意的是,这里并不能比较nFree与nInitSize或nGrowSize的大小来判断某个内存块中所有分配单元都为自由状态,这是因为第1次分配的内存块(分配单元个数为nInitSize)可能被移到链表的后面,甚至可能在移到链表后面后,因为某个时间其所有单元都处于自由状态而被整个返回给进程堆。即在回收分配单元时,无法判定某个内存块中的分配单元个数到底是nInitSize还是nGrowSize,也就无法通过比较nFree与nInitSize或nGrowSize的大小来判断一个内存块的所有分配单元是否都为自由状态。
以上面分配后的内存池状态作为例子,假设这时第2个内存块中的最后一个单元需要回收(已被分配,假设其编号为m,pFree指针指向它),如图6-5所示。
不难发现,这时nFirst的值由原来的0变为m。即此内存块下一个被分配的单元是m编号的单元,而不是0编号的单元(最先分配的是最新回收的单元,从这一点看,这个过程与栈的原理类似,即先进后出。只不过这里的"进"意味着"回收",而"出"则意味着"分配")。相应地,m的"下一个自由单元"标记为0,即内存块原来的"下一个将被分配出去的单元",这也表明最近回收的分配单元被插到了内存块的"自由分配单元链表"的头部。当然,nFree递增1。
处理至⑥处之前,其状态如图6-6所示。
这里需要注意的是,虽然pFree被"回收",但是pFree仍然指向m编号的单元,这个单元在回收过程中,其头两个字节被覆写,但其他部分的内容并没有改变。而且从整个进程的内存使用角度来看,这个m编号的单元的状态仍然是"有效的"。因为这里的"回收"只是回收给了内存池,而并没有回收给进程堆,因此程序仍然可以通过pFree访问此单元。但是这是一个很危险的操作,因为首先该单元在回收过程中头两个字节已被覆写,并且该单元可能很快就会被内存池重新分配。因此回收后通过pFree指针对这个单元的访问都是错误的,读操作会读到错误的数据,写操作则可能会破坏程序中其他地方的数据,因此需要格外小心。
接着,需要判断该内存块的内部使用情况,及其在内存块链表中的位置。如果该内存块中省略号"……"所表示的其他部分中还有被分配的单元,即nFree乘以nUnitSize不等于nSize。因为此内存块不在链表头,因此还需要将其移到链表头部,如图6-7所示。
如果该内存块中省略号"……"表示的其他部分中全部都是自由分配单元,即nFree乘以nUnitSize等于nSize。因为此内存块不在链表头,所以此时需要将此内存块整个回收给进程堆,回收后内存池的结构如图6-8所示。
一个内存块在申请后会初始化,主要是为了建立最初的自由分配单元链表,下面是其详细代码:
这里可以看到,①处pData的初值是aData,即0编号单元。但是②处的循环中i却是从1开始,然后在循环内部的③处将pData的头两个字节值置为i。即0号单元的头两个字节值为1,1号单元的头两个字节值为2,一直到(nTypes-2)号单元的头两个字节值为(nTypes-1)。这意味着内存块初始时,其自由分配单元链表是从0号开始。依次串联,一直到倒数第2个单元指向最后一个单元。
还需要注意的是,在其初始化列表中,nFree初始化为nTypes-1(而不是nTypes),nFirst初始化为1(而不是0)。这是因为第1个单元,即0编号单元构造完毕后,立刻会被分配。另外注意到最后一个单元初始并没有设置头两个字节的值,因为该单元初始在本内存块中并没有下一个自由分配单元。但是从上面例子中可以看到,当最后一个单元被分配并回收后,其头两个字节会被设置。
图6-9所示为一个内存块初始化后的状态。
当内存池析构时,需要将内存池的所有内存块返回给进程堆:
分析内存池的内部原理后,本节说明如何使用它。从上面的分析可以看到,该内存池主要有两个对外接口函数,即Alloc和Free。Alloc返回所申请的分配单元(固定大小内存),Free则回收传入的指针代表的分配单元的内存给内存池。分配的信息则通过MemoryPool的构造函数指定,包括分配单元大小、内存池第1次申请的内存块中所含分配单元的个数,以及内存池后续申请的内存块所含分配单元的个数等。
综上所述,当需要提高某些关键类对象的申请/回收效率时,可以考虑将该类所有生成对象所需的空间都从某个这样的内存池中开辟。在销毁对象时,只需要返回给该内存池。"一个类的所有对象都分配在同一个内存池对象中"这一需求很自然的设计方法就是为这样的类声明一个静态内存池对象,同时为了让其所有对象都从这个内存池中开辟内存,而不是缺省的从进程堆中获得,需要为该类重载一个new运算符。因为相应地,回收也是面向内存池,而不是进程的缺省堆,还需要重载一个delete运算符。在new运算符中用内存池的Alloc函数满足所有该类对象的内存请求,而销毁某对象则可以通过在delete运算符中调用内存池的Free完成。
为了测试利用内存池后的效果,通过一个很小的测试程序可以发现采用内存池机制后耗时为297 ms。而没有采用内存池机制则耗时625 ms,速度提高了52.48%。速度提高的原因可以归结为几点,其一,除了偶尔的内存申请和销毁会导致从进程堆中分配和销毁内存块外,绝大多数的内存申请和销毁都由内存池在已经申请到的内存块中进行,而没有直接与进程堆打交道,而直接与进程堆打交道是很耗时的操作;其二,这是单线程环境的内存池,可以看到内存池的Alloc和Free操作中并没有加线程保护措施。因此如果类A用到该内存池,则所有类A对象的创建和销毁都必须发生在同一个线程中。但如果类A用到内存池,类B也用到内存池,那么类A的使用线程可以不必与类B的使用线程是同一个线程。
另外,在第1章中已经讨论过,因为内存池技术使得同类型的对象分布在相邻的内存区域,而程序会经常对同一类型的对象进行遍历操作。因此在程序运行过程中发生的缺页应该会相应少一些,但这个一般只能在真实的复杂应用环境中进行验证。