一篇文章了解c++中的new和delete
作者:木千
new expression
new一个类型,会创建一个该类型的内存,然后调用构造函数,最后返回该内存的指针
注意:该操作是原子性的。
在vc6中的实现如下
void *operator new(size_t size, const std::nothrow_t &) _THROW0() { void *p while((p = malloc(size)) == 0) { // 如果调用malloc失败后会调用_callnewh // _callnewh含义是call new handler,简单说就是用户设定一个回调函数 // 使用_set_new_handler来设置,通常是用户自己控制释放一些不用的内存 _TRY_BEGIT if(_callnewh(size) == 0) break; _CATCH(std::bad_alloc) return (0); _CATCH_END } return (p); }
delete expression
delete 一个指针,先调用析构函数,然后释放内存
在vc6中的实现如下
void *operator delete(void *p) _THROW0() { free(p); }
new[]和new()
new[]是分配指针数组,new()是分配时直接初始化,这两个很容易搞混,关键是编译都能过,一定要注意。比如:
int *p = new[3]; // 是分配三个int*指针所组成的指针数组 int *q = new(3); // 是分配一个int堆内存,并初始化为3
new[]和delete[]
Complex *pca = new Complex[3];
调用三次Complex的构造函数,分配三个Complex对象
delete[] pca;
释放内存。
如果这里的delete[]只写写成delete会怎么样?好多人一定会说:会内存泄露。
其实正确的答案是不确定,具体需要看Complex类的内部有没有堆内存
new[]后内存是怎么样的呢?看下图
关键是看图中的cookie部分,存放了一些内存相关的数据,其中最关键的是在cookie中存放了分配内存的大小
再来看一下下面的代码
string *psa = new string[3]; delete psa;
执行完该代码后内存分配如下
由于string类的内部使用动态堆内存来保存字符串,new[]分配的内存的cookie只记录了string类的信息,而类内部的动态堆内存信息由每个实例自行管理,不在new[]的cookie中。
前面说过,delete释放内存的过程是先调用析构函数,再释放内存。在本例中,如果使用delete[]来释放内存,会依次调用每个实例的析构函数,每个析构函数会自行释放自己内部的堆内存,然后在释放new的内存块。但是如果使用delete来释放内存,只会是第一个实例调用一次析构函数,另外两个实例不会调用,然后根据cookie中记录的内存大小释放有new分配的内存,另两个实例中的堆内存就泄露了。
也就是说,对于上图string的例子,如果使用delete直接释放内存,泄露的是str2和str3箭头右边的白色区域所示的内存,而pas箭头右边的绿色区域是能够正确释放的(具体是调用的str1还是str3取决于编译器的具体实现,理解意思即可)。
但是,这不意味着你可以在类内部没有堆内存的情况下就可以毫无顾忌的使用delete来释放new[],这是编码规范的问题,使用delete不一定有错,但使用delete[]则是一定没错。
new的内存分布
下图是vc6中new的内存布局
我们得到的是图中0x00441c30这一部分的指针,但实际上内存管理的是图中所有的一大块内存,其中橘黄色部分只有在debug模式下才有。由于内存管理需要是16的倍数,如果不够16的倍数,则添加一些数据凑到16的倍数,图中蓝色的pad部分就是添加的无用数据。图中61h部分就是cookie,上下部分分别为上cookie和下cookie。由于本例使用的是int类型举例,而int没有析构不析构的,所以可以直接使用delete就能完整释放整块内存。这里这么写是为了让读者加深理解,实际编码的时候要加上[],这里对比一下下图
这张图使用的类型是一个类,用new[]分配内存的时候,返回的指针和调试信息中间多出来一块内存用来记录实例的个数,就是图中的3。这中情况,如果使用delete[]来释放内存,会正确索引到实例的首地址进行释放操作,如果使用delete来释放内存,索引到的内存是记录实例个数的整型数据位置,如果从这里开始按找该类的内存结构进行析构,肯定是会出问题的,整个内存结构都乱了。
这里有个地方需要注意,这里的delete和delete[]部分看起来和new[]和delete[]小结中介绍的有些矛盾,老师是怎么讲的,由于是看的盗版网课,也没办法请教老师,具体是怎么情况我也不太清楚。猜测是因为不同编译器具体实现时,3的位置不同,有的在前面,有的在后面,关键是看具体实现,在前面的情况就是矛盾的,在后面就没事,关键是领会精神。
placement new
placement new 允许我们将对象构建于一个已经分配的内存当中
没有所谓的placement delete,因为placement根本就没有分配内存,它只是使用了一个已经分配好的内存,所以不需要配套的释放操作,具体用法如下
#include <new> // 分配内存 char *buf = new char[sizeof(Complex) * 3]; // 在分配好的内存上构造Complex Complex *pc = new(buf)Complex(1, 2); // 注意这里要释放的指针 // 感觉如果直接释放pc应该也没错 // 手上没环境不能测试,以后有时间测一下 delete[] buf;
new失败处理
在纯C中使用malloc来分配内存,需要判断一下返回的指针,如果返回一个空指针,则代表内存分配失败。
到了c艹中,使用new来分配内存,则无法通过判断空指针的方法判断是否失败。因为在c艹中,如果new失败会抛出异常,代码是走不到判断空指针的语句的。new失败正确处理方法有以下几种
捕捉异常
try { int* p = new int[SIZE]; // 其它代码 } catch ( const bad_alloc& e ) { return -1; }
据说古老的c++编译器new失败不会抛异常,而是和malloc一样返回空指针,因为那时候c++还没有异常机制,坊间流传,也懒得考证,了解以下即可。顺便吐槽一下,说c艹的异常是屎,这是对屎的侮辱,屎还能当肥料种地呢,c艹的异常除了捣乱没任何鸟用。
禁用new的异常
int* p = new (std::nothrow) int; // 这样如果 new 失败了,就不会抛出异常,而是返回空指针
new-handler
文章开始介绍new源码的时候提到过,new实现的时候会调用new-handler的回调函数,在new之前设置好回调函数即可。由于此方法太过麻烦,懒得研究,具体用法读者自行查找相关资料。
重载
重载的时候,一般不重载全局的::operator new,因为全局的影响太大,一般只重载类自身的Foo::operator new。
重载一般在内存池中用的比较多,可以减少cookie
重载全局的::operator new
void *myAlloc(size_t size) { return malloc(size); } void myFree(void *p) { free(p); } // 下面代码实现部分不重要,关键看接口的重载 // 它们不可以被声明在一个namespace内 inline void *operator new(size_t size) { cout << "global new()\n"; return myAlloc(size); } inline void *operator new[](size_t size) { cout << "global new[]()\n"; return myAlloc(size); } inline void operator delete(void *p) { cout << "global delete()\n"; return myFree(p); } inline void operator delete[](void *p) { cout << "global delete[]()\n"; return myFree(p); }
重载局部的Foo::operator new
class Foo { public: void *operator new(size_t); void operator delete(void*); };
需要注意的是,重载局部的new和delete必须是static的,因为new调用时是内存对象创建过程当中,此时还没有一个完整的内存对象,无法通过对象来调用一般的函数。由于必须是static的,不管写不写static,编译器都会当成是static处理。
数组版本也是一样的,只是都加了一个[],这里就不再写一次了
重载placement new
placement new的括号中不一定非要放指针,我们可以自己来定义放任意的东西。放指针的版本是标准库中先写好给我们用的,我们也可以通过重载placement new来自定义所放的数据,比如Foo *pf = new(300, 'c')Foo;。可以重载为多种参数形式,但多个重载的参数列形式不能重复,必须满足普通函数重载的条件。其中第一个参数必须是size_t,用来传递类的大小,该参数类似于成员函数的this指针,在调用时自动传递,不需要显示传递。比如在Foo *pf = new(300, 'c')Foo;中,其声明形式为void *operator new(size_t, int, char);。如果内存不是外部申请好的,需要在placement new函数内部去申请内存。
重载new的时候应该对应重载一个相同形式的delete。但重载placement delete时需要注意,只有在placement new中产生异常,才会调用其对应的placement delete函数。c++这么设计的原因是,在调用placement new函数后,如果内存是由在placement new内申请的,在调用构造函数时如果发生了异常,可以在对应的在placement delete函数中将在placement new中申请的内存释放掉。
如果没有对应形式的delete,编译器也不会报错,编译器会认为你放弃处理该形式的new中产生的异常(个别编译器会给个警告)
class Foo { public: // 重载一个一般形式的operator new void *operator new(size_t); // 标准库中placement new的重载形式 void *operator new(size_t, void *); // Foo *pf = new(300, 'c')Foo;调用形式的重载方式 void *operator new(size_t, int, char); // 随便写的一种重载形式 void *operator new(size_t, size_t, char *, int); // 以下是对应的delete void *operator delete(void *, size_t); void *operator delete(void *, void *); void *operator delete(void *, int, char); void *operator delete(void *, size_t, char *, int); };
std::string中就是一个很好的placement new重载,有兴趣的朋友可以去看string的源码
c++ new与malloc的10点差别表格:
特征 | new/delete | malloc/free |
---|---|---|
分配内存的位置 | 自由存储区 | 堆 |
内存分配失败返回值 | 完整类型指针 | void* |
内存分配失败返回值 | 默认抛出异常 | 返回NULL |
分配内存的大小 | 由编译器根据类型计算得出 | 必须显式指定字节数 |
处理数组 | 有处理数组的new版本new[] | 需要用户计算数组的大小后进行内存分配 |
已分配内存的扩充 | 无法直观地处理 | 使用realloc简单完成 |
是否相互调用 | 可以,看具体的operator new/delete实现 | 不可调用new |
分配内存时内存不足 | 客户能够指定处理函数或重新制定分配器 | 无法通过用户代码进行处理 |
函数重载 | 允许 | 不允许 |
构造函数与析构函数 | 调用 | 不调用 |
malloc给你的就好像一块原始的土地,你要种什么需要自己在土地上来播种
而new帮你划好了田地的分块(数组),帮你播了种(构造函数),还提供其他的设施给你使用:
当然,malloc并不是说比不上new,它们各自有适用的地方。在C++这种偏重OOP的语言,使用new/delete自然是更合适的。
总结
到此这篇关于c++中new和delete的文章就介绍到这了,更多相关c++中new和delete内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!