当前位置:   article > 正文

【C++修炼之路 第三章】内存管理:new 与 delete

【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
  • 3
  • 4
  • 5
  • 6
  • 7

1.2 初始化

// 初始化:自己不初始化,数组中默认初始化为 0(和C语言相同),而 new 一个的值是随机值
int* p3 = new int(33);
int* p4 = new int[10] {1, 2, 3, 4};  // new 多个,花括号初始化多个
  • 1
  • 2
  • 3

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}};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22



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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24



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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18



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;
  • 1
  • 2
  • 3
  • 4

举个例子:

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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14




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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

打开调试 查看内存窗口:

看 p1 的开辟空间的内存分布:这里一共开了 44 个字节,40 个字节存储了 数值2(我构造函数那里赋值了 数值2)

第一行的那 4 个字节存储着 a (就是 十进制的 数字10 ):表示对象个数

存储对象个数 :是为了方便提醒 析构函数 ,当前这里一共创建了多少个对象,便于析构

在这里插入图片描述


连续开辟空间 ,且类中有显式的析构函数时,编译器会自动在开辟的总空间地址的前面一个位置存入 此次 创建的对象个数

这样写不会触发:

A* p1 = new A;
  • 1

带有括号的才会触发:表示连续开辟空间

A* p1 = new A[1]; // 这个虽然只有一个,但也算
A* p1 = new A[2];
A* p1 = new A[10];
  • 1
  • 2
  • 3

分析过程:

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[] 释放时,没有告诉编译器这里的需要析构的对象个数,所以底层会自己先记录下来


声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/寸_铁/article/detail/850055
推荐阅读
相关标签