什么是new和delete
new
和delete
不是函数,和sizeof
一样都是C++定义的关键字,不同的是sizeof
在编译时就可以确定其返回值,而new
和delete
相对复杂
示例
string *ps = new string("hello world");
如果换做c语言,上面这句话就会变成:
char *ps = (char *)malloc(sizeof(char)*12);
ps = "hello world";
这里就可以看出new
和mallocc
的几点不同:
malloc
申请完空间后不会对内存进行必要的初始化,而new
可以new
操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,即new
是类型安全性的操作符;而malloc内存分配成功后返回的是void*
,需要通过强制类型转换,将通用类型指针void*
转换成所需要的指针new
操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算;而malloc
需要显示地写出所需内存块的大小
总结表
特征 | new/delete | malloc/free |
---|---|---|
分配内存的位置 | 自由存储区 | 堆 |
内存分配成功的返回值 | 完整类型指针 | void* |
内存分配失败的返回值 | 默认抛出异常 | 返回NULL |
分配内存的大小 | 由编译器根据类型计算得出 | 必须显式指定字节数 |
处理数组 | 有处理数组的new版本new[] | 需要用户计算数组的大小后进行内存分配 |
已分配内存的扩充 | 无法直观地处理 | 使用realloc简单完成 |
是否相互调用 | 可以,看具体的operator new/delete实现 | 不可调用new |
分配内存时内存不足 | 客户能够指定处理函数或重新制定分配器 | 无法通过用户代码进行处理 |
函数重载 | 允许 | 不允许 |
构造函数与析构函数 | 调用 | 不调用 |
new和delete的背后机制
通过示例说明:
class A
{
private:
int var;
FILE *file;
public:
A(int v): var(v) {
fopen_s(&file, "test", "r");
}
~A(){
fclose(file);
}
};
类A中有两个私有成员,一个构造函数和析构函数,构造函数根据传递参数初始化var并且打开文件,析构函数关闭文件
我们使用下面代码创建一个类的对象,返回其指针pa
A *pa = new A(10);
如下图所示new完成的工作:
可以将new实例化对象的过程分为三步:
- 分配指定大小的内存块;
- 在内存块上调用构造函数对类对象进行初始化
- 返回内存块的地址(指针)
那么delete会做什么呢?
delete pa;
如下图所示:
即将delete一个对象的过程也可以分两步:
- 先调用析构函数,将打开的文件关闭
- 释放pa所指内存块的空间,即pa变成空指针
申请和释放一个数组
常用的动态分配一个数组方法
string *psa = new string[10];
int *pia = new int[10];
上面在申请数组的时候都用到了new []
表达式,第一个数组是string类型,在分配了保存对象的内存空间(10个string的大小),并调用string类的默认构造函数来依次初始化每个元素,最后返回第一个string的地址作为string数组的地址;第二个数组是int类型的,int是内置类型不存在构造函数,所以new的过程中,不存在初始化,只分配了10个int类型的内存空间。
如果想释放空间,则使用下面语句
delete [] psa;
delete [] pia;
都用到了delete []
表达式,注意这个[]
一般情况下不能漏下。释放string数组的空间时,先对数组内的每个元素都调用析构函数析构对象,再释放掉整个数组的空间;而在释放int数组时,因为不存在析构函数,所以会直接释放整个int数组的空间。
可以看到delete
的[]
中并没有填数组的大小,那么delete关键字怎么知道需要调用析构函数多少次呢?
回到new [size]
,我们new一个对象数组时,还需要保存数组的维度,c++的做法是在分配数组空间时多分配4个字节,专门保存数组的大小,在delete []
时就可以取出这个保存的数,就知道需要调用析构函数多少次了。
依旧以类A为例,
A *pAa = new A[3];
发生过程如下图:
注意到,在申请数组对象的上面确实多分配了4个字节用来保存数组的大小,但是最终返回的地址(指针)是指向第一个数组元素的。
在释放空间时:
delete []pAa;
发生的过程如下图:
要注意的是,先从目标地址的前4个字节中,取出数作为调用析构函数的次数,依次析构数组内的元素;最后在释放内存空间的时候,传递给operator delete[]()
的参数是pAa-4
,即还要释放前面4个字节
new/delete与new[]/delete[]的配对使用
经过上面的分别对new/delete和new[]/delete[]的使用,可以得知这两对之间一般情况下不能拆开随意组合,不然会导致严重的内存泄露/重复释放问题:
string *psa = new string[10];
delete psa;
如果delete没有后面的[]
意味它只会析构一次,那么剩下的9个string对象和上面的4字节数将永远不会被释放,当数组很大时会造成很严重的内存泄露;相反如果是new/delete[]会导致重复释放内存的问题。
一般情况下意味也有特殊情况,如下所示:
int *pia = new int[10];
delete pia;
这个操作又是合理的,因为差别在int和string不同,int是内置类型,不存在构造和析构函数,也就是说new[]
的时候多分配的4个字节,是因为delete的时候需要知道调用析构函数的次数,但是当对象类型都没有析构函数时,也就没有多分配这4个字节的必要。直接delete pia
给operator delete
传递的参数就是pia
的值(数组第一个元素的地址),直接释放所分配的内存块大小即可,无需析构。