【C++】内存管理
在C语言中,我们可以通过 malloc/calloc/realloc/free 等函数进行动态内存管理,而到了C++中就有了新的内存管理方式: new/delete
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);
}
以下数据分别存在哪里?选项:A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)
- globalVar ->静态区,全局变量存在静态区
- staticGlobalVar ->静态区,静态变量存在静态区
- staticVar ->静态区,静态变量存在静态区
- localVar ->栈,普通变量存在栈区
- num1 ->栈,数组存放于栈区
- char2 ->栈,拷贝常量字符串给字符数组,数组存放于栈区
- *char2 ->栈,数组中的内容存在栈区,如上图
- pChar3 ->栈,虽然指向的内容存放于常量区,但是指针本身存放于栈区
- *pChar3 ->常量区,指向的内容是常量字符串,所以存放于常量区,如上图
- ptr1 ->栈,虽然指向的内容存放于堆,但是指针本身存放于栈区
- *ptr1 ->堆,指向的内容是在堆区开辟的空间,如上图
C语言的内存管理
C语言的内存管理在本篇不是重点,所以只稍微提一下
malloc/calloc/realloc的区别
- mallco,参数是需要开辟的空间总大小
- calloc,更加精细一些,参数是要开辟多少个空间,每个空间有多大。而且还会将空间内的值初始化为0
- realloc,对已有空间进行扩容。如果原空间后面有足够的可使用空间,就会原地扩容;如果不够,就会异地扩容:将原空间的数据拷贝至新空间+扩容,销毁原空间,返回新空间地址
mallco 系列函数使用时都需要强制类型转换,且需要对返回值进行检查,如果开辟空间失败需要进行相应的处理。而且数据初始化很不方便
int* p1 = (int*) malloc(sizeof(int));
if (p1 == NULL)
{
perror("malloc fail\n");
exit(-1);
}
C++的内存管理
为了解决以上痛点,C++提出了新的内存管理方式:使用new和delete操作符进行内存管理
new/delete 内置类型
使用格式如下:
// 申请空间:内置类型指针 指针名 = new 内置类型
int* p1 = new int;
// 释放空间:delete 指针名
delete p1;
使用 new/delete 管理空间,不用强转指针,也不用指定空间大小,使用起来简便许多。不仅如此,new还支持初始化,如下
int *p1 = new int(10) // 申请一个 int 空间,并初始化为10
如果想一次申请多个空间,要在类型后面加[数量]
,格式如下:
int* p1 = new int[10]; // 申请10个 int 类型的空间
一次申请多个空间也可以初始化,格式如下:
int* p1 = new int[3] {1, 2, 3};
释放多个空间时,一定要在delete后加[]
int* p1 = new int[3] {1, 2, 3};
delete[] p1;
- 单个空间的申请和释放要使用
new和delete
操作符 - 多个空间的申请和释放要使用
new[]和delete[]
操作符 - 以上操作符一定要匹配使用
new/delete 自定义类型
在C语言中,使用 malloc 函数创建一个自定义对象有些麻烦,例如链表节点
struct ListNode
{
int _val;
ListNode* _next;
};
// 创建新节点
struct ListNode* CreateNode(int x)
{
ListNode* newnode = (struct ListNode*)malloc(sizeof(struct ListNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(-1);
}
newnode->_val = x;
newnode->_next = NULL;
return newnode;
}
而使用new就可以直接创建自定义类型对象,会自动为自定义类型开辟空间,并且调用构造函数
class ListNode
{
public:
ListNode(int val)
:_val(val)
, _next(nullptr)
{}
private:
int _val;
ListNode* _next;
};
ListNode* ln1 = new ListNode(10); // 开辟空间+调用构造
使用 delete 释放自定义类型时,会先自动调用相应的析构函数,再释放空间。如果先释放对象的空间,就找不到自定义对象了,自然调用不了析构函数,如果对象申请了空间,就会造成内存泄漏
operator new与operator delete函数
operator new与operator delete不是 new 和 delete 的重载,而是独立的函数。
- new 和 delete 是操作符,而operator new与operator delete是全局函数
- new 在底层调用 operator new 来申请空间;delete 在底层调用 operator delete 来释放空间
- new[] 在底层调用 operator new[] 来申请空间;delete[] 在底层调用 operator delete[] 来释放空间
而 operator new 是通过 malloc 申请空间的,并且封装了其他功能;operator delete 也类似,是通过 free 来释放空间的
new 和 delete 的原理
内置类型
对于内置类型,malloc/free 和 new/delete 基本类似,但不同的是:
- malloc 失败会返回 NULL,需要手动检查
- new 失败会抛异常,需要捕获
自定义类型
如下自定义类型用来测试
class A
{
public:
A(int val = 0)
:_a(val)
{
cout << "A(int val = 0)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
new 和 delete
- new一个自定义类型的对象,会先调用 operator new 为对象在堆上申请空间,然后在空间上调用对象的构造函数,完成对象的构造
反汇编:
- delete对象,会先在空间上调用对象的析构函数,释放对象申请的空间,然后调用 operator delete 销毁对象,释放对象的空间
new[] 和 delete[]
- new A[10] :会先调用 operator new[],为 10 个A类对象申请空间,之后在申请的空间上调用 10 次构造函数,完成构造
- delete[] A :先在申请的空间上调用 10 次析构函数,清理掉对象申请的资源,然后调用 operator delete 释放这 10 个A类对象的资源
new/delete 和 new[]/delete[] 要匹配使用
new A[10]表明了有10个空间,但是delete[] A 中没有数字,delete是如何得知要调用10次析构函数?那是因为在 new 时,我们已经给过数字,那么在申请空间时,会多申请一个 int 大小的空间,来存放申请了多少个空间
这样 delete[] 就会知道多开了一个 int 大小的空间,会先调用 10 次析构,然后调用 operator delete 从多开的空间开始释放空间
如果new[]没有配套使用delete[],而是使用delete,那么就不会从额外申请的空间开始释放,这样就会出错,因为释放空间只能从申请的空间开头释放
定位new(placement-new)
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象
使用格式:
- new (place_address) type或者new (place_address) type(initializer-list)
- place_address必须是一个指针,initializer-list是类型的初始化列表
使用场景:
定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如
果是自定义类型的对象,需要使用new的定义表达式进行显式调用构造函数进行初始化。
A* pa = (A*)malloc(sizeof(A)); // pa只是指向一块大小和A相同的空间,未初始化,不算做一个对象
new(pa)A(10); // 显式调用构造,初始化
总结:malloc/free 和 new/delete 的区别
- malloc 是函数,new 是操作符
- malloc 返回的是 void* 指针,需要强转才可以使用;new 返回的是对应的类型的指针
- malloc 需要手动计算开辟空间的大小并传递;new 只需要在后面跟上空间的类型即可
- malloc 不可以初始化空间数据;new 可以初始化
- malloc 失败时会返回 NULL,需要手动判空;new 失败时会抛异常,需要捕获异常
- molloc/free 申请自定义对象时,不会调用构造和析构;new 会在申请空间后调用对象的构造函数完成构造,delete 会在释放对象空间之前,调用对象的析构函数清理对象申请的资源