一、new/delete 简介
new 和 delete 是 c 用于管理 堆内存 的两个运算符,对应于 c 语言中的 malloc 和 free,但是 malloc 和 free 是函数,new 和 delete 是运算符。除此之外,new 在申请内存的同时,还会调用对象的构造函数,而 malloc 只会申请内存;同样,delete 在释放内存之前,会调用对象的析构函数,而 free 只会释放内存。
new 运算符的内部实现分为两步:
-
内存分配
调用相应的
operator new(size_t)
函数,动态分配内存。如果operator new(size_t)
不能成功获得内存,则调用new_handler()
函数用于处理new失败问题。如果没有设置new_handler()
函数或者new_handler()
未能分配足够内存,则抛出std::bad_alloc
异常。“new运算符”所调用的operator new(size_t)
函数,按照c 的名字查找规则,首先做依赖于实参的名字查找(即adl规则),在要申请内存的数据类型t的 内部(成员函数)、数据类型t定义处的命名空间查找;如果没有查找到,则直接调用全局的::operator new(size_t)
函数。 -
构造函数
在分配到的动态内存块上 初始化 相应类型的对象(构造函数)并返回其首地址。如果调用构造函数初始化对象时抛出异常,则自动调用
operator delete(void*, void*)
函数释放已经分配到的内存。
delete 运算符的内部实现分为两步:
-
析构函数
调用相应类型的析构函数,处理类内部可能涉及的资源释放。
-
内存释放
调用相应的
operator delete(void *)
函数。调用顺序参考上述operator new(size_t)
函数(adl规则)。
关于 new/delete 的内部实现,参考如下代码。
class t{
public:
t(){
cout << "构造函数。" << endl;
}
~t(){
cout << "析构函数。" << endl;
}
void * operator new(size_t sz){
t * t = (t*)malloc(sizeof(t));
cout << "内存分配。" << endl;
return t;
}
void operator delete(void *p){
free(p);
cout << "内存释放。" << endl;
return;
}
};
int main()
{
t * t = new t(); // 先 内存分配 ,再 构造函数
delete t; // 先 析构函数, 再 内存释放
return 0;
}
结果如下:
每个 new 获取的对象,必须用 delete 析构并释放内存,以免 内存泄漏。
举例说明:
class test{
public:
test(){
str = new char[2];
}
~test(){
delete [] str;
}
private:
char * str;
};
int main(){
// ①
test * t = new test;
free(t);
// ②
test * t2 = (test*)malloc(sizeof(test));
delete t2;
return 0;
}
-
对于 ①,
new test
的时候将会产生两方面的内存:-
test 对象本身的内存( win32环境,4 bytes 存储 char * 指针);
-
str 所指向的 2 bytes 堆内存。
如果调用 free 释放内存,那么由于 free 并不会调用 test 的析构函数,所以 free 只能释放 test 对象的内存(4 bytes),而 str 所指向的 2-bytes 堆内存并不能得到释放,因此而造成 内存泄漏 。
-
-
对于 ②,malloc 并不会调用类的构造函数,所以只分配了 test 对象的内存,str 并未初始化为指向一块堆内存。所以当调用 delete 释放内存的时候,将调用类的析构函数 (
delete [] str
),此时 delete 一块没有使用权的内存,程序崩溃 。
总之,编写c 程序时,在进行动态内存分配的时候,最好使用 new 和 delete。并且记住,new 出来的对象用 delete “消灭”它。
二、new/delete 表达式语法
2.1 new 表达式语法
2.1.1 内存分配
1)普通的 new 运算符表达式
new 的基本语法 :
type * p_var = new type; // int * a = new int; // 分配内存,但未初始化,垃圾值
通过new初始化对象,使用下述语法:
type * p_var = new type(init); // int * a = new int(8); //分配内存时,将 *a 初始化为 8
其中 init 是传递给构造函数的实参表或初值。
2)动态生成对象数组的 new 运算符表达式
new 也可创建一个对象数组:
type p_var = new type [size]; // int * a = new int[3] ; // 分配了 3个 int 大小的连续内存块, 但未初始化
c 98 标准规定,new 创建的对象数组不能被显式初始化, 数组所有元素被缺省初始化。如果数组元素类型没有缺省初始化(默认构造函数),则编译报错。但 c 11 已经允许显式初始化,例如:
int *p_int = new int[3] {
1,2,3};
如此生成的对象数组,在释放时必须调用 delete []
表达式。
2.1.2 placement new 运算符表达式
placement new 运算符表达式 就是 在用户指定的内存位置上构建新的对象 ,这个构建过程并不需要额外分配内存,只需要调用对象的构造函数即可。
placement new 的语法是:
new ( expression-list ) new-type-id ( optional-initializer-expression-list );
使用这种 placement new 运算符表达式,原因之一是 用户的程序不能在一块内存上自行调用其构造函数,必须由编译系统生成的代码调用构造函数。原因之二是可能需要把对象放在特定硬件的内存地址上,或者放在多处理器内核的共享的内存地址上。(ps:构造函数没办法直接这么调用 p->a(),而析构函数可以直接这么调用 p->~a()。)
释放这种 placement new 运算符对象时,不能调用 placement delete,应直接调用析构函数,如:pobj->~classtype() ; 然后再自行释放内存。
注意: c 中并没用与 placement new 运算符 功能相对应 的 placement delete 运算符(没有placement delete 运算符的概念,但是有 placement delete 函数)。^_^
解释:
-
首先看看 c 设计者,大牛 - bjarne stroustrup 的说法 is there a “placement delete”?
class arena { public: void * allocate(size_t); void deallocate(void\*); .... }; void * operator new(size_t sz, arena& a) { return a.allocate(sz); } arena a1(some arguments); arena a2(some arguments); x* p1 = new(a1) x; y* p2 = new(a1) y; z* p3 = new(a2) z;
对于上述代码,c 的类型机制并不能推断 p1 指向的对象是否位于 a1 之上。那么直接调用
delete(a1) p1;
就容易出错。所以为了安全,c 不提供 placement delete 运算符。 -
placement new 运算符不另外分配内存,换句话说,不是new运算符。它完成的功能是在给定地址上调用构造函数。如果提供p->t(),那么 placement new 运算符就不需要了。如果存在功能对应的 placement delete 运算符,那么功能就应该是在给定地址上调用析构函数。但因为 c 已经提供了p->~t(),就没必要有 placement delete 运算符。
-
如果存在对应的 placement delete 运算符,其实就是调用析构函数。而本身析构函数就可以自行主动调用,那么自己调用就好了,但是对象本身所占用这块内存还可以继续使用。如果想 placement delete 运算符像打洞一样,连对象内存一起回收,那
operator new(size_t )
的大块蜂窝煤内存如何 delete 。这不科学,既然整块内存是operator new(size_t)
的,就应该由operator delete(void *)
回收,而不能用 placement delete 运算符部分回收。 -
总之,没有与 placement new 运算符功能相对应的 placement delete 运算符。而且需要注意的是,运算符和函数是两个不同的概念,c 有 placement new 运算符和函数的概念,但是没有 placement delete 运算符的概念,有 placement delete 函数的概念 。
所以,对于 placement new 运算符,我们需要主动调用对象的析构函数。如下示例:
#include
using namespace std;
class test{
public:
test(){
cout << "test 构造" << endl;
str = new char[2];
}
~test(){
cout << "test 析构" << endl;
delete [] str;
}
private:
char * str;
};
int main(int argc, char* argv[])
{
char buf[100]; // 栈变量
test *p = new(buf) test(); // test()产生的临时变量用于初始化 指定内存地址
p->~test(); // 一定要主动调用析构函数,避免内存泄漏。 而且调用必须在 buf 生命周期内调用才有效。
// buf 指向的栈内存并不需要程序员主动释放。
// 栈变量过了生命周期会自动释放内存
// 其实栈内存的释放也不叫内存释放,只是栈顶指针移动,如果该块栈内存没有被其他程序刷新,那么该栈内存的值依然不变。
char * buf2 = new char[100];
test * p2 = new(buf2) test();
p2->~test(); // 切记,主动调用析构函数
delete [] buf2; // 堆内存需要主动释放
return 0;
}
如上代码,如果把 p->~test();
注释掉,上述代码的结果将为:
test 构造
显然是只调用构造函数。所以对于placement new,我们需要主动调用对象的析构函数 pobj->~classtype()
。
2.1.3 如何在栈上new?
我们知道,new 是用于管理堆内存,那又怎么可能在栈上 new 出一个对象呢?
通过上面的讨论,我们发现,new 除了能用于动态分配内存,还能够使用 placement new 在特定内存位置进行初始化。所以,如何在栈上 new 呢?上述代码(2.1.2)就是一个很好的例子。
2.1.4 不抛出异常的new运算符
在分配内存失败时,new运算符的标准行为是抛出std::bad_alloc
异常。也可以让new运算符在分配内存失败时不抛出异常而是返回空指针。
new (nothrow) type ( optional-initializer-expression-list );
或
new (nothrow) type[size]; // new (std::nothrow_t) type[size];
其中 nothrow 是 std::nothrow_t
的一个实例.
2.2 delete 表达式语法
2.2.1 内存释放
1)普通的 delete 运算符
delete 的基本语法是:
delete val_ptr;
2)释放对象数组的 delete 运算符
delete [] val_ptr
2.2.2 没有 placement delete 运算符表达式
通过上面的讨论,我们可以知道 c 中并没有提供与 placement new 运算符功能相对应 placement delete 运算符。但是仍然有placement delete函数的概念,功能在后面有介绍。
c 不能使用 placement delete 运算符表达式直接析构一个对象但不释放其内存。因此,对于placement new表达式构建的对象,析构释放时有两种办法:
-
是直接写一个函数,完成析构对象、释放内存的操作:
void destroy (t * p, a & arena) { // *p 是在 arena 之上构建的对象,即 t * a = new(&arena) t; p->~t() ; // 先析构 *p 对象 arena.deallocate(p) ; // 再释放 arena 整个内存,而不是位于arena中的部分内存(*p) } a arena ; t * p = new (arena) t ; .... destroy(p, arena) ;
-
分两步显式 调用析构函数 与 带位置的 operator delete 函数:
a arena ; t * p = new (arena) t ; /* ... */ p->~t() ; // 先析构 operator delete(p, arena) ; // 调用 placement delete 函数(非运算符) // then call the deallocator function indirectly via operator delete(void *, a &) .
带位置的 operator delete(void *,void *)
函数,可以被 placement new 运算符表达式自动调用。这是在对象的构造函数抛出异常的时候,用来释放掉 placement new 函数获取的内存(类内部可能涉及的内存分配)。以避免内存泄露。
#include
#include
char buf[100];
struct a {} ;
struct e {} ;
class t {
public:
t()
{
std::cout << "t 构造函数。" << std::endl;
throw e(); //抛出异常
}
void * operator new(std::size_t,const a &)
{
std::cout << "placement new called for class t." << std::endl;
return buf;
}
void operator delete(void*, const a &)
{
std::cout << "placement delete called for class t." << std::endl;
}
} ;
void * operator new ( std::size_t, const a & )
{
std::cout << "placement new called." << std::endl;
return buf;
}
void operator delete ( void *, const a & )
{
std::cout << "placement delete called." << std::endl;
}
int main ()
{
a a ;
try {
t * p = new (a) t ;
/* do something */
}
catch (e exp)
{
std::cout << "exception caught." << std::endl;
}
return 0 ;
}
结果如下:
c 有 placement delete 函数,但是没有 placement delete 运算符的概念。
2.2.3 delete 类对象时该注意的问题
问题 1
如下一段代码,是否是产生内存泄漏? 此题的讨论详见 csdn 论坛 。
class a
{
public:
a(){}
virtual void f(){}
private:
int m_a;
};
class b : public a
{
public:
virtual void f(){}
private:
int m_b;
};
int main()
{
a *pa = new b;
delete pa;
pa = null;
return 0;
}
答案:不会产生内存泄漏。
-
delete 释放内存时,会调用类的析构函数。 但是需要明确的是 析构函数并不会释放 对象本身 的内存 。
-
delete 运算符分为2个阶段。 第一个阶段是调用类的析构函数,第二阶段才是释放对象内存(但是这个工作不是析构函数在做)。
-
析构函数是free()之前的调用,而真正释放内存的操作是
free(void *ptr)
,注意只有指针一个参数,没有长度参数,这说明了什么?说明了a *pa = new b;
时带着长度sizeof(b)最终调用了malloc(sizeof(b))
;申请的内存及长度已经被记录,当free(pa)是就会释放掉自pa开始长度为sizeof(b)的内存。析构函数仅仅是应用逻辑层次的释放资源,不是物理层次的释放资源。(ps:关于new/delete运算符的具体实现后面还会涉及。)
问题 2
修改一下上面的题目,如下是否会造成内存泄漏呢?
class a
{
public:
a(){
m_a = new int(1);
}
~a(){ // 声明为virtual, 防止内存泄漏
delete m_a;
}
private:
int * m_a;
};
class b : public a
{
public:
b() : a(){
m_b = new int(2);
}
~b(){
delete m_b;
}
private:
int * m_b;
};
int main()
{
a * pa = new b;
delete pa;
pa = null;
return 0;
}
答案:会造成内存泄漏。
-
delete pa 的时候,只会调用基类的析构函数。所以 m_b 指向的内存块没得到释放。造成内存泄漏。
-
通过这个例子,应该深刻理解 析构函数的作用: 程序员处理类内部可能涉及的内存分配、资源释放。而不是释放类本身的内存。
三、operator new/delete() 的函数重载
平时使用 new 动态生成一个对象,实际上是调用了 new 运算符。
该运算符首先调用了operator new(std::size_t )
函数动态分配内存,然后调用类型的构造函数初始化这块内存。 new / delete 运算符是不能被重载的,但是下述各种 operator new/delete()
函数既可以作为 1. 全局函数重载,也可以作为 2. 类成员函数或 3. 作用域内的函数重载,即由编程者指定如何获取内存。
3.1 普通的operator new/delete(size_t size)函数
new 运算符 首先调用 operator new(std::size_t )
函数动态分配内存。首先查找 类内 是否有 operator new(std::size_t)
函数可供使用(即依赖于实参的名字查找)。
-
operator new(size_t )
函数的参数是一个 size_t 类型,指明了需要分配内存的规模。 -
operator new(size_t )
函数可以被每个 c 类作为成员函数重载。也可以作为全局函数重载:
void * operator new (std::size_t) throw(std::bad_alloc);
void operator delete(void*) throw();
内存需要回收的话,调用对应的operator delete(void *)
函数。
例如,在 new 运算符表达式的第二步,调用构造函数初始化内存时如果抛出异常,异常处理机制在栈展开(stack unwinding)时,要回收在new运算符表达式的第一步已经动态分配到的内存,这时就会 自动调用 对应 operator delete(void*)
函数。(注意:此处调用的是非位置delete函数)
struct e{};
class t{
public:
t(){
cout << "构造函数。" << endl;
throw e();
}
~t(){
cout << "析构函数。" << endl;
}
void * operator new(size_t sz){
t * t = (t*)malloc(sizeof(t));
cout << "内存分配。" << endl;
return t;
}
void operator delete(void *p){
free(p);
cout << "内存释放。" << endl;
return;
}
};
int main()
{
try {
t * p = new t;
/* do something */
}
catch (e exp){
std::cout << "exception caught." << std::endl;
}
return 0;
}
结果:
3.2 数组形式的operator new/delete[](size_t size)函数
new type[] 运算符,用来动态创建一个对象数组。这需要调用数组元素类型内部定义的void* operator new[](size_t)
函数来分配内存。如果数组元素类型没有定义该函数,则调用全局的void* operator new[](size_t)
函数来分配内存。
在 #include
中声明了 void* operator new[](size_t)
全局函数:
void * operator new [] (std::size_t) throw(std::bad_alloc);
void operator delete [](void*) throw();
3.3 placement new/delete 函数
void * operator new(size_t,void*)
函数用于带位置的 new 运算符调用。c 标准库已经提供了operator new(size_t,void*)
函数的实现,包含
头文件即可。这个实现只是简单的把参数的指定的地址返回,带位置的new运算符就会在该地址上调用构造函数来初始化对象:
// default placement versions of operator new.
inline void* operator new(std::size_t, void* __p) throw() { return __p; }
inline void* operator new[](std::size_t, void* __p) throw() { return __p; }
// default placement versions of operator delete.
inline void operator delete (void*, void*) throw() { }
inline void operator delete[](void*, void*) throw() { }
禁止重定义这4个函数。因为都已经作为
的内联函数了。在使用时,实际上不需要#include
虽然上面的4个 placement new/delete 函数不能重载,但是仍然可以写一个自己的 placement new/delete 函数,例如 :
inline void* operator new(std::size_t, a * /* 或者 const a &*/);
inline void* operator new[](std::size_t, a * /* 或者 const a &*/);
inline void operator delete (void*, a* /* 或者 const a &*/);
inline void operator delete[](void*, a* /* 或者 const a &*/);
但是,基本没有什么意义 ^_^。
3.4 保证不抛出异常的operator new/delete函数
c 标准库的
中还提供了一个nothrow的实现,用户可写自己的函数替代:
void* operator new(std::size_t, const std::nothrow_t&) throw();
void* operator new[](std::size_t, const std::nothrow_t&) throw();
void operator delete(void*, const std::nothrow_t&) throw();
void operator delete[](void*, const std::nothrow_t&) throw();
3.5 clang关于operator new/delete 的实现
以下这段代码是clang编译器关于operator new(std::size_t)
和 operator delete (void *)
的实现:
void * operator new(std::size_t size) throw(std::bad_alloc) {
if (size == 0)
size = 1;
void* p;
while ((p = ::malloc(size)) == 0) {
std::new_handler nh = std::get_new_handler();
if (nh)
nh();
else
throw std::bad_alloc();
}
return p;
}
void operator delete(void* ptr) {
if (ptr)
::free(ptr);
}
这段代码很简单,神秘的 operator new/delete 在背后也不过是在偷偷地调用c函数库的 malloc / free !当然,这跟具体实现有关,clang libcxx 是这样实现,不代表其它实现也是如此。
需要意识到的是, operator new 和 operator () 一样,只不过是普通的函数,是可以重载的,所谓的 placement new ,也是一个全局 operator new 的重载版本,在clang libcxx 中定义如下:
inline _libcpp_inline_visibility void* operator new (std::size_t, void* __p) _noexcept
{
return __p;
}
四、小结
new 和 delete 是 c 用于管理 堆内存 的两个运算符。
new 运算符 进行动态内存申请的时候,包含 2 个阶段:
-
内存申请 new。
根据 clang 的实现,我们可以猜测 内存new 基本就是通过 malloc 进行动态内存申请,但是本步骤并不初始化内存。本步骤对应
operator new(size_t )
函数。 -
构造函数。
delete 运算符 进行内存释放的时候,也包含 2 个阶段:
-
析构对象。
-
内存释放 delete。
本步骤对应
operator delete(void*)
函数。
除了用于内存管理的 new/delete 运算符,还有带位置的 placement new 运算符,但是没有带位置的 placement delete 运算符。
placement new 运算符
-
解决不能主动调用构造函数的“矛盾”。
-
对应的函数是
operator new(size_t , void *)
。
placement delete 运算符
-
没有此类运算符。
-
但有带位置的 placement delete 函数,如全局的
operator delete(void *,void*)
。
五、扩展 : free/delete 怎么知道有多少内存要释放 ?
参考 matthewgao github page
在使用c或者c 的时候我们经常用到malloc/free和new/delete,在使用malloc申请内存的时候我们给定了需要申请的内存大小,但是在free或者delete的时候并不需要提供这个大小,那么程序是怎么实现准确无误的释放内存的呢?
实际上,在申请内存的时候,申请到的地址会比你实际的地址大一点点,他包含了一个存有申请空间大小的结构体。
比如你申请了20byte的空间,实际上系统申请了48bytes的block
- 16-byte header containing size, special marker, checksum, pointers to next/previous block and so on.
- 32 bytes data area (your 20 bytes padded out to a multiple of 16))
这样在 free的时候就不需要提供任何其他的信息,可以正确的释放内存
这里有个在 stackoverflow.com 上的提问,可以参考
- http://stackoverflow.com/questions/1518711/c-programming-how-does-free-know-how-much-to-free