本系列内容总结自侯捷老师的内存管理课程,主要针对STL标准库中分配器的实现以及vc6中malloc的实现。对深入理解内存管理有很大帮助。
前言
我们在分配内存时,经常使用new和delete,但却经常“知其然不知其所以然”。
其实这一切的实现,都是C++设计者对于内存管理的精妙设计。
如侯捷老师常说,“源码之前,了无秘密”。
C++内存管理的源码剖析是了解底层实现的最好途径。
这里就对侯捷老师的C++内存管理这门课程做一些知识性的总结。
因为设计结构非常精妙,所以文中会引用课程的ppt作为解析参考,这些ppt所阐述的内容非常丰富,值得我们好好研究学习。
纵观
想要更好地掌握细节,就要先从宏观入手,明白内存分配的来龙去脉。
我们在使用C++应用程序时,会有这么几种情况:
- 调用库中的分配器分配内存(比如STL中的allcoator)
- 使用new与delete分配内存
- 使用malloc和free分配内存
- 使用系统API分配内存
这些情况有以下关系:
我们可以看到它们之间的递进关系。
库中的分配器其实是基于new和delete实现的。
而new和delete则是基于malloc和free实现的。
malloc和free则是基于系统API实现的。
这么说来,好像只要明白malloc和free就明白了所有的机制。其实不然,这种彼此之间的依赖关系只是精妙实现的基础。
new与delete
new
首先从最熟悉的new开始。我们都知道new是c++的表达式,用于分配堆内存。
假设我们有一个复数类,当我们执行
Complex* pc= new Complex(1,2);
new到底发生了什么呢?
其实这其中执行了三步:
可以看到第一步调用了operator_new()分配了内存。
然后将这块内存的类型进行转换,转换为Complex类型。
最后调用Complex的构造函数,这样这块Complex类型的内存就分配好了。
(operator_new()其实调用了malloc(),可以理解为直接用malloc分配了内存。)
当然,我们不可以直接用指针调用构造函数,除非使用placement new(后面说)
delete
有new就要有delete,那么当我们执行
delete pc;
会发生什么呢?
其实delete也分为两步:
先对Complex对象进行析构,再operator_delete()这块内存。
(operator_delete()调用了free(),也就是用free()释放内存)
array new与array delete
new和delete最基本的操作就是需要配套使用,new[]和[]delete也需要配套,否则会出现内存泄漏。
到底是哪里泄漏了呢?
我们就来看看这两组方法的区别。
基本数据类型
对于基本数据类型来说,比如int
int *a=new int[10];
使用两种方法delete
delete a;
delete[] a;
其实这种情况并没有内存泄漏。两种方式都可以正确的释放。
因为分配基本数据类型时,系统可以记忆分配的内存大小,析构时并不会调用析构函数。
通过指针可以直接获取到实际分配的内存空间。
没有指针成员的类
如果是一个没有指针成员的类,比如:
class Complex{
public:
Complex(){}
~Complex(){}
private:
int a;
int b;
}
Complex* pc=new Complex[3];
那么这时使用
delete pc;
delete[] pc;
会有区别么?
区别是有的,但这种细微的差异在这种情况下不会导致内存泄漏。
我们在构造时会调用三次构造函数,而使用delete pc却只会调用一次析构函数。
但使用delete pc会释放pc指针对应的内存空间,所以不论使用哪种方法,我们申请的三块内存都会被释放。
如果使用delete[] pc就是下图的实现方式。
所以对于这样的类来说,不论调用几次析构函数,都不会引起内存泄漏。
含有指针成员的类
其实真正的内存泄漏发生在这样的类中。
class string{
public:
...
private:
char* charaters;
int len;
}
可以看到string类中有指针成员,指向一块地址空间。
如果执行
string* psa=new string[3];
这时使用delete和delete[]就会有很大的区别。
如果我们使用delete psa,那么只会调用一次析构,而实际上还有两块空间需要析构才可以释放,就会产生内存泄漏。
所以在这种含有指针成员的类中,对应new[]必须要使用delete[]才能保证释放完整,不产生内存泄漏。
最好的方法就是不论在什么情形下,都使得new和delete,new[]和delete[]配对。
空间分配一览
对于new这个操作分配的空间,大概是这样的。
int *pi= new int[10];
这就是malloc分配的空间
我们得到的是00441c30这个地址,而实际大小因为加上了一些附加值要大得多。
malloc为什么这样分配,我们后面继续探究。
因为int是基本数据类型,所以系统可以记忆分配了多少空间。如果分配的是自定义类型,那么要多一点空间。
假设一个Demo类中有3个int成员
Demo *p=new Demo[3];
这里布局不同,多了一个用于记录分配长度的3,我们得到的指针是00481c34,也就是黑色的指针。
而调用delete[]p后,p指向的是00481c30,也就是红色的指针,这样可以读取到有多少个对象分配了。
如果在栈上分配,那么大小应该就是4字节(int)*3*3=36
但是堆上会分配61h,还是有较大差距的。