
🎈主页传送门****:良木生香
🔥个人专栏:《C语言》 《数据结构-初阶》 《程序设计》《鼠鼠的C++学习之路》
🌟人为善,福随未至,祸已远行;人为恶,祸虽未至,福已远离

前言:在此之前,我们已经花了三篇文章的大篇幅来讲解C++初阶中的"类和对象"这个基础的语法知识,很庆幸我们熬过了C++概念中的第一座大山(bushi)。俗话说:"人往高处走,水往低处流"。我们要继续进步继续前进,所以今天我们来学习C++中的内存管理,看看与C语言中的动态内存管理有何不同。
目录
[1. realloc 底层自带旧内存回收逻辑](#1. realloc 底层自带旧内存回收逻辑)
[2. 两种扩容场景,统一不用手动 free](#2. 两种扩容场景,统一不用手动 free)
[operator new与new](#operator new与new)
[operator delete与delete](#operator delete与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);
}
- 选择题: 选项: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属于堆。
具体的显式结果可以参考下面这张图:

说明:
- 栈 又叫堆栈 -- 非静态局部变量 / 函数参数 / 返回值等等,栈是向下增长的。
- 内存映射段 是高效的 I/O 映射方式,用于装载一个共享的动态内存库。用户可使用系统接口
创建共享共享内存,做进程间通信。- 堆 用于程序运行时动态内存分配,堆是可以上增长的。
- 数据段 -- 存储全局数据和静态数据。
- 代码段 -- 可执行的代码 / 只读常量
二、C语言的内存管理与C++的内存管理
2.1、C语言的内存管理
在C语言中对于内存申请有三种方式:malloc、calloc、realloc
**malloc:**只申请空间,不对空间内容进行初始化
calloc: 申请空间的同时也进行初始化,初始化成什么内容,由程序员决定
**realloc:**对已经存在的空间进行扩容,如果剩下的空间足够扩容,那就直接在后面申请空间,如果后面的空间不够,那就重新找一块空间申请扩容后的大小。
realloc对原有空间进行扩容以后,要对原空间进行手动释放吗?不用,因为:
1.
realloc底层自带旧内存回收逻辑
realloc(ptr, new_size)做了三件事:
- 开辟新的更大内存块;
- 自动把原旧空间的数据拷贝到新空间;
- 系统自动释放、回收原来的旧堆内存 ,不需要你手动写
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的底层实现
new和delete是 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肯定有了一个基本的认识,现在我们就把它们的区别都罗列一遍吧
- malloc 和 free 是函数, new 和 delete 是操作符
- malloc 申请的空间不会初始化, new 可以初始化
- malloc 申请空间时,需要手动计算空间大小并传递, new 只需在其后跟上空间的类型即可,
如果是多个对象, [] 中指定对象个数即可- malloc 的返回值为 void*, 在使用时必须强转, new 不需要,因为 new 后跟的是空间的类型
- malloc 申请空间失败时,返回的是 NULL ,因此使用时必须判空, new 不需要,但是 new 需
要捕获异常- 申请自定义类型对象时, malloc/free 只会开辟空间,不会调用构造函数与析构函数,而 new
在申请空间后会调用构造函数完成对象的初始化, delete 在释放空间前会调用析构函数完成
空间中资源的清理释放
在看的时候不要想着死记硬背,这样是及不牢固的,要将知识点全部理解透彻,形成框架,这样在面试的时候才能对答如流。
那么以上就是本次所有的内容了
文章是自己写的哈,有什么描述不对的、不恰当的地方,恳请大佬指正,看到后会第一时间修改,感谢您的阅读~~~~
博主手写笔记:




