【C++ 初阶】:内存管理的迭代革新——从malloc/free 到 new/delete 的时代更迭

🎈主页传送门****:良木生香

🔥个人专栏:《C语言》 《数据结构-初阶》 《程序设计》《鼠鼠的C++学习之路》

🌟人为善,福随未至,祸已远行;人为恶,祸虽未至,福已远离



前言:在此之前,我们已经花了三篇文章的大篇幅来讲解C++初阶中的"类和对象"这个基础的语法知识,很庆幸我们熬过了C++概念中的第一座大山(bushi)。俗话说:"人往高处走,水往低处流"。我们要继续进步继续前进,所以今天我们来学习C++中的内存管理,看看与C语言中的动态内存管理有何不同。


目录

一、概念引入

二、C语言的内存管理与C++的内存管理

2.1、C语言的内存管理

[1. realloc 底层自带旧内存回收逻辑](#1. realloc 底层自带旧内存回收逻辑)

[2. 两种扩容场景,统一不用手动 free](#2. 两种扩容场景,统一不用手动 free)

2.2、C++的内存管理

2.3、C与C++的内存管理对比

三、new/delete的底层实现

3.1、new的底层:

[operator new与new](#operator new与new)

3.2、delete的底层

[operator delete与delete](#operator delete与delete)

3.3、new和delete的底层用法:

内置类型:

自定义类型:

[new T[N]原理:](#new T[N]原理:)

[delete T[N]原理:](#delete T[N]原理:)

[四、定位new表达式(placement new)](#四、定位new表达式(placement new))

[五、new/delete 与 malloc/free的区别](#五、new/delete 与 malloc/free的区别)


一、概念引入

在学习新知识之前,我们先由一道题进行知识的回顾:

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. 选择题: 选项:A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)

globalVar在哪里?____ staticGlobalVar在哪里?____

staticVar在哪里?____ localVar在哪里?____

num1 在哪里?____

char2在哪里?____ *char2在哪里?____

pChar3在哪里?____ *pChar3在哪里?____

ptr1在哪里?____ *ptr1在哪里?____

答案:CCCAA AAADAB

题解:

前备知识:除了sizeof,&,初始化这三种情况代表整个数组,其他情况数组名都是代表首元素地址。

1、global定义在main()函数之外,放在的是静态区;staticGlobal定义时有static修饰,也在 静态区。

2、staticVar虽然是在main()函数中定义的,但是有static修饰,所以在静态区。

3、localVar在main函数中定义,没有任何限定符修饰,所以属于栈区。

4、num1虽然是数组,但是是在main()函数中定义的,所以属于栈区。

5、char2本质是数组(虽然内容是字符串,但是这个字符串是从常量区拷贝过来的),属于栈区。

6、*char2:数组名在进行运算(如+1,解引用)的时候是被当做数组的首地址,当对首地址进行解引用,得到的是数组的首元素,char2在栈上,首元素也是在栈上。

7、pChar3被const关键字限定,但是它本质是指针变量,所以属于栈区,即使指向的内容属于代码段

8、*pChar3是对pChar3进行解引用,其指向的内容是字符串,所以*pChar3属于代码段。

9、ptr1是指针变量,属于栈区,*ptr1指向的是在堆上开辟的空间的首地址,所以*ptr1属于堆。

具体的显式结果可以参考下面这张图:

说明:

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

二、C语言的内存管理与C++的内存管理

2.1、C语言的内存管理

在C语言中对于内存申请有三种方式:malloc、calloc、realloc

**malloc:**只申请空间,不对空间内容进行初始化

calloc: 申请空间的同时也进行初始化,初始化成什么内容,由程序员决定

**realloc:**对已经存在的空间进行扩容,如果剩下的空间足够扩容,那就直接在后面申请空间,如果后面的空间不够,那就重新找一块空间申请扩容后的大小。

realloc对原有空间进行扩容以后,要对原空间进行手动释放吗?不用,因为:

1. realloc 底层自带旧内存回收逻辑

realloc(ptr, new_size) 做了三件事:

  1. 开辟新的更大内存块
  2. 自动把原旧空间的数据拷贝到新空间;
  3. 系统自动释放、回收原来的旧堆内存 ,不需要你手动写 free(ptr)

2. 两种扩容场景,统一不用手动 free

  • 场景①:原空间后面连续内存足够 直接原地扩容,没有开辟新空间,旧内存直接续用,完全不用释放。

  • 场景②:原空间后面内存不够 找一块新堆空间 → 拷贝数据 → 自动 free 掉旧地址 → 返回新地址。

malloc的底层实现原理:【CTF】GLibc堆利用入门-机制介绍_哔哩哔哩_bilibili

2.2、C++的内存管理

C++对于内存管理使用了一种与C语言不同的方式:new和delete。其与C语言的不同在于,C语言中的malloc/calloc/realloc和free是函数的形式,而new和delete是C++的关键字,是一种操作符,在使用的时候不用像C语言那样自己计算大小,强制类型转换等等。

使用方式如下:

cpp 复制代码
#include<iostream>
using namespace std;

int main() {

	int* ptr = new int;  //创建单个对象

	int* ptr_for_arr = new int[10];  //申请一块大小为40个字节的空间

	return 0;
}

一般来说,C++的new是不会进行初始化的,只能手动初始化:

cpp 复制代码
int* ptr = new int(10);    //申请四个字节,初始化为10;
int* ptr_arr = new int[10]{1,2,3,4};    //申请40个字节大小,前16个字节初始化为1,2,3,4,剩下的自动为0

对于delete也是同样的道理:

cpp 复制代码
delete ptr;	//删除单个对象的空间
delete[] ptr_for_arr;	//删除连续空间

显式图如下:

在delete这一块有一个知识点:delete的类型要和申请的空间类型相匹配:申请的是一块连续的空间,那delete就要用delete[]类型区释放,如果用delete(没有[])的话,那只会释放这一段空间的第一个地址,并且C/C++都不支持分段释放内存,所以类型不匹配会导致释放失败。

2.3、C与C++的内存管理对比

看到这里,感觉malloc/free的功能与new/delete也不差多少啊,为什么还要创建这么一出呢?使用new有意义吗?

哎,这时候就由说法了,C++总体而言是为了弥补C语言的不足才诞生的,malloc/free针对的是只是具体的变量,而new/delete可以对类进行操作,既然有类这个概念了,那配套的设施总该配套齐全吧。

cpp 复制代码
#include<iostream>
using namespace std;

class A {
public:
	//构造函数
	A(int a = 1)
		:_a(a)
	{
		cout << "a已经初始化成" << _a << "了" << endl;
	}

	//析构函数
	~A() {
		cout << "已经析构了" << endl;
	}
private:int _a;
};

int main() {

	//使用malloc:
	int* ptr_malloc = (int*)malloc(sizeof(int) * 1);
	//使用new
	A* ptr_new = new A(3);

	//free
	free (ptr_malloc);
	//delete
	delete ptr_new;
	return 0;
}

运行结果为:

所以new/delete 和 malloc/free最大区别是 new/delete对于【自定义类型】除了开空间还会调用构造函数和析构函数

他们的区别还会在一种场景中体现:内存申请失败

cpp 复制代码
#include<iostream>
using namespace std;

int count_num = 0;


void Func_new() {
	int* ptr = nullptr;
	int i = 0;
	try {
		do {
			ptr = new int[1024 * 1024];
			cout << i << ':' << ptr << endl;
			i++;
		} while (ptr);
	}
	catch (const bad_alloc& e) {
		cout << "失败原因" << e.what() << endl;
	}
	delete[] ptr;
}

void Func_malloc() {
	int* ptr = nullptr;
	int i = 0;
	do {
		ptr = (int*)malloc(sizeof(int) * 1024 * 1024);
		cout << i << ':'<<ptr << endl;
		i++;
	} while (ptr);
	free(ptr);
}

int main() {

	//Func_new();

	Func_malloc();
	return 0;
}

上面这段代码就是对malloc和new在内存申请失败后的对比。

当使用new进行申请失败后,运行结果是这样的:

也就是说,能申请4870次4MB,一共就差不多是20G的空间。

使用malloc的运行结果如下:

在最后一次申请失败的时候,显示的是一串0000000,这就说明malloc返回的是nullptr,而不会像new那样抛异常。

三、new/delete的底层实现

newdelete 是 C++ 运算符(操作符), 它们在底层 会调用对应的函数(operator new / operator delete 最终由编译器翻译成具体机器指令。

下面我们一个一个来讲解:

3.1、new的底层:

new在使用的时候会调用一个叫做operator new的全局函数,它和new的关系是:

operator new与new

new的功能有三种:1、申请空间 ;2、调用构造函数;3、返回地址

operator的功能有两种:1、申请空间 ;2、抛异常

当new被使用的时候,会调用operator new函数,operator new函数又回去调用malloc(没错!new的底层就是malloc实现的!!!),如果空间申请成功了,那operator与malloc没有区别,就是纯纯的申请空间,但是如果申请失败,那将malloc封装起来的operator new就会抛出异常,这就导致了new在申请空间时也会抛出异常。operator new是new的一个组成部分,他们的关系显式如下:

cpp 复制代码
new 表达式
   ↓  ↓  ↓
1. 调用 operator new()   <-- 负责申请内存(可抛异常)
2. 调用 构造函数          <-- 负责初始化对象
3. 返回指针

3.2、delete的底层

delete的底层与new的底层原理差不多,毕竟是一对搭档,delete在使用的时候会调用一个叫做operator delete的全局函数,它和new的关系是:

operator delete与delete

delete在被使用的时候会调用operator delete,operator delete又会调用free将空间进行释放,operator delete与类的析构函数共同组成了delete。显式关系如下:

cpp 复制代码
delete 表达式(运算符)
      ↓
1. 调用对象的析构函数      【清理资源】
      ↓
2. 调用 operator delete(ptr) 【函数:只释放内存】

所以综上所述,new和delete的底层依旧是malloc和free,只不过是在malloc和free的基础上进行了改进,使得功能更加的全面便捷。

3.3、new和delete的底层用法:

内置类型:

如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。

自定义类型:

以下面的代码为例子:

cpp 复制代码
#include<iostream>
using namespace std;

class A {
public:
	//构造函数
	A(int a=0):
		_a(a)
	{
		cout << "A()" << endl;
	}

	//析构函数
	~A() {
		cout << "~A()" << endl;
	}
private:
	int _a;
};

int main() {

	A* ptr = new A[10];
	return 0;
}

new原理:

调用operator new对空间进行申请,再调用构造函数对其进行初始化。

delete原理:

调用析构函数,对空间中的资源进行清理,再调用operator对空间进行释放。

new T[N]原理:

调用operator new申请空间,循环N次申请,再在申请的空间上调用N次构造函数进行初始化。

注意!!!当自定义类型中显式写了析构函数,new会多申请4/8个字节的空间用来存放N,以记录后面要调用几次析构函数。

实际上只是想申请40个字节的空间:

cpp 复制代码
A* ptr = new A[10];

这就是因为类中显式写了析构函数,如果将析构函数注释掉,那么size就会变成40:

显式图如下:

delete T[N]原理:

对应的,在释放内存的时候就要用delete[] ptr才能将整个的N大小的空间释放完毕,如果使用delete ptr指挥释放前一小块空间:

这样就会报错。

但是如果没有自己显式写析构函数,编译器就会自己判断要不要多开空间来存放空间大小的信息,当发现没有什么东西能释放后就不会多开空间。

实际上想要强调的就是类型匹配。

补充一次点:如果像直接使用operator函数也是可以的,就相当于封装版的malloc。

四、定位new表达式(placement new)

定位new表达式的作用是对一块已经开辟好的空间进行初始化,(不会管这空间从哪里来,只负责初始化),一般用于高性能池化技术中。可以根据下面的代码对其进行基本的了解:

cpp 复制代码
#include<iostream>
using namespace std;

class A {
private:
	int _a;
public:
	//构造函数
	A(int a = 10):
		_a(a)
	{
		cout << "A():" << endl;
	}

	//析构函数
	~A() {
		cout << "~A():" << endl;
	}
};

int main() {

	//placement管的只是对已有的空间进行初始化,所以这里我们用malloc申请空间再用placement new进行初始化
	A* ptr = (A*)malloc(sizeof(A) * 1);

	//食用placement new进行初始化
	//食用格式为:new(point_address)type(Init_num)
	new(ptr)A(54);   //将ptr指向的空间初始化成54

	//析构的时候也只能自己手动析构
	ptr->~A();


	return 0;
}

因为定位new是不参与申请空间的,所以在进行析构的时候也只能自己手动清理资源,不能释放空间(空间不是我申请的自然不能通过我来释放)。

五、new/delete 与 malloc/free的区别

在上面讲了这么多,大家对于new/delete与malloc/free肯定有了一个基本的认识,现在我们就把它们的区别都罗列一遍吧

  1. malloc 和 free 是函数, new 和 delete 是操作符
  2. malloc 申请的空间不会初始化, new 可以初始化
  3. malloc 申请空间时,需要手动计算空间大小并传递, new 只需在其后跟上空间的类型即可,
    如果是多个对象, [] 中指定对象个数即可
  4. malloc 的返回值为 void*, 在使用时必须强转, new 不需要,因为 new 后跟的是空间的类型
  5. malloc 申请空间失败时,返回的是 NULL ,因此使用时必须判空, new 不需要,但是 new 需
    要捕获异常
  6. 申请自定义类型对象时, malloc/free 只会开辟空间,不会调用构造函数与析构函数,而 new
    在申请空间后会调用构造函数完成对象的初始化, delete 在释放空间前会调用析构函数完成
    空间中资源的清理释放

在看的时候不要想着死记硬背,这样是及不牢固的,要将知识点全部理解透彻,形成框架,这样在面试的时候才能对答如流。


那么以上就是本次所有的内容了

文章是自己写的哈,有什么描述不对的、不恰当的地方,恳请大佬指正,看到后会第一时间修改,感谢您的阅读~~~~


博主手写笔记:

相关推荐
枫叶丹41 小时前
【HarmonyOS 6.0】ArkWeb:Web组件销毁模式深度解析
开发语言·前端·华为·harmonyos
傻啦嘿哟1 小时前
使用 Python 管理 Word 节及页面布局设置
开发语言·python·word
小则又沐风a2 小时前
深剖string内部结构 手撕string
java·前端·数据库·c++
XGeFei2 小时前
__init__ 初始化方法
开发语言·python
Rust研习社2 小时前
Rust 并发同步:Mutex 与 RwLock 智能指针
开发语言·后端·rust
会编程的土豆2 小时前
常用算法里的细节
数据结构·c++·算法·图论
code_li2 小时前
▍Type-C 不等于 Type-C,是看起来已经「统一」了
c语言·开发语言·type-c
CHANG_THE_WORLD2 小时前
C 语言的 `fread` 与 C++ 的 `ifstream::read` 区别及设计哲学
java·c语言·c++
geovindu2 小时前
go: Abstract Factory Pattern
开发语言·后端·设计模式·golang