在之前的C语言部分,我们已经介绍过内存中的存储区域的划分:包括内核空间(用户代码不能访问)、栈、内存映射段(文件映射,动态库,匿名映射)、堆、数据段(静态区)、代码段(常量区)。
并且可以熟练的使用malloc、calloc、realloc、free来进行动态内存管理:
C语言动态内存管理详解
但是在C++中,使用这些函数来进行动态内存管理时,有一些需求是做不到的(比如申请空间时对对象初始化),而且使用时比较麻烦。所以C++提出了动态申请空间的方式new
与delete
:
不同于malloc与free,new与delete是操作符而非函数。
在使用new与delete申请空间时:
对于单个元素,用 new 类型;
即可申请一块指定类型的空间,表达式的值就是这块空间的指针。可以在类型的后面用括号初始化这个类型的值 new 类型(初始值);
;
对应的,需要用 delete 指针;
的形式来释放这块单个元素的空间。
对于连续的元素,用 new 类型[num];
即可申请一块num个类型大小的连续空间,表达式的值就是这块空间的地址。可以在[num] 的后面用{}来初始化这块空间:new 类型[num]{};
;
对应的,需要用 delete[] 指针;
的形式来释放这块连续元素的空间。
按照上面所介绍的规则,对于内置类型的动态申请空间有如下代码:
int main()
{
int* p1 = new int;
//动态开辟一块int的空间,不初始化
int* p2 = new int(10);
//动态开辟一块int的空间,初始化为10
int* p3 = new int[10];
//动态开辟一块10个int的空间,不初始化
int* p4 = new int[10]{ 1,2,3,4,5,6,7,8,9,0 };
//动态开辟一块i10个int的空间,初始化为1,2,3,4,5,6,7,8,9,0
delete p1;
delete p2;
//释放单个元素空间用delete
delete[] p3;
delete[] p4;
//释放连续空间需使用delete[]
return 0;
}
我们可以通过调试来查看各部分申请的空间中的值:
需要注意的是,new
与delete
、new []
与delete[]
不能混用,在动态申请内置类型时虽然不会出现什么问题,但是对于自定义类型就会程序崩溃
对于内置类型,使用malloc与free还可以勉强达到我们的需求,但是对于自定义类型,比如类类型,我们在申请空间的时候,还需要初始化类对象,malloc显然不能满足我们的需求。
而new在动态申请空间之后,还会调用默认构造函数;delete在调用析构函数之后才会释放动态申请的空间。
这里需要特别注意类对象的空间与类对象中资源空间的区别:
class A
{
public:
A(int a = 0)
:_a(a)
{
_date = new char[_a + 1] {0};
cout << "A(int a = 0)" << endl; //构造函数中打印
}
~A()
{
delete[] _date;
_a = 0;
cout << "~A()" << endl; //析构函数中打印
}
private:
int _a;
char* _date;
};
例如这个A类,有两个成员变量int
与char*
,占8字节,这8个字节就是类对象的空间,这个空间可以在栈区,也可以动态申请在堆区,这取决于使用者;
但是其中char*
指向一块连续的空间,这块空间的大小是不确定的,它是在类对象实例化的时候,在构造函数中动态申请的,这是使用类的人无法改变的。这块动态开辟的空间就是类的资源,它只能在构造函数中被申请,在析构函数中被释放。这也就是为什么malloc满足不了我们对与自定义类型动态申请空间的原因。
对于new自定义类型,用法上与内置类型一致:
在使用new申请单个对象时,调用一次构造函数,申请num个连续的对象时,就调用num次构造函数;delete同理:
int main()
{
A* pa1 = new A;
//动态开辟一个对象,调用默认构造初始化
A* pa2 = new A(5);
//动态开辟一个对象,传参给构造函数初始化
A* pa3 = new A[5];
//动态开辟一块连续的类对象空间,全部调用默认构造初始化
A* pa4 = new A[5]{ 1,2,3,4,5 };
//动态开辟一块连续的类对象空间,分别传参初始化
delete pa1;
delete pa2;
//释放单个元素空间用delete
delete[] pa3;
delete[] pa4;
//释放连续空间需使用delete[] !!!
return 0;
}
我们可以通过调试来查看动态申请的情况:
一共调用了1+1+5+5 =12次构造函数与12次析构函数:
如果申请的是内置类型的空间,new和malloc,delete和free基本类似。但是new
和delete
申请和释放的是单个元素的空间,new []
和delete[]
申请的是连续空间,而且new
在申请空间失败时会抛异常,malloc
会返回NULL
需要注意的是,在动态申请或释放空间时,对于申请或释放失败的情况,C++更倾向于抛异常来反映失败的原因。这个抛异常的行为是在operator new
与operator delete
两个函数中来实现的。
我们也可以通过反汇编来查看,在new一块空间时,确实调用了operator new函数,在delete时确实调用了operator delete函数:
当然,申请与释放连续的空间时,调用的是operator new[]
与operator delete[]
函数。而这两个函数中还是调用多个operator new
与operator delete
来实现的。
我们可以来简单了解一下operator new
与operator delete
函数:
new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。
而operator new
实际也是通过malloc
来申请空间,如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete
最终是通过free
来释放空间的。
new的原理
调用operator new
函数申请空间,然后在申请的空间上执行构造函数,完成对象的构造。
delete 的原理
在空间上执行析构函数,完成对象中资源的清理工作,然后调用operator delete
函数释放对象的空间
new T[num] 的原理
调用operator new[]
函数,在operator new[]中实际调用operator new
函数完成num个对象空间的申请,在申请的空间上执行num次构造函数
delete[] 的原理
在释放的对象空间上执行num次析构函数,完成num个对象中资源的清理,然后调用operator delete[]
释放空间,实际在operator delete[]中调用operator delete
来释放空间
定位new可以用一块已经申请的动态空间调用构造函数来初始化类对象
定位new通常配合内存池来使用(内存池就是事先申请好的一块空间,事先申请一部分空间就可以避免因经常扩容而经常申请空间带来的效率的降低),内存池的空间只是申请,并没有初始化,我们就可以通过定位new来实现初始化:
使用定位new的格式为:new (要初始化的空间的指针) 类型;
,需要注意的是,如果要初始化的类类型没有默认构造函数,就必须传参 new (要初始化的空间的指针) 类型(构造函数参数列表);
;
在释放定位new初始化的类对象的资源时,就需要显式的调用该类对象的析构函数来释放了:
class A
{
public:
A(int a = 0)
:_a(a)
{
_date = new char[_a + 1] {0};
cout << "A(int a = 0)" << endl; //构造函数中打印
}
~A()
{
delete[] _date;
_a = 0;
cout << "~A()" << endl; //析构函数中打印
}
private:
int _a;
char* _date;
};
int main()
{
A* ptr = (A*)malloc(1000);
//创建1000个字节的空间
new(ptr)A(5);
//用上面申请的空间初始化一个A对象
ptr->~A();
//释放上面定位new初始化的对象的资源
free(ptr);
//释放malloc出的空间
return 0;
}
需要注意的点还是类的空间与类的资源的空间的区别:这里malloc只是申请一块空间,下面的new是初始化malloc中的空间,这个初始化的行为包括申请类的资源,这个资源是不属于malloc出的空间的;
delete释放的是类的资源,而free掉的是malloc出的空间。
malloc和free是函数;new和delete是操作符
malloc申请的空间不会初始化;new可以初始化
malloc申请空间时,需要手动计算空间大小并传递;new只需在其后跟上空间的类型即可,如果是多个对象,[num]中指定对象个数即可
malloc的返回值为void*
, 在使用时必须强转;new不需要,因为new后会指定空间的类型
malloc申请空间失败时,返回的是NULL,因此使用时必须判空;new不需要,但是new需要捕获异常
申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数;而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理
到此,关于C/C++内存管理的内容就介绍完了
相信大家不仅学会了C++中new与delete的使用,更对内存管理加深了理解
如果大家认为我对某一部分没有介绍清楚或者某一部分出了问题,欢迎大家在评论区提出
如果本文对你有帮助,希望一键三连哦
希望与大家共同进步哦