【C++】如何搞定 C++ 内存管理?

前言:内存管理在C/C++中扮演着重要的角色,同时它也是一把双刃剑,管理得好可以保障程序得稳定、提升运行效率;管理不好就会引发野指针、内存泄露、或者导致程序直接崩溃或异常。所以内存管理对于我们来说还是非常重要的!本篇文章所要讲的new delete等可以帮助你更好的掌握内存管理!

✨ 坚持用 清晰易懂的图解 + 代码语言, 让每个知识点都 简单直观 !

🚀 个人主页MSTcheng · CSDN

🌱 代码仓库MSTcheng · Gitee

📌 专栏系列

💬 座右铭 : "路虽远行则将至,事虽难做则必成!"

文章目录

一,C/C++的内存分布

1.1C/C++内存分布

在C语言阶段学习的时候,总会有一些问题就是我们写过的各种各样的代码,局部变量,全局变量,静态变量等它们到底是存在哪的呢?相信有很多人在学C/C++的时候会有这些疑问,下面就来看看C/C++中的内存分布:

cpp 复制代码
int globalVar = 1;
static int staticGlobalVar = 1;
void Test()
{
	static int staticVar = 1;
	int localVar = 1;
	int num1[10] = { 1, 2, 3, 4 };
	char char2[] = "abcd";
	const char* pChar3 = "abcd";
	int* ptr1 = (int*)malloc(sizeof(int) * 4);
	int* ptr2 = (int*)calloc(4, sizeof(int));
	int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
	free(ptr1);
	free(ptr3);
}

上面代码中的一些局部变量、全局变量、静态变量、数组、指针等都存在什么地方?

说明:

  1. 栈又叫堆栈:存储非静态局部变量/函数参数/返回值等等,栈是向下增长的。
  2. 内存映射段:是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信.
  3. 堆:用于程序运行时动态内存分配,堆是可以上增长的。
  4. 数据段:存储全局数据和静态数据。
  5. 代码段:可执行的代码/只读常量。

1.2函数栈帧的创建和销毁

上面我们看到的大部分在函数内部创建的变量,指针,数组,它们基本都是在栈区创建的说明栈区对我们来说也是一块比较重要的区域,那么为什么它们是存在栈区的呢?

  1. 内存管理的高效性: 栈区采用后进先出(LIFO)的分配机制,内存分配和释放仅需移动栈指针,速度极快。函数调用时,栈指针下移分配空间;函数返回时,栈指针上移自动回收内存,无需复杂的内存管理操作。
  2. 生命周期与函数调用匹配: 局部变量的生命周期严格绑定函数执行周期。栈区的自动分配和释放特性完美契合这一需求,避免了手动管理内存的复杂性,减少内存泄漏风险。
    下面我们就简单来看看函数栈帧的创建和销毁:

下面以一个简单的main函数调用Add函数为例来看看,函数栈帧创建和销毁的具体过程:

二,C语言中的动态内存管理方式

在C语言中,内存管理是通过我们之前所学过的三个内存函数,malloc calloc realloc来管理的,比如下面这段代码:

c 复制代码
void Test ()
{
// 1.malloc/calloc/realloc的区别是什么?
	int* p2 = (int*)calloc(4, sizeof (int));
	int* p3 = (int*)realloc(p2, sizeof(int)*10);
// 这里需要free(p2)吗?
	free(p3 );
}

这两个问题我们在C语言阶段就已经详细的解答过了,在这就不再赘述了。不了解的读者可以去看我之前写的文章:C语言内存函数

三,C++的内存管理方式

3.1 new和delete的简单使用

我们在之前说过,C++是兼容C的用法的,因此C语言哪些内存管理函数像malloc,calloc,realloc函数是可以在C++中去使用的。但是有些地方就无能为力,因此C++就提出了自己的内存管理方式new和delete操作符进行内存管理。

cpp 复制代码
/new和delete的用法
void Test()
{
	//动态开辟一个int(整形)大小的空间
	int* ptr4 = new int;
	//给创建好的空间初始化 使用()圆括号
	int* ptr5 = new int(1);
	
	//动态开辟10个整形大小的空间 相当于一个数组
	int* ptr6 = new int[10];
	//给新开辟的空间进行初始化
	int* ptr7 = new int[10] {1, 2, 3, 4};//隐式类型转换

	//释放空间用delete delete是一个关键字 不是函数 不需要带括号 
	delete ptr4;
	delete[] ptr6;//释放全部空间
	delete[3] ptr7; //释放ptr7的前3个空间
}

注意:申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[]和delete[]。注意匹配起来使用。

这时候可能就会有人有疑问:上面的代码不用new和delete使用C语言的内存函数也照样能够完成啊,C++的new和delete的优势在哪呢?

  • 对于内置类型new/delete相比于C语言的malloc/free差别不大,但是如果要操作的不是上面的内置类型而是一个类呢?C语言的内存函数还能胜任吗?答案是不行
cpp 复制代码
class A
{ 
public:
		A(int a = 0)
		: _a(a)
	{
		cout << "A():" << this << endl;
		cout << _a << endl;
	} 
	~A()
	{
		cout << "~A():" << this << endl;
	}
private:
	int _a;
}
int main()
{
//使用C语言的内存函数开辟一块空间
	A* p1 = (A*)malloc(sizeof(A));
	//初始化
	//p1->_a=10;在初始化的时候由于_a是私有成员 我们无法在外面访问 所以也就无法初始化
	free(p1);
	//而new创建的对象 就会去调用对应的构造
	//A* p2 = new A(); 不传参数就调用默认构造为100
	A* p2 = new A(10);
	//delete就会去调用对应的析构函数
	delete p2;
}

