赞
踩
C语言内存管理方式在C++中可以继续使用,但使用起来比较麻烦,因此 C++又提 出了自己的内存管理方式:通过 new 和 delete 操作符进行动态内存管理。
除了用法,和 C语言的 malloc 没什么区别
// 内置类型
// 使用什么类型就 new 什么
int* p1 = new int;
int* p2 = new int[10]; // new 多个
delete p1;
delete[] p2;
// 初始化:自己不初始化,数组中默认初始化为 0(和C语言相同),而 new 一个的值是随机值
int* p3 = new int(33);
int* p4 = new int[10] {1, 2, 3, 4}; // new 多个,花括号初始化多个
// 自定义类型 // 使用 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; };
总结:new 可以调用构造和析构,更加适用于 自定义类型,malloc 不再适用了(95%的场景都要用 new)
另外,C++没有realloc扩容,需要手动扩容
之前 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; }
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 和一些其他东西(暂时不用了解)
// 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
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[] 释放时,没有告诉编译器这里的需要析构的对象个数,所以底层会自己先记录下来
⭐
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。