C++ 动态内存分配

一、new/delete

1.1 为什么需要new/delete

 C语言中通过malloc/free等函数实现动态内存管理,为什么C++中还引入new/delete对动态内存管理?原因在于:malloc/free等函数不能满足对自定义类型的对象管理要求,对象在创建时要自动调用构造函数,对象在销毁前要自动调用析构函数,由于malloc/free是库函数而不是运算符,编译器不能把调用构造函数和析构函数的任务强加给它们。因为C++需要一个能够完成动态内存分配和初始化工作的运算符new,以及一个能够完成资源释放和释放内存工作的运算符delete

注意:new/delete是运算符,而不是函数

1.2 new/delete对内置类型处理

 new/delete对内置类型处理与malloc/free对内置类型处理没有本质区别,只是用法不一样。new直接返回目标类型的指针,不需要显式类型转换,而malloc返回void*,必须显式地转换为目标类型后使用

用法:
类型名* 指针变量名 = new 类型名; delete 指针变量名;
类型名* 指针变量名 = new 类型名[元素个数]; delete[] 指针变量名;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

using namespace std;

int main()
{
int* p1 = (int*)malloc(sizeof(int)); // 动态开辟1个大小为int的内存空间
int* p2 = (int*)malloc(sizeof(int)*5); // 动态开辟5个大小为int的连续内存空间

int* p3 = new int; // 动态开辟1个大小为int的内存空间
int* p4 = new int[5]; // 动态开辟5个大小为int的连续内存空间

free(p1); // 释放起始地址为p1的动态开辟内存空间
free(p2); // 释放起始地址为p2的动态开辟内存空间

delete p3; // 释放单个元素内存空间
delete[] p4; // 释放连续的申请的动态内存空间

return 0;
}

malloc与new对内置类型都没有初始化,但可以int* p3 = new int(1); 将对应空间值初始化为1

1.3 new/delete对自定义类型处理

  malloc/free对自定义类型只会开辟/销毁空间,并不会调用构造函数/析构函数。new先开辟空间然后调用构造函数初始化,delete先调用析构函数释放资源然后销毁空间
在这里插入图片描述

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
53
54
55
56
57
58
#include <iostream>

using namespace std;

typedef int ElemType; //元素类型重定义,以后元素类型发生改变只用改这里
#define initSize 4 // 顺序表初始化大小

class SeqList
{
private:
ElemType* _data; //动态内存分配存放元素的数组
size_t _size; //已存放元素个数
size_t _capacity; //数组容量

public:
SeqList(int capacity = 5)
:_size(0)
,_capacity(capacity)
{
_data = new ElemType(capacity);
cout << "调用构造函数" << endl;
}

~SeqList()
{
delete _data;
_data = nullptr;
_size = _capacity = 0;
cout << "调用析构函数" << endl;
}

void InitSeq(int capacity = 5)
{
_data = new ElemType(capacity);
_size = 0;
_capacity = capacity;
}

void DestorySeq()
{
delete _data;
_data = nullptr;
_size = _capacity = 0;
}
};

int main()
{
SeqList* s1 = (SeqList*)malloc(sizeof(SeqList));
s1->InitSeq();
// 业务处理
s1->DestorySeq();
free(s1);

SeqList* s2 = new SeqList; // SeqList* s2 = new SeqList(10); 调用有参构造函数
// 业务处理
delete s2;
}

输出

1
2
调用构造函数
调用析构函数

 使用malloc为自定义类型分配空间需要手动调用InitSeq函数初始化,业务处理后需要调用DestorySeq函数进行资源释放然后free释放空间,比较麻烦且容易忘记初始化及释放资源。使用new/delete会自动调用构造函数/析构函数帮我们进行处理

二、new/delete 实现原理

new运算符底层是调用 operator new函数与构造函数实现的,delete运算符底层是调用析构函数 operator delete函数实现的
在这里插入图片描述

2.1 operator new函数

operator new为库函数,源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
// try to allocate size bytes
void *p;
while ((p = malloc(size)) == 0)
if (_callnewh(size) == 0)
{
// report no memory
static const std::bad_alloc nomem;
_RAISE(nomem);
}
return (p);
}

可以发现operator new函数申请空间还是调用malloc函数实现的,申请成功返回地址,申请空间失败会抛出异常。可以发现之所以用operator new而不是直接用malloc主要是对于申请失败时抛出异常