所以,从上面的代码我们就能看出new和deletemalloc free最大的区别就是:new除了开空间还会去调用类中的构造函数;delete就会去调用类中的析构函数。而mallocfree就不会。
总结:对于内置类型,malloc/free和new/delete没有什么太大的区别,但是对于自定义类型来讲new/delete会调用构造和析构,malloc/free不会。所以从本质上讲malloc/free不能自动的去初始化和释放资源,但是new和delete可以!

四,new和delete的实现原理

4.1 operator new和operator delete

cpp 复制代码
typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		cout << "Stack(int n = 4)" << endl;
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}
	~Stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}

private:
	STDataType* _a;
	size_t    _capacity;
	size_t    _top;
};
int main()
{
	Stack* p1 = new Stack(10);
	delete p1;

//对于内置类型没有资源释放可以new 和free配对用 但还是建议与delete配对用
	int* p2 = new int(1);
	//free(p2);
	delete p2;
	return 0;
}

总结上面的内容就是:当调用new时,就会去调用operator new+构造;当调用delete时就会调用析构+delete注意这里要区分delete和析构,析构不一定释放资源,有资源才释放,而delete是一定释放资源,不能将二者混淆!
当然如果去看operator new和operator delete这两个函数的源码的话就会发现这两个函数的源码其实也是用mallocfree来实现的只不过是对它们进行了封装而已。
那就会有人问了:既然newdelete本质上也使用mallocfree那么为什么还要搞operator new和operator delete呢?

  • 其实是因为C++更喜欢使用抛异常这种方式来应对空间不足的情况,像malloc我们之前就会写一个if判断语句去判断它开辟成功还是开辟失败,但是new如果开辟失败那就会抛异常。(异常在后面章节会介绍,目前就作为了解知识)

4.2 new[]和delete[]的原理

了解完了上面new和delete的原理,下面我们来看看new[]和delete[]的原理:

cpp 复制代码
int main()
{
	A* p1 = new A[10];
	delete[] p1;

	A* p1 = (A*)malloc(sizeof(A));
	//malloc开辟的空间 要手动调用析构
	p1->~A();
	free(p1);

	return 0;
}

五,定位new

定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。

cpp 复制代码
class A
{ 
public:
	A(int a = 0)
		: _a(a)
	{
		cout << "A():" << this << endl;
	} 
	~A()
	{
		cout << "~A():" << this << endl;
	}
private:
	int _a;
};
int main()
{
// p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
	A* p1 = (A*)malloc(sizeof(A));
	new(p1)A; //此处要想调用构造函数就必须new(对象) 如果构造函数有参数就需要传参
	p1->~A();
	free(p1);
	
	A* p2 = (A*)operator new(sizeof(A));
	new(p2)A(10);
	p2->~A();
	operator delete(p2);
return 0;
}

定位 new 的特点:

  1. 不分配内存:仅调用构造函数,内存由用户预先分配。
  2. 显式析构调用:对象析构需手动调用,因为定位 new 不管理内存生命周期。
  3. 无异常抛出:普通 new 在分配失败时抛出 std::bad_alloc,而定位 new 假设内存已有效。
    定位new表达式在实际中一般是配合内存池使用,感兴趣的读者可以去了解一下内存池。

六,malloc/free和new/delete的区别

以下是 malloc/freenew/delete 的比较表格:

比较维度 malloc/free new/delete
本质 函数 操作符
初始化 不初始化申请的空间 可通过构造函数初始化
空间大小计算 需手动计算并传递(例如 sizeof(int) * 10 自动计算,只需指定类型和数量如果是多个对象[]中指定对象个数即可(例如 new int[10]
返回值类型 返回 void*,使用时必需强制转换 返回与类型匹配的指针,无需转换,但要捕获异常
失败处理 返回 NULL,需手动判空 抛出异常(如 std::bad_alloc),需捕获处理
自定义类型对象处理(底层功能) 仅分配/释放内存,不调用构造或析构函数 分配内存后调用构造函数完成初始化,释放前调用析构函数完成空间中资源的清理释放

|---------------------------------------------------------|
| 以上就是本篇文章的所有内容了,感谢各位大佬观看,制作不易还望各位大佬点赞支持一下!有什么问题可以加我私信交流! |

相关推荐
钟离墨笺3 小时前
Go语言-->切片注意点及理解
java·开发语言·golang
余大侠在劈柴3 小时前
go语言学习记录9.23
开发语言·前端·学习·golang·go
麦麦鸡腿堡3 小时前
Java的数组查找
java·开发语言
小欣加油3 小时前
leetcode 98 验证二叉搜索树
c++·算法·leetcode·职场和发展
Vect__3 小时前
list 迭代器:C++ 容器封装的 “行为统一” 艺术
c++·list
郝学胜-神的一滴3 小时前
解析前端框架 Axios 的设计理念与源码
开发语言·前端·javascript·设计模式·前端框架·软件工程
沐怡旸3 小时前
【基础知识】C++的几种构造函数
c++
Dream achiever3 小时前
10.WPF布局
开发语言·c#·wpf
HyperAI超神经3 小时前
【TVM 教程】设置 RPC 系统
开发语言·网络·人工智能·python·网络协议·rpc·tvm