【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 在释放空间前会调用析构函数完成空间中资源的清理

相关推荐

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-18 19:40:02       70 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-18 19:40:02       74 阅读
  3. 在Django里面运行非项目文件

    2024-07-18 19:40:02       62 阅读
  4. Python语言-面向对象

    2024-07-18 19:40:02       72 阅读

热门阅读

  1. Web前端-Web开发CSS基础5-浮动

    2024-07-18 19:40:02       19 阅读
  2. 【J1期末测试】学习之星

    2024-07-18 19:40:02       26 阅读
  3. MySQL 溢出页、页分裂、表空间碎片

    2024-07-18 19:40:02       24 阅读
  4. mysql8和mysql5版本在使用mybatis框架时的注意事项

    2024-07-18 19:40:02       25 阅读
  5. C++基础语法:STL之容器(3)--序列容器中的deque

    2024-07-18 19:40:02       20 阅读
  6. 一文搞懂C语言

    2024-07-18 19:40:02       23 阅读
  7. Go语言 字典(map)

    2024-07-18 19:40:02       27 阅读
  8. 深拷贝一个json,可以循环调用

    2024-07-18 19:40:02       23 阅读
  9. VUE +Element-plus+leanCloud 分页逻辑

    2024-07-18 19:40:02       28 阅读
  10. 测试面试题(七)

    2024-07-18 19:40:02       24 阅读
  11. 从Oracle到PostgreSQL:详细对比与迁移工具说明

    2024-07-18 19:40:02       24 阅读
  12. jquery return false的作用

    2024-07-18 19:40:02       22 阅读