转自:https://blog.csdn.net/hihozoo/article/details/51441521
一、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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
结果如下:
每个 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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
对于 ①,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; // 分配内存,但未初始化,垃圾值
1
通过new初始化对象,使用下述语法:
type * p_var = new type(init); // int * a = new int(8); //分配内存时,将 *a 初始化为 8
1
其中 init 是传递给构造函数的实参表或初值。
2)动态生成对象数组的 new 运算符表达式
new 也可创建一个对象数组:
type p_var = new type [size]; // int * a = new int[3] ; // 分配了 3个 int 大小的连续内存块, 但未初始化
1
C++98 标准规定,new 创建的对象数组不能被显式初始化, 数组所有元素被缺省初始化。如果数组元素类型没有缺省初始化(默认构造函数),则编译报错。但 C++11 已经允许显式初始化,例如:
int *p_int = new int[3] {1,2,3};
1
如此生成的对象数组,在释放时必须调用 delete [] 表达式。
2.1.2 placement new 运算符表达式
placement new 运算符表达式 就是 在用户指定的内存位置上构建新的对象 ,这个构建过程并不需要额外分配内存,只需要调用对象的构造函数即可。
placement new 的语法是:
new ( expression-list ) new-type-id ( optional-initializer-expression-list );
1
使用这种 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;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
对于上述代码,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 <iostream>
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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
如上代码,如果把 p->~Test(); 注释掉,上述代码的结果将为:
Test 构造
1
显然是只调用构造函数。所以对于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 );
1
或
new (nothrow) Type[size]; // new (std::nothrow_t) Type[size];
1
其中 nothrow 是 std::nothrow_t 的一个实例.
2.2 delete 表达式语法
2.2.1 内存释放
1)普通的 delete 运算符
delete 的基本语法是:
delete val_ptr;
1
2)释放对象数组的 delete 运算符
delete [] val_ptr
1
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) ;
1
2
3
4
5
6
7
8
9
10
分两步显式 调用析构函数 与 带位置的 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 &) .
1
2
3
4
5
6
7
8
带位置的 operator delete(void *,void *) 函数,可以被 placement new 运算符表达式自动调用。这是在对象的构造函数抛出异常的时候,用来释放掉 placement new 函数获取的内存(类内部可能涉及的内存分配)。以避免内存泄露。
#include <cstdlib>
#include <iostream>
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 ;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
结果如下:
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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
答案:不会产生内存泄漏。
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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
答案:会造成内存泄漏。
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();
1
2
内存需要回收的话,调用对应的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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
结果:
3.2 数组形式的operator new/delete[](size_t size)函数
new type[] 运算符,用来动态创建一个对象数组。这需要调用数组元素类型内部定义的void* operator new[](size_t)函数来分配内存。如果数组元素类型没有定义该函数,则调用全局的void* operator new[](size_t)函数来分配内存。
在 #include <new> 中声明了 void* operator new[](size_t) 全局函数:
void * operator new [] (std::size_t) throw(std::bad_alloc);
void operator delete [](void*) throw();
1
2
3.3 placement new/delete 函数
void * operator new(size_t,void*) 函数用于带位置的 new 运算符调用。C++标准库已经提供了operator new(size_t,void*)函数的实现,包含 <new> 头文件即可。这个实现只是简单的把参数的指定的地址返回,带位置的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() { }
1
2
3
4
5
6
7
禁止重定义这4个函数。因为都已经作为 <new> 的内联函数了。在使用时,实际上不需要#include <new>
虽然上面的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 &*/);
1
2
3
4
5
但是,基本没有什么意义 ^_^。
3.4 保证不抛出异常的operator new/delete函数
C++标准库的<new>中还提供了一个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();
1
2
3
4
5
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);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
这段代码很简单,神秘的 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;
}
1
2
3
4
四、小结
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