动态内存管理
- 1.语法层面
- 1.基本语法
- 注意点
- 2.new/delete和malloc/free的区别
- 3.operator new和operator delete函数(底层重点)
- 1.operator new/delete原理
- 2.图解
- 1.new/new[]
- 2.delete/delete[]
- 3.new[n]和delete[]
- 4.定位new
- 1.定义
- 2.使用格式
1.语法层面
1.基本语法
class Node
{
public:
Node(int val)
:_val(val),_next(nullptr)
{
}
private:
int _val;
Node* _next;
};
void test()
{
int* p1 = new int; //申请空间
int* p2 = new int(1); //申请空间 + 初始化
int* p3 = new int[4]; //申请4个int整型空间
int* p4 = new int[4] {1, 2, 3, 4}; //申请4个整型空间 + 初始化
Node* n1 = new Node(2); //申请空间 + 调用构造函数
delete p1;
delete p2;
delete[] p3;
delete[] p4;
delete[] n1;
}
注意点
1.申请单个空间时使用new/delete,申请一段连续空间时使用new[ ]/delete[ ],且一定要匹配使用
2.对于自定义类型,new会调用构造函数,delete会调用其的析构函数
2.new/delete和malloc/free的区别
用法上:
(1)new/delete可以自定义初始化,而malloc/free只负责开空间,不会初始化,且用法上比malloc/free方便
(2)new/delete不需要手动计算申请空间的大小,直接在后面加上类型即可
(3)new/delete不需要强转,malloc和free由于返回值类型为void*,因此需要强转使用
(4)new/delete在实现时使用了抛异常的机制,可以更好的处理问题,而malloc/free则需要手动实现判断
(5)new/delete可以更好的处理自定义类型,对于自定义类型,new的时候调用它的构造函数,delete的时候调用它的析构函数,同时new的时候还可以通过隐式类型转换调用它的有参构造,这对于链表的构造非常方便
(6)new/delete是操作符,malloc和free是函数
3.operator new和operator delete函数(底层重点)
1.operator new/delete原理
2.图解
1.new/new[]
new/new[]的调用规则如下图。
2.delete/delete[]
delete/delete[]的调用规则如下图。
new/delete操作符在二进制指令上本质是调用operator new和operator delete函数,而operator new在底层是对malloc的封装(operator delete)类似。
注意:delete和delete[]必须先调用析构函数,否则对于栈这样的类,它们申请的空间无法释放
3.new[n]和delete[]
在使用new[n]时,编译器可以通过n确定调用多少次构造函数,但是delete[]时我们并不会给值,那么调用delete[]时编译器如何知道调用多少次析构函数呢。
先看如下代码。
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
int main()
{
A* a = new A[10];
delete[] a;
return 0;
}
按照计算,每一个A中有一个_a,为4个字节,那么申请10个A应该为40字节,可是我们观察汇编,却发现申请了48字节(30为16进制表示),那么多余的8个字节是干什么的呢?
以下为a的地址,通过内存窗口我们可以看到确实开出了10个整型的空间。
但是如果再向上看一点,就会发现我们多开出的8个字节。其中存储了十六进制的a(即十进制的10),即为我们需要调用构造函数和析构函数的次数。
总结:
在我们调用new[]时,会有一个接收的地址,这个地址是我们开辟好的空间的第一个位置的地址,但是编译器其实在这个位置之前多开了字节来存储对象的个数,这样在调用delete[]时就可以通过这个值知道调用多少次析构函数了。如果使用delete[] 来释放空间,那么其在内部会从a指向的空间再向前移动的位置开始释放空间,而不是从a处直接释放空间,如果从a处直接释放空间则会报错。
那么如果不匹配使用,比如:
此时,由于调用的delete,因此它不会进行指针向前移动字节,而是直接从a的位置开始释放,而申请的空间不能分割释放(也就是释放位置错了),因此会报错。
上述情况是在我们显式写了析构函数的情况下,==如果我们没有显式写析构函数,那么由编译器默认生成的析构函数没有作用,因此编译器会优化掉,不会调用析构函数,也不会去存储调用次数,不会多申请那一部分空间,==因此不会报错。
从下图可以看到没有多申请空间来存储个数。
注意:
新版编译器会给出警告如图:
4.定位new
1.定义
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
可以理解为这块空间我先拿到它的使用权,但是我先不初始化使用,等到需要使用的时候再调用定位new。
2.使用格式
new (place_address) type或者new (place_address) type(initializer-list)
注:place_address必须是一个指针,initializer-list是类型的初始化列表
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
nitializer-list是类型的初始化列表
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};