2.2 operator delete函数

operator delete为库函数,源代码如下:

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
void operator delete(void* pUserData)
{
_CrtMemBlockHeader* pHead;

RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));

if (pUserData == NULL)
return;

_mlock(_HEAP_LOCK); /* block other threads */
__TRY

/* get a pointer to memory block header */
pHead = pHdr(pUserData);

/* verify block type */
_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));

_free_dbg(pUserData, pHead->nBlockUse);

__FINALLY
_munlock(_HEAP_LOCK); /* release other threads */
__END_TRY_FINALLY

return;
}

operator delete 通过调用free来释放空间的。

2.3 new/delete对内置类型实现原理

 如果申请的是内置类型的空间,new和malloc,delete和free基本类似。只是new在申请空间失败时会抛异常,malloc会返回NULL

2.4 new/delete对自定义类型实现原理

new:
● 先调用operator new函数为对象动态内存分配空间
● 然后调用构造函数对对象初始化

delete:
● 先调用析构函数释放资源
● 然后调用operator delete函数释放对象的空间

new [N]:
● 先调用operator new[ ]函数对对象动态内存分配空间 (operator new[ ]函数内调用operator new函数实现对N个对象空间分配)
● 然后调用N次构造函数对对象初始化

delete[ ]:
● 先调用N次析构函数,对N个对象释放资源
● 然后调用operator delete[ ]函数释放空间(operator delete[ ]函数内调用operator delete函数实现对N个对象空间释放)

2.5 operator new与operator delete 函数重载

 有些情况下希望动态内存空间不是来源于堆而是来源内存池,以提高效率。可以在类中对operator new与operator delete函数重载。类中对象申请/释放的是内存池空间,类外还是从堆中申请/释放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class ListNode
{
private:
ListNode* _next;
ListNode* _prev;
int _data;

public:
void* operator new(size_t n)
{
void* p = nullptr;
p = allocator<ListNode>().allocate(1);
cout << "memory pool allocate" << endl;

return p;
}

void operator delete(void* p)
{
allocator<ListNode>().deallocate((ListNode*)p, 1);
cout << "memory pool deallocate" << endl;
}
};

三、placement-new

placement-new:在一块已分配的内存空间上调用构造函数初始化对象或对象数组

用法:
new (place_address) type
new (place_address) type(initializer-list)
new (place_address) type[元素个数]

placement-new主要应用场景是配合内存池使用:使用一块较大的动态内存分配空间,用来构造不同类型的对象或对象数组。

注意:placement-new构造的对象或对象数组要显式调用它们的析构函数进行资源释放,不要使用delete。因为构造起来的对象或对象数组大小并不一定等于原来申请空间大小,使用delete会造成内存泄漏或在之后释放原来申请空间出现运行时错误

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
#include <iostream>

using namespace std;

typedef int ElemType; //元素类型重定义,以后元素类型发生改变只用改这里
#define initSize 4 // 顺序表初始化大小

class SeqList
{
private:
ElemType* _data; //动态内存分配存放元素的数组
size_t _size; //已存放元素个数
size_t _capacity; //数组容量

public:
SeqList(int capacity = 5)
:_size(0)
,_capacity(capacity)
{
_data = new ElemType(capacity);
cout << "调用构造函数" << endl;
}

~SeqList()
{
delete _data;
_data = nullptr;
_size = _capacity = 0;
cout << "调用析构函数" << endl;
}
};

int main()
{

SeqList* s1 = (SeqList*)operator new(sizeof(SeqList)); // 动态内存申请大小为SeqList的空间
new(s1)SeqList; // 调用SeqList的默认构造函数对起始地址为s1的对象初始化

s1->~SeqList(); // 调用析构函数资源释放
operator delete(s1); // 释放动态内存分配空间

return 0;
}

四、C/C++ 动态内存分配对比

 C语言中使用malloc/free等函数进行动态内存分配,C++中使用new/delete运算符进行动态内存分配.

相同点:
● 都是从堆上分配空间,需要手动释放空间

不同点:
● malloc和free是函数,new和delete是运算符
● 对于自定义类型,malloc/free不会调用构造函数/析构函数,new会调用构造函数/析构函数
● malloc申请空间时,需要手动计算空间大小,new只需给类型名
● malloc的返回值为void*, 在使用时必须强转,new返回的是类型指针
● malloc申请空间失败返回NULL,new申请抛出异常