文章目录
- 前言
- 一、C/C++内存区域划分
- 二、C/C++动态内存管理
- C语言动态内存管理
- C++动态内存管理
- 对于内置类型
- 对于自定义类型
- 三、new和delete的底层实现
- 四、new和delete的实现原理
- 五、定位new
- 六、malloc/free和new/delete的区别
- 总结
前言
软件开发过程中,内存管理的重要性不言而喻
因此我们有必要了解一下C++中关于内存管理的一些特性
C++对内存的自由度使其获得了更高的性能,以及更高的难度。
内存泄漏往往是每个C++学习者绕不开的错误, 而内存管理的水平高低也能看出一个编程者的能力
一、C/C++内存区域划分
我们先来看一下以下代码:
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);
}
你能说出globalVal、staticGlobalVar、staticVar、localVar、num1、*num1存在哪里吗?
在C++中,程序的内存区域从低地址到高地址划分如下:
- 代码段:存储可执行程序的代码和只读常量
- 数据段:存储已初始化的全局变量和静态变量
- 堆:用于程序运行时动态内存分配,从低地址向高地址增长
- 栈:又叫堆栈,存储非静态局部变量/函数参数和返回值等,从高地址向低地址增长
图形语言如下:
我们倒回去看,我挑几个比较有意思的来细讲,你也可以重点关注一下,staticVar、char2、*char2、pChar3、*pChar3、*ptr2分别存储在哪里
变量名 | 存储段 |
---|---|
staticVar | 静态局部变量,存在数据段 |
char2 | 字符指针变量,存在栈 |
*char2 | 数组元素,存在栈,不是存在代码段,因为不是只读!!! |
pChar3 | 局部指针变量,存在栈 |
*pChar3 | 只读变量,存在代码段 |
*ptr2 | 动态分配的内存,存在堆 |
哦,对了,关于上面所说的“栈是向下增长的,而堆是向上增长的”,
简单来说就是在栈区开辟空间,先开辟的空间地址较高,而在堆区开辟空间,先开辟的空间地址较低
你可以通过以下代码来验证一下:
// 实在验证不出来就算了,这个跟编译器等环境关系很大
#include <iostream>
using namespace std;
int main()
{
// 栈区开辟空间,先开辟的空间地址高
int a = 10;
int b = 20;
cout << &a << endl;
cout << &b << endl;
// 堆区开辟空间,先开辟的空间地址低
// 具体实现中,关于堆我们可能会验证失败
// 你可以试着想一下这是为什么?
int* c = (int*)malloc(sizeof(int)* 10);
cout << c << endl;
// free(c);加了这行,发现两个输出相同
// 说明在堆区,后开辟的空间也有可能位于前面某一被释放的空间位置
int* d = (int*)malloc(sizeof(int)* 10);
cout << d << endl;
return 0;
}
二、C/C++动态内存管理
C语言动态内存管理
我们来回顾一下几种用于动态分配内存的函数:malloc、calloc、realloc 和 free,这些函数用于在程序运行时动态地分配和释放内存
malloc:用于分配指定大小的内存块,内存中的内容未初始化
calloc:类似于 malloc,但会将内存初始化为零。它的参数为元素的数量和每个元素的大小
realloc:用于调整之前分配的内存块的大小,如果新大小大于原大小,可能会移动内存块的位置
来个示例代码:
int* ptr1 = (int*)malloc(sizeof(int) * 4); // 分配4个int类型大小的内存块
int* ptr2 = (int*)calloc(4, sizeof(int)); // 分配并初始化4个int类型大小的内存块
int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4); // 重新分配内存
free(ptr1);
free(pter2);
free(ptr3);
C++动态内存管理
C++继承了C语言的内存管理方式,并在此基础上引入了new和delete操作符,提供更方便的动态内存管理机制,这并不奇怪,因为C++本来就是祖师爷觉得C麻烦,在其基础上发展而来的
new 和 delete 适用于对象的动态内存分配,并且会自动调用构造函数和析构函数,这很重要
对于内置类型
对于内置类型,其实 new 和 delete 在底层上多大的差别,只是使用的规则要有所区分
以下相对应内容等价
// 动态申请单个int类型的空间
int* p1 = new int; //申请
delete p1; //销毁
// 动态申请单个int类型的空间
int* p2 = (int*)malloc(sizeof(int)); //申请
free(p2); //销毁
// 动态申请10个int类型的空间
int* p3 = new int[10]; //申请
delete[] p3; //销毁
// 动态申请10个int类型的空间
int* p4 = (int*)malloc(sizeof(int)* 10); //申请
free(p4); //销毁
// 动态申请单个int类型的空间并初始化为10
int* p5 = new int(10); //申请 + 赋值
delete p5; //销毁
// 动态申请一个int类型的空间并初始化为10
int* p6 = (int*)malloc(sizeof(int)); //申请
*p6 = 10; //赋值
free(p6); //销毁
// 动态申请10个int类型的空间并初始化为0到9
int* p7 = new int[10]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; //申请 + 赋值
delete[] p7; //销毁
// 动态申请10个int类型的空间并初始化为0到9
int* p8 = (int*)malloc(sizeof(int)* 10); //申请
for (int i = 0; i < 10; i++) //赋值
{
p8[i] = i;
}
free(p8); //销毁
申请和释放单个元素的空间,使用new和delete操作符;申请和释放连续的空间,使用new[ ]和delete[ ]
对于自定义类型
new会调用构造函数,delete会调用析构函数,而malloc和free不会,原理下文再来解释
三、new和delete的底层实现
new和delete并不是函数,而是用户进行动态内存申请和释放的操作符
但是其底层还是需要调用函数
且虽然函数名中带operator,但并不是重载函数,具有很强的误导性!!!
new 和 delete 是用户进行动态内存申请和释放的操作符,operator new 和 operator delete 是系统提供的全局函数,new 在底层调用 operator new 全局函数来申请空间,delete 在底层通过 operator delete 全局函数来释放空间
我们来看一下operator new 和 operator free 的底层实现:
void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
// try to allocate size bytes
void* p;
while ((p = malloc(size)) == 0) // 注意这里,就是malloc
if (_callnewh(size) == 0)
{
// report no memory
// 如果申请内存失败了,这里会抛出bad_alloc 类型异常
static const std::bad_alloc nomem;
_RAISE(nomem);
}
return (p);
}
void operator delete(void* pUserData)
{
_CrtMemBlockHeader* pHead;
RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
if (pUserData == NULL)
return;
_mlock(_HEAP_LOCK); /* block other threads */
__TRY
/* get a pointer to memory block header */
pHead = pHdr(pUserData);
/* verify block type */
_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
_free_dbg(pUserData, pHead->nBlockUse); // 注意这里,就是free
__FINALLY
_munlock(_HEAP_LOCK); /* release other threads */
__END_TRY_FINALLY
return;
}
看不懂?看不懂就对了,我也看不懂,但是你注意一下我两个特意的注释点
可以看出 operator new 实际上也是通过 malloc 来申请空间的,如果 malloc 申请空间成功就直接返回,如果失败则执行用户提供的应对措施,如果用户提供该措施则继续申请空间,否则抛出异常
其实,这也叫封装,就像引用的底层也是用指针的方式实现的
四、new和delete的实现原理
内置类型无非就是包一下,加个抛出异常,而对于自定义类,就复杂了
一、new的原理
调用operator new函数申请空间,在申请的空间上执行构造函数,完成对象的构造
二、delete的原理
在空间上执行析构函数,完成对象中资源的清理工作,调用operator delete函数释放对象的空间
三、new T[N]的原理
调用operator new[]函数,而operator new[]函数实际上又会调用operator new函数完成N个T类型对象的空间申请,在申请的空间上执行N次构造函数
四、delete[]的原理
在空间上执行N次析构函数,完成N个对象的资源清理调用operator delete[]函数,而operator delete[]函数又会调用operator delete函数来释放空间
五、定位new
定位new表达式用于在已分配的原始内存空间中调用构造函数初始化一个对象
使用格式
new (place_address) type或者new (place_address) type(initializer-list)
place_address必须是一个指针,initializer-list是类型的初始化列表
其实这个内容,你可以暂时做个了解,因为这个一般搭配内存池使用,这又牵扯到一个概念叫做池化技术,而内存池分配出的内存没有初始化,所以如果是自定义类型的对象,就需要使用定位new表达式进行显示调用构造函数进行初始化
#include <iostream>
using namespace std;
class A
{
public:
A(int a = 0) // 构造函数
:_a(a)
{}
~A() // 析构函数
{}
private:
int _a;
};
int main()
{
// new(place_address)type 形式
A* p1 = (A*)malloc(sizeof(A));
new(p1)A;
// new(place_address)type(initializer-list) 形式
A* p2 = (A*)malloc(sizeof(A));
new(p2)A(2021);
// 析构函数也可以显示调用
// 这就是为什么只有定位new,没有定位delete的缘故
p1->~A();
p2->~A();
return 0;
}
六、malloc/free和new/delete的区别
首先,它们的共同点都是都是从 堆 上申请空间,并且需要用户手动释放
不同点,那就多了:
- malloc和free是函数,new和delete是操作符
- malloc申请的空间不会初始化,new可以初始化
- malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,
如果是多个对象,[ ]中指定对象个数即可 - malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
- malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需
要捕获异常 - 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new
在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成
空间中资源的清理释放 (这是原理,希望你对此有个深刻的印象)
总结
哈,本节内容还是蛮轻松惬意的,至少跟类和对象比起来是这样
那么现在,我们来接着往下看模板
相信我,这会更有意思!!!