【C++修炼之路 第三章】内存管理:new 与 delete
1、C++内存管理方式
C语言内存管理方式在C++中可以继续使用,但使用起来比较麻烦,因此 C++又提 出了自己的内存管理方式:通过 new 和 delete 操作符进行动态内存管理。
除了用法,和 C语言的 malloc 没什么区别
1.1 内置类型
// 内置类型
// 使用什么类型就 new 什么
int* p1 = new int;
int* p2 = new int[10]; // new 多个
delete p1;
delete[] p2;
1.2 初始化
// 初始化:自己不初始化,数组中默认初始化为 0(和C语言相同),而 new 一个的值是随机值
int* p3 = new int(33);
int* p4 = new int[10] {1, 2, 3, 4}; // new 多个,花括号初始化多个
1.3 自定义类型
// 自定义类型
// 使用 malloc :自定义类型不会初始化
// 使用 free :仅仅释放空间
A* p5 = (A*)malloc(sizeof(A));
free(p5);
// 使用 new :会自动调用默认构造函数(不传参只会调用默认构造)
A* p6 = new A;
// 可以显式调用构造函数:传个参数 12
A* p7 = new A(12);
// 开辟连续的空间会连续调用 构造函数,销毁会连续调用 析构函数
A* p8 = new A[10];
// 给连续的空间赋值:这里涉及隐式类型转换,如将 1 赋值给 第一个A 就是 先 生成临时对象,再拷贝给 第一个A (这一过程常常会被编译器合二为一)
// 单参数
A* p9 = new A[10]{ 1, 2, 3, 4 };
// 多参数
B* p10 = new B[10]{ {1, 2}, {2, 3}, {3, 4} };
// 单参数和多参数 可以混着用
A* p11 = new A[10]{ 1, 2, {4, 5}};
A 和 B 都是 自定义的类
class A{
public:
A(int n = 2)
:_a(10)
{}
A(int x, int y)
:_a(10)
, _b(20)
{}
private:
int _a;
int _b;
};
class B {
public:
B(int x = 10, int y = 20)
:_a(10)
,_b(20)
{}
private:
int _a;
int _b;
};
1.4 小结
总结:new 可以调用构造和析构,更加适用于 自定义类型,malloc 不再适用了(95%的场景都要用 new)
另外,C++没有realloc扩容,需要手动扩容
注意:申请和释放**单个元素**的空间,使用 **new和delete** 操作符,申请和释放**连续的空间**,使用**new[]和 delete[]**,注意:匹配起来使用。
1.5 new 的使用举例
之前 C语言 创造一个链表节点 需要额外写一个 CreateNode 函数,需要生成节点时调用,同时还要传参
而 现在直接 new 一个就好:
struct ListNode
{
ListNode* _next;
int _val;
ListNode(int val)
:_next(nullptr)
, _val(val)
{}
};
int main()
{
ListNode* n1 = new ListNode(2);
ListNode* n2 = new ListNode(3);
n1->_next = n2;
return 0;
}
2、operator new与operator delete函数(重要点进行讲解)
2.1 概念
new 和 delete 是用户进行动态内存申请和释放的操作符
operator new 和operator delete是系统提供的 全局函数
new 在底层调用 operator new 全局函数来申请空间,
delete 在底层通过 operator delete 全局函数来释放空间。
注意:严格来说,这两个函数不是 new 和 delete 的重载,而是库里面的 全局函数
同时,C语言中使用 malloc ,需要检查 是否为 NULL,而 C++ 的 new 不用你写,若 new 失败了,会自己抛异常(后面会学),不用检查返回值(是否为 NULL)
这里也就说明为什么 不能直接使用 malloc 代替 operator new
operator new = malloc + 失败抛异常
(这里其实是进行了封装,便于使用,还不用自己写判断返回值(是否为 NULL))
operator delete 纯粹是为了 和 operator new 配对,封装了 free (和 free 没什么很大的区别,就是为了和 operator new 对称)
总结:在底层
operator new == 封装 malloc + 异常抛出
operator delete == 封装 free 和一些其他东西(暂时不用了解)
2.2 operator new 与 operator delete函数 的使用:new 和 delete 的底层函数调用顺序
// operator new(底层是 malloc) + 构造函数 == 先开空间,再构造
A* p = new A;
// 先 析构 + 再 operator delete == 先析构对象资源,再 free 空间
delete p;
举个例子:
class A {
public:
A(int n = 10)
:_a((int*)malloc(sizeof(int) * 8))
{}
private:
int* _a;
};
int main()
{
A* p = new A;
delete p;
return 0;
}
1、new:先调用 operator new,再调用 构造函数
operator new :负责开辟 一个 A 对象需要的总空间,即给指针 p 指向一片空间
构造函数 :负责内部的初始化与资源开辟,如 这个对象中给 指针_a 指向一块 malloc 开的空间
2、delete:先调用 析构 + 再调用 operator delete
析构:负责 对象内各种资源清理,如 free 掉 指针 _a 指向的空间
operator delete :负责 对象指针 p 所指的空间(总空间的free)
【问题】为什么外部的 operator delete free 了总空间,内部还要调用 析构 free 指针 _a 指向的空间?
答:free 掉整个对象的 空间,不代表已经 free 了内部指针 _a 指向的空间
指针 _a 指向的空间,始终被占用,若不手动 free ,会导致内存泄漏
⭐这里讲讲连续开辟的原理(其他的也差不多明白了)
(1)new T[N] 的原理
1、调用 operator new[] 函数,在 operator new[] 中实际调用 operator new 函数完成 N个对象空间的申请
2、在申请的空间上执行N次构造函数
(2)delete[]的原理
1、在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
2、调用 operator delete[] 释放空间,实际在 operator delete[] 中调用 operator delete 来释放空间
注意:这里一次开辟一块连续的空间,其中分成 N 个位置,但是 仅仅是调用一次operator new 函数 和 operator delete 函数 这两函数是用于开辟和释放一整块空间的
而需要对 N 个对象处理,所以调用 N 次 构造函数 和 N次析构函数
⭐小结
相对于 malloc,new 可以调用 构造函数 初始化目标 ,可以在无法开辟空间时自动抛出异常(不用手动检查)
相对于 free,delete 可以调用 析构函数清理资源,其他的没什么区别,主要是为了配套 new 使用(其中有些细节暂时不讨论)
operator new 底层是 malloc
operator delete 底层是 free
2.3 对于自定义类型,new[] 不使用配套的 delete[] ,而是使用 delete 或 free 为什么会报错?
class A {
public:
A(int n = 2)
:_a(n)
{}
~A() {
cout << "~A" << '\n';
}
private:
int _a;
};
int main()
{
// 为什么这两项实际大小是不一样的?
// p1 这里会多开 4 个字节
A* p1 = new A[1]; // 44
int* p2 = new int[10]; // 40
//free(p1); //报错
//delete(p1); // 报错
delete[](p1);
return 0;
}
打开调试 查看内存窗口:
看 p1 的开辟空间的内存分布:这里一共开了 44 个字节,40 个字节存储了 数值2(我构造函数那里赋值了 数值2)
第一行的那 4 个字节存储着 a (就是 十进制的 数字10 ):表示对象个数
存储对象个数 :是为了方便提醒 析构函数 ,当前这里一共创建了多少个对象,便于析构
当 连续开辟空间 ,且类中有显式的析构函数时,编译器会自动在开辟的总空间地址的前面一个位置存入 此次 创建的对象个数
这样写不会触发:
A* p1 = new A;
带有括号的才会触发:表示连续开辟空间
A* p1 = new A[1]; // 这个虽然只有一个,但也算
A* p1 = new A[2];
A* p1 = new A[10];
分析过程:
A* p1 = new A[10] :先 开辟 40 个字节的空间,然后将这块空间的首地址给 指针变量 p,编译器会自动在 p 的前面开辟 4 个字节的空间,在其中存储 ”对象的个数“
(注意:编译器多开的 4 个字节是 int 的大小,不是因为 类A的大小 是 4 个字节,不管自定义类型的大小如何,每次都是 固定开一个 int 4个字节,刚好用于存个数)
当 delete[] 释放时,调用 operator delete 函数,其中会将 [p1-4] 这个地址给 free 用于释放(即最后 free 释放的空间并不仅仅是指针p 所指向的那片空间,必须往前偏移 4 个字节):因为编译器自己开辟的那 4 个字节的空间也需要被释放,否则内存泄漏
另外:
编译器会多开 4 个字节的情况,是你显式写了一个 析构函数,若你没有显式写,编译器不会多开 4 个字节
多开 4 个字节 纯粹是存储 对象的个数,因为 delete[] 释放时,没有告诉编译器这里的需要析构的对象个数,所以底层会自己先记录下来
⭐🐔总结:
-
当 显式写析构函数,编译器自动开 4 字节空间 记录 自定义类型个数,则 释放空间的位置是 [p-4]、
-
当 没有显式写析构函数,编译器不会多开空间,则 释放空间的位置是 p
-
用 delete 和 free 会报错,是因为释放的位置不对,应该包含编译器自己开的 4 字节空间 (delete 匹配的是 new,不是
new[])
问:核心是因为释放的位置不对,那是不是我们不显式写 析构函数,就不会多开空间,释放的位置不会出错,就能使用 delete 和 free ?
答:确实不报错了,这样可能是巧合,同时这样不规范,且可能会出些小问题
这些地方会出错,都是底层编译器搞鬼,为了不出错,不要错配使用
new 配 delete
new[] 配 delete[]
malloc 配 free
3、定位new表达式 (placement-new) (了解)
## 3.1 引入
如果不能使用 delete :可以显式调用 析构
A* p1 = new A;
p1->~A();
free(p1);
如果不能使用 new:却不可以向上面一样显式的调用 构造
A* p1 = (A*)operator new(sizeof(A)); // 或者写 malloc
p1->A(); // 不能这样写!!!!
(祖师爷老本不愿意(doge))
这里就需要使用下面这种方式来调用 构造函数了
3.2 概念与使用
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
使用格式: new (place_address) type 或者 new (place_address) type(initializer-list) place_address 必须是一个指针:空间的指针地址,initializer-list 是类型的初始化列表
使用场景: 定位 new 表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用 new 的定义表达式进行显示调构造函数进行初始化。
// 使用格式:new (place_address) type
new(p1)A;
对已有空间,显式调用构造
如果要同时初始化多个对象:写成循环
int main()
{
// 手动开空间
A* p1 = (A*)operator new(sizeof(A)*10); // 或者写 malloc
for (int i = 0; i < 10; ++i) {
new(p1+i)A(33);
}
return 0;
}
注意这里指针+i 的含义:
指针+1 并不是向后偏移 1 个字节,而是偏移指针指向元素类型大小的字节
比如int* p; p + 1 是向后偏移 4 个字节
上面的 p1 是 A* 类型,+1 也就是向后偏移 sizeof(A) 个字节
或者用花括号显式初始化
A* p1 = (A*)operator new(sizeof(A) * 10); // 或者写 malloc
new(p1)A[10]{ 1, 2, 3, 4 };//没有显式赋值,就用缺省
配套的delete,也可以这样整
for(int i = 0; i < 10; ++i){
(p1+i)->~A();
}
operator delete(p1);
3.3 定位new表达式的一种使用场景
malloc 向系统申请空间 就和 你找你妈要钱,而钱包里面还有多少钱、有没有申请超额,你都不需要管
其中,你要一次,就申请一次,来回的交互
malloc 要不断申请空间,就需要和向系统不断交互,有点影响系统运行效率
干脆一次申请多点,比如一次拿1000元放进自己的卡里,自己要就从自己的卡拿出
自己的卡空了,再和妈妈申请
同时要考虑的问题:这样虽然减少了 要钱次数 ,但是你需要自己管理卡是否用超额等等一些问题
上面的比喻分别对应
系统的堆:妈妈的钱包
内存池:自己的卡
一个进程:我(妈妈的其中一个孩子)(系统上可能同时存在许多个进程)
直接从自己的内存池中取空间,减少和系统交互的次数,增加了系统运行效率
直接从系统的堆中申请的空间是初始化好的
从内存池中申请的空间是没有初始化的,因此对于自定义类型就需要显式调用构造函数,定位new表达式就派上用场了
A* p = MemoryPool.Alloc(sizeof(A)); // 从内存池中调用内存(这里的写法不是正确的,仅仅作为演示)
new(p)A; // 显式调用构造函数
4. 【常见面试题】malloc/free和new/delete的区别
面试就喜欢考这种 功能类似 原理类似的 概念对比,主要从 用法 和 原理 两方面思考
malloc / free 和 new / delete 的共同点是:都是从堆上申请空间,并且需要用户手动释放。
不同的地方是:
1、malloc 和 free 是函数,new 和 delete 是操作符
2、malloc 申请的空间不会初始化,new 可以 初始化(是可以,不是一定)
3、malloc 申请空间时,需要手动计算空间大小并传递(如 sizeof(int) * 8),new 只需在其后跟上空间的类型即可, 如果是多个对象,[] 中指定对象个数即可
4、malloc 的返回值为 void*, 在使用时必须强转,new 不需要,因为 new 后跟的是空间的类型
5、malloc 申请空间失败时,返回的是NULL,因此使用时必须判空,new 不需要,但是 new 需要捕获异常,会自己抛出异常
6、申请自定义类型对象时,malloc/free 只会开辟空间,不会调用构造函数与析构函数,而new 在申请空间 后会调用构造函数完成对象的初始化,delete 在释放空间前会调用析构函数完成空间中资源的清理
原文地址:https://blog.csdn.net/2301_79499548/article/details/140516214
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!