C++new/delete与malloc/free的区别
一.new与delete
1. new与delete函数原型:
void *operator new(size_t);
void *operator delete(void *)
2. new与delete的运行机制
class A{
public:
A(int v) : var(v){
fopen_s(&file, "test", "r");
}
~A(){
fclose(file);
}
private:
int var;
FILE *file;
};
- 当我们用new创建一个A类型的指针数组,new返回的是一个指向内存空间的A类型指针
class * pA = new A(10);
- 背后实则发生了这些事:
- new的工作分为以下几点:
(1)由于A类大小为8Bytes(参考《深入C++对象模型》),那么new函数会开辟一个8Bytes的地址空间,但内存空间没有初始化和类型化。
(2)对内存空间进行类对象的初始化,调用构造函数,给private基本变量赋值,指针变量指向对应位置。
(3)返回对象指针。 - 当我们用delete释放掉类对象的时候
delete pA
,背后完成的工作:
- delete的工作分为以下几点:
(1)调用指针所指向对象的析构函数,对打开的文件进行关闭
(2)通过调用delete库函数来释放掉该对象的内存空间,传入的参数是指针pA,也就是对象的地址。
3.用new typep[]和delete []申请和释放数组
(1)申请和释放基本数据类型的数组空间
string *psa = new string[10];//array of 10 empty strings
int *pia = new int[10];//array of 10 uninitialized ints
delete []psa;
delete []pia;
- 在上面的例子中,释放string类型数组空间时实际上先为10个string对象分别调用析构函数,再释放掉为10个string对象所分配的所有内存空间;而当释放int类型数组空间时,因int为内置类型不存在析构函数所以直接释放掉了为10个int类型变量分配的所有空间。
- 这就造成了一个问题,非内置类型数据用new type[]来动态分配内存时,必须保存数据的维度,以确定在析构时需要调用对象析构函数的次数。而C++的解决方案为在分配数组空间时前面多分配了4Bytes大小的空间,专门保存数组的维度,在delete时根据数据维度调用析构函数,最后再释放所有内存空间。
- 用一个例子来说明他们的运行机制,首先为对象数组分配内存空间:
class A * pAa = new A[3];
- 释放对象数组内存空间时:
delete []pAa;
- 调用析构函数的总次数是由调用new库函数时开辟的内存空间的前4Bytes中保存的数据决定的;调用delete []name库函数时,其参数不是指针name的值(也就是第一个数组元素的地址),而是这个地址值减去4Bytes。
4.内存泄漏
int *pia = new int[10];
delete []pia;
- 这样写是没有问题的,但一旦把
delete []pia;
换成delete pia;
,有问题吗?
其实这也是没问题的,但不建议这样做,原因在于上面提到了在new[]时多分配4个字节的缘由,因为析构时需要知道数组的大小,但如果不调用析构函数呢(就例如这里的内置类型:int数组)?我们在new[]时就没必要多分配那四个字节,delete [] 时直接到第二步释放为int数组分配的空间。如果这里使用delete pia; 那么将会调用operator delete 函数,传入的参数是分配给数组的起始地址,所做的事情就是释放掉这块内存空间,不存在问题。 - 但是,上述写法不存在问题有一个大前提是:对象的类型是内置类型或者是无自定义的析构函数的类类型!
- 我们再看看带有自定义析构函数的类类型,用上述写法写时会发生什么?
class A *pAa = new class A[3];
delete pAa;
- delete pAa;做了两件事:
(1)调用一次pAa指向的对象的析构函数;
(2)调用operator delete(pAa);
释放内存。
显然,后面的两个对象均未调用析构函数,如果类对象中申请了大量的内存需要在析构函数中释放,而你在销毁数组对象时少调用了析构函数,造成内存泄漏。 - 如果说上面的问题还不足以引起你的重视,第二点就非常致命了:直接释放pAa指向的内存空间,这个总是会造成严重的段错误(Segmentation fault),程序必然会崩溃。因为分配的空间的起始地址是pAa指向的地址减去4字节的地方,应将传入参数设为那个地址!
- 总的来说,谨记:new/delete, new[]/delete[]配套使用。
二.malloc与free
- 头文件为#include <stdlib.h>,函数声明为:
void* malloc(size_t size);
(1)参数size_t size表示动态内存分配空间的大小,以字节为单位。
(2)malloc()函数的返回值为一个指针,或者说是分配后内存空间的首地址。
(3)如果malloc()申请空间成功则返回一段内存空间的首地址,失败则返回NULL。
(4)返回指针类型需要强制类型转换(存在安全隐患)
int *p = (int *)malloc(sizeof(int));
(1)在使用malloc()函数申请的空间之前,最好用memset()函数把这段内存空间清理一下。malloc只能保证内存空间的大小,无法保证是否有垃圾数据。
(2)同new/delete一样,malloc与free要配对使用,否则会内存泄漏。
#include <stdlib.h>
char *p = (char *)malloc(sizeof(char));//定义一个char* 指针变量p,并分配10字节内存空间(字符数组)
memset(p, 0, 10 * sizeof(char));//将字符数组中的内容全部修改为0
strcpy(p, "hello");//复制一段字符串到p指向的数组空间(长度要比所分配内存小)
p = (char*)realloc(p, 20 * sizeof(char));//用realloc扩充p指向的内存空间
strcat(p, "world");//在原字符串后拼接一段字符
free(p);//释放空间
三.new/delete和malloc/free的区别
1.申请的内存所在位置
new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆为操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配。
那么自由存储区是否能够是堆(等价于new是否在堆上动态分配内存),取决于operator new 的实现细节。自由存储区不仅可以是堆,还可以是静态存储区,这都看operator new在哪里为对象分配内存。
- 特别的,new甚至可以不为对象分配内存!定位new的功能可办到这一点:
new (place_address) type
place_address为一个指针,代表一块内存的地址。当使用上面这种仅以一个地址调用new操作符时,new操作符调用特殊的operator new,也就是下面这个版本:
void * operator new(size_t, void *)//这个版本的operator new不允许重定义
此operator new 不分配任何内存,只是简单的返回指针实参,然后右new表达式负责在place_address指定的地址进行对象的初始化工作。
2.返回类型安全性
- new操作符内存分配成功时,返回的时对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符;而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void* 指针转换成我们需要的类型。
而类型安全在很大程度上可以等价于内存安全,类型安全的代码不会试图访问自己未被授权的内存区域。
3.内存分配失败时的返回值
new内存分配失败时,会抛出bad_alloc异常,不会返回NULL;malloc分配内存失败返回NULL。
使用C语言时,我们习惯在malloc分配内存后判断是否成功:
int *a = (int *)malloc(sizeof(int ));
if (NULL == a){
...
}else{
...
}
走入C++阵营的新手可能把这个习惯带入:
int *a = new int();
if (NULL == a){
...
}else{
...
}
实际这样毫无意义,因为new根本不会返回NULL,而且若程序能够执行到if语句已经说明内存分配成功,失败早就抛异常了,正确的做法要使用异常机制:
try{
int *a = new int();
}catch (bad_alloc)
{
...
}
4.是否需要指定内存大小
使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算,而malloc则需要显式地指出所需内存的尺寸。
class A{...};
A *ptr = new A;
A *ptr = (A *)malloc(sizeof(A));
5.是否调用构造函数/析构函数
使用new操作符分配对象内存经历三个步骤:
- 一:调用operator new 函数(对于数组为operator new[])分配一块足够大的,原始的,未命名的内存空间以便存储特定类型的对象。
- 二:编译器运行相应的构造函数以构造对象,并为其传入初值。
- 三:对象构造完成后,返回一个指向该对象的指针。
使用delete操作符来释放对象内存会经过两个步骤: - 一:调用对象的析构函数。
- 二:编译器调用operator delete(或operator delete[])释放内存空间。
总的来说,new/delete会调用对象的构造函数/析构函数来完成对象的构造/析构。malloc则不会,例如:
class A{
public:
A() : a(1), b(1.11){}
private:
int a;
int double;
};
总结
特征 | new/delete | malloc/free |
---|---|---|
分配内存的位置 | 自由存储区 | 堆 |
内存分配成功返回值 | 完整类型指针 | void* |
内存分配失败返回值 | 默认抛出异常 | 返回NULL |
分配内存的大小 | 由编译器根据类型计算得出 | 必须显式指定字节数 |
处理数组 | 有处理数组的new版本new[] | 需要用户计算数组的大小后进行内存分配 |
已分配内存的扩充 | 无法直观的处理 | 使用realloc完成 |
是否相互调用 | 可以,看具体operator new/delete实现 | 不可调用new |
分配内存时内存不足 | 客户能指定处理函数或重新制定分配器 | 无法通过用户代码处理 |
函数重载 | 允许 | 不允许 |
构造函数与析构函数 | 调用 | 不调用 |
四.placement new/delete
1.什么是placement new/delete?
如果operator new接受的参数除了一定会有的那个size_t之外还有其他参数,这便是个所谓的placement new。例如众多placement new版本中特别有用的一个“接受一个指针指向对象该被构造之处”
void* operator new(std::size_t, void* pMemory) throw();
这个版本的new已经被纳入STL,只需#include <new>
。它的用途之一就是负责vector的未使用空间上创建对象。
- 一般性术语“placement new”意味带任意额外参数的new。
2.为什么要用到placement new/delete?
记得在前面说过,new/delete区别于malloc/free很重要的一点就是他们会额外调用构造/析构函数,完成对象的构造和销毁。
- 那么问题就来了,如果operator new内存分配成功,而为其分配内存的对象的构造函数抛出异常,运行期系统有责任取消operator new的分配并恢复旧观。而在实际中运行期系统可能不会那么智能,它只会寻找参数个数和类型都与operator new相同的某个operator delete。所以一个placement new一定要对应一个与之参数个数类型都相同的placement delete。
- 如果一个带额外参数的operator new没有带相同额外参数的对应版operator delete,那么当new的内存分配动作需要取消并恢复旧观时就没有任何operator delete会被调用,就会出现内存泄漏。
3.避免内存泄漏要集齐全部版本
delete pw;
如果你像上面这么写,调用的是正常形式的operator delete,而非其placement版本 。placement delete只会在伴随placement new调用而触发的构造函数出现异常时才会被调用,对一个指针(pw)绝不会调用,所以我们必须提供一个正常的operator delete(用于构造期间无任何异常抛出)和一个placement delete。
4.使用placement new/delete的一些注意事项
(1)成员函数的名称会掩盖其外围作用域中的相同名称,要避免让自己class专属的news掩盖客户期望的其他news。例如:
class Base{
public:
···
static void* operator new(std::size_t size, std::ostream& logStream)//这个new掩盖正常的global形式
throw(std::bad_alloc);
};
Base* pb = new Base;//error,正常形式operator new被掩盖
Base* pb = new(std::cerr) Base;//正确,调用Base的placement new
同理,derived classes中的operator news会掩盖global版本和继承而得的operator new版本
(2)缺省情况下C++在global作用域提供三种形式的operator new:
void* operator new(std::size_t) throw(std::bad_alloc);//normal
void* operator new(std::size_t, void*) throw();//placement
void* operator new(std::size_t, const std::nothrow_t&) throw();//nothrow
nothrow形式的operator new负责供应传统的C++:分配失败便返回NULL的行为。此形式颇为局限,只适用于内存分配,后继的构造函数调用还是可能抛出异常。
class A{···};
A* p2 = new(std::nothrow) A;//如果分配失败,返回0
if (p2 == 0)···//古老的侦测NULL的写法