在本篇文章中,我们主要剖析c++中的动态内存管理,包括malloc、new expression、operator new、array new和allocator内存分配方法以及对应的内存释放方式和他们之间的调用关系,另外也包括一些会引发的陷阱如内存泄漏。
c++中的动态内存分配和释放方式有很多,主要包括:
除此之外还有placement new
,但需要注意placement new
不是用来内存分配和释放的,而是在已分配的内存上构造对象。
他们之间的调用关系如下:
下面我们来具体看下每一种分配和释放方式的使用和原理。
void *p1 = malloc(32); //分配32字节的内存 free(p1);//释放指针p1指向的内存
malloc函数以字节数为参数,返回指向分配的内存的首地址的void指针;而free函数释放给定指针指向的内存。
void *p6 = ::operator new(32); //分配32字节 ::operator delete(p6);
PS:底层调用malloc
和free
。gnu的实现:
_GLIBCXX_WEAK_DEFINITION void * operator new (std::size_t sz) _GLIBCXX_THROW (std::bad_alloc) { void *p; /* malloc (0) is unpredictable; avoid it. */ if (__builtin_expect (sz == 0, false)) sz = 1; while ((p = malloc (sz)) == 0) { new_handler handler = std::get_new_handler (); if (! handler) _GLIBCXX_THROW_OR_ABORT(bad_alloc()); handler (); } return p; }
_GLIBCXX_WEAK_DEFINITION void operator delete(void* ptr) noexcept { std::free(ptr); }
首先来看下简单的使用:
int *p2 = new int; delete p2; string *p3 = new string("hello"); delete p3;
new expression
完成两样工作:
申请并分配内存。
调用构造函数。
string *p3 = new string("hello");
被编译器替换成下面的工作:
string *p3; try{ void * tmp_p = operator new(sizeof(string)); p3 = static_cast<string *>(tmp_p); //string 通过宏被替换为basic_string,string的实际实现是basic_string,这里不是重点。 p3 -> basic_string::basic_string("hello"); //编译器可以这么调用,但我们自己写代码时不能。即我们不能以这种方式通过指针显式调用构造函数。 }catch (std::bad_alloc){ //若分配失败,构造函数不执行 }
我们看到,原来new expression
的内存申请和分配是通过调用operator new()
来完成的。
delete expression
也完成两样工作:
delete p3;
被编译器替换成下面的工作:
p3 -> ~string();//通过指针直接调用析构函数。我们自己写代码时也可以这么做。 operator delete(p3);//释放内存
//Complex为自定义类,只需要知道Complex类中没有指针成员。 Complex *pca = new Complex[3];//3次构造函数 delete[] pca;//3次析构函数 string *psa = new string[3];//3次构造函数 delete[] psa;//3次析构函数
array new
调用一次内存分配函数(底层源码实现中,其实是调用operator new,只是调用的时候计算好了大小。因此,有上下两个cookie。)和多次构造函数。正因为调用多次构造函数,因此只能调用无参构造函数。
Complex和string的很大不同之处在于,string有指针成员,布局如下图:
array delete
调用多次析构函数,一次内存释放函数(底层源码实现中其实是调用一次operator delete
)。
我们来看下,如果本应该使用array delete
的地方使用了delete expression
会发生什么:
Complex *pca = new Complex[3];//3次构造函数 delete pca;//1次析构函数 string *psa = new string[3];//3次析构函数 delete psa;//1次析构函数
对于Complex
,我们使用了array new
调用了3次构造函数,却没有使用array delete
而使用了delete expression
,因此只调用了一次析构函数。那么,会发生内存泄漏吗? 不会。因为Complex
的析构函数是无关痛痒的(trivial),因为没有要释放的关联的内存(Complex对象自身所占内存之外没有隐式占用的内存)。
同样,对于string
,我们使用了array new
调用了3次构造函数,却没有使用array delete
而使用了delete expression
,因此只调用了一次析构函数。那么,会发生内存泄漏吗? 会。因为string
的析构函数不是无关痛痒的(non-trivial),因为要释放关联的内存(我们知道string底层是通过char[]存储的,析构时会释放掉那些实际存储字符的内存)。
PS: 具体的内存布局例子(涉及到cookie、对齐填充padding等等)。
int *p = new int[10]; delete[]p; //delete p 亦可。int无关痛痒。
VC6中的内存布局如下:
另:
Demo *p = new Demo[3];//Demo为析构函数non-trivial的自定义class delete[] p; //delete p; //错误
VC6中的内存布局(注意红框内的3
):
#ifdef __GNUC__ //GNUC环境下 void *p7 = allocator<int>().allocate(4); //非static函数,通过实例化匿名对象调用allocate,分配4个int的内存。 allocator<int>().deallocate((int *)p7, 4); void *p8 = __gnu_cxx::__pool_alloc<int>().allocate(4); __gnu_cxx::__pool_alloc<int>().deallocate((int *)p8, 4); #endif
allocator
为模板,实例化时需提供模板类型参数,上面的程序中模板类型参数为<int>
,allocate的参数为4
则allocate函数分配时就分配4
个int
的内存。释放内存时需要给出指向所要释放的内存位置的指针,以及要释放的内存大小,单位为模板类型参数类型的大小。
__pool_alloc
也为模板,除底层调用malloc的时机不同外(__pool_alloc使用内存池降低cookie带来的overhead),使用和上面的allocator
相同。
用法:
char *buf = new char[sizeof(Complex) * 3]; Complex *pc = new(buf) Complex(1, 2); new(buf + 1) Complex(1, 3); new(buf + 2) Complex(1, 3); delete[] buf;
Complex *pc = new(buf) Complex(1, 2);
被编译器替换成如下的工作:
Complex *pc; try{ void *tmp = operator new(sizeof(Complex), buf);//该重载版本并不分配内存。buf指针已经指向内存。 pc = static_cast<Complex*>(tmp); pc->Complex::Complex(1, 2);//构造函数 }catch(std::bad_alloc){ //若分配失败则不执行构造函数。实际上没有分配,因为之前已经分配完。 }
上面使用的GNU库重载版本的operator new()函数如下:
// Default placement versions of operator new. _GLIBCXX_NODISCARD inline void* operator new(std::size_t, void* __p) _GLIBCXX_USE_NOEXCEPT { return __p; }
可以看到确实没有分配内存。
new expression
、delete expression
都不可重载。
operator new
、operator delete
可以重载:
operator new
、operator delete
,即::operator new(size_t)
与::operator delete(void *)
。(一般不会重载全局的该函数,因为影响太广)operator new
、operator delete
若某个类重载了operator new
、operator delete
,则用new expression
实例化该类时,调用的是类的operator new
、operator delete
,否则,调用globaloperator new
、operator delete
。
array new
、array delete
也可以重载。同样分全局的和类所属的。
具体如何重载这些内存管理函数,以及如何使用重载的内存管理函数,将在下一篇文章中分析。
[1] 《STL源码剖析》
[2] 《Effective C++》3/e
[3] 《C++ Primer》5/e
[4] 侯捷老师的课程
[5] gcc开源库:https://github.com/gcc-mirror/gcc