博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
重载new和delete
阅读量:2243 次
发布时间:2019-05-09

本文共 6865 字,大约阅读时间需要 22 分钟。

重载new和delete


当我们创建一个new表达式时,会发生两件事,首先,使用operator new()来分配内存,然后调用构造函数。在delete表达式里,调用了析构函数,然后使用operator delete()释放内存。我们无法控制构造函数和析构函数的调用(这是编译器自动做的事),但可以改变内存分配函数operator new()operator delete()

1.为什么要重载

使用了new和delete的内存分配系统是为通用目的而设计的。但在特殊情况下,它不能满足需要(比如假设默认的分配策略适合分配小的内存块,而我们经常需要分配大块内存)。C/C++允许重载new和delete来实现我们自己的内存分配策略。

2.重载时发生了什么

当重载new和delete时,我们只是改变了原有的内存分配策略,记住这点很关键。编译器将用重载后的new代替默认版本取分配内存,然后为那个内存块调用构造函数。所以,当编译器看到new时,编译器分配内存并调用构造函数,但是当重载new时,可以改变的只是内存分配部分(delete也一样)。

当重载operator new()时,也可以替换它用完内存时的行为,所以必须在operator new()里决定做什么:返回0、写一个调用new-handler的循环、再试着分配或者产生bad_alloc的异常信息。

重载new和delete与重载其他运算符一样。但可以选择重载全局内存分配函数或者是特定类的内存分配函数。

3.如何重载

重载全局的new和delete

当全局版本的new和delete不能满足需要时,对其重载是很极端的方法。如果重载了全局版本,那么默认版本将完全无法访问(在这个重新定义里也不能调用它们)。

重载的new必须有一个size_t类型的参数,这个参数由编译器产生并传递给我们,它是要分配内存的对象的长度(使用new时,后面接一个对象,编译器会知道这个对象的大小)。必须返回一个指向等于(或者大于)这个长度的对象的指针,如果没有找到可用内存,则返回0。然而如果找不到可用内存,不能仅仅返回0,也许还应该做一些诸如调用new-handler或者产生一个异常信息之类的事。

operator new()返回值是一个void*,而不是指向某个具体类型的指针。所做的事情只是分配内存,而不是完成一个对象的建立——直到调用了构造函数才算完成了对象的构建,它是编译器确保的动作,不在我们可控范围内。

oeprator delete()的参数是一个指向由operator new()分配的内存的void*。参数是一个void*是因为它是调用析构函数之后得到的指针。析构函数从存储单元中移除对象。operator delete()返回值是void。

煮个栗子:

//GlobalOperatorNew.cpp//Overloat global new/delete#include 
#include
using namespace std;void* operator new(size_t sz){ printf("operator new: %d Bytes\n", sz); void *m = malloc(sz); if(!m) puts("out of memory"); return m;}void operator delete(void *m){ puts("operator delete"); free(m);}class S{ int i[100];public: S() { puts("S:S()"); } ~S() { puts("S:~S()"); }};int main(){ puts("creating & destroying an int"); int *p = new int(47); delete p; puts("creating & destroying an S"); S *s = new S; delete s; puts("creating & destroying S[3]"); S *sa = new S[3]; delete []sa;}

这里可以看到重载new和delete的通常形式,这里的内存分配使用了标准C函数库的mallocfree(可能默认的new和delete也使用这些函数)。注意,这里打印信息使用的是printf()和puts()而不是iostreams。因为,当创建一个iostream对象(像cin,cout和cerr),它们调用new取分配内存。而用printf()不会进入死锁状态,因为它不调用new来初始化。

对于一个类重载new和delete

为一个类重载new和delete时,尽管不必显式的使用static,但实际上仍然是创建static成员函数。它的语法也和重载其他运算符一样。当编译器看到使用new创建自己定义的类的对象时,它选择成员版本的operator new()而不是全局版本的new()。但是全局版本的new和delete任然为其他类型所使用。

下面的例子里为类Framis创建了一个简单的内存分配系统。程序开始时在静态数据区留出一块存储单元。这块内存被用来为Framis类型的对象分配存储空间。

//Framis.cpp//Local overloaded new & delete#include 
//Size_t#include
#include
#include
using namespace std;ofstream out("Framis.out");class Framis{ enum { sz = 10; }; //古老的定义类内静态常量成员的方法 char c[sz]; //为了占据空间,不使用 static unsigned char pool[]; static bool alloc_map[];public: enum { psize = 100; }; Framis() { out << "Framis()\n"; } ~Framis() { out << "~Framis()\n"; } void * operator new(size_t) throw(bad_alloc); void operator delete(void *) ;};unsigned char Framis::pool[psize * sizeof(Framis)];bool Framis::alloc_map[psize] = {
false};void* Framis::operator new(size_t) throw(bad_alloc){ for(int i = 0; i < psize; i++){ if(!alloc_map[i]){ out << "using block " << i << "..."; alloc_map[i] = true; return pool + (i * sizeof(Framis)); } out << "out of memory" << endl; throw bad_alloc();}void Framis::operator delete(void *m){ if(!m) return ; unsigned long block = (unsigned long)m - (unsigned long)pool; block /= sizeof(Framis); out << "freeing block" << block << endl; alloc_map[block] = false;}int main(){ Framis * f[Framis::psize]; try{ for(int i = 0; i < Framis::psisz; i++){ f[i] = new Framis; new Framis; //out of memory } catch(bad_alloc){ cerr << "Out of memory!" << endl; } delete f[10]; f[10] = 0; Framis *x = new Framis; delete x; for(int i = 0; i < Framis::psize; i++) delete f[i]; //Delete f[10] OK}

为一个类数组重载new和delete

如果为一个类重载了operator new和operator delete,那么无论何时创建一个这个类的对象,都会调用重载的new和delete。但如果要创建这个类的一个数组的话,编译器会使用全局的new和delete。所以我们还要重载这个类的new的数组版本。即operator new[]和operator delete[]。

//ArrayOperatorNew.cpp#include 
//size_t definitoin#include
using namespace std;ofstream trace("ArrayOperatorNew.out");class Widget{ enum { sz = 10; }; int i[sz];public: Widget() { trace << "*"; } ~Widget() { trace << "~"; } void* operator new(size_t sz){ trace << "Widget::new: " << sz << " bytes" << endl; return ::new char[sz]; } void operator delete(void *m){ trace << "Widget::delete" << endl; ::delete []p; } void* operator new[](size_t sz){ trace << "Widget::new[]: " << sz << " bytes" << endl; return ::new char[sz]; } void operator delete[](void *p){ trace << "Widget::delete[]: " << endl; ::delete []p; }};int main(){ trace << "new Widget " <

输出信息:

new Widget Widget::new: 40 bytes*delete Widget~Widget::deletenew Widget[25]Widget::new[]: 1004 bytes*************************delete []Widget~~~~~~~~~~~~~~~~~~~~~~~~~Widget::delete[]:

这里重载的new和delete只是一个套用了全局new和delete的壳子,当然,可以在重载的new和delete里使用任意的内存分配方案。

在语法上,除了多一对括号之外,数组版本的new和delete与单个对象版本的是一样的。不管是哪个版本,我们都要决定所要分配的内存的大小(根据编译器给出的形参决定,可以大于)。数组版本的大小指的是整个数组的大小。应该明确,重载new唯一要做的是返回一个足够大的内存块的指针。然后编译器负责初始化它。

使用数组版本的new时,需要的长度比期望的多了4个字节。这额外的4字节是系统用来存放数组信息的,特别是数组对象的数量。当用delete []widget;表达式时,方括号就告诉编译器他是一个数组,所以编译器产生寻找数组大小的代码,然后多次调用析构函数。

4.分配失败怎么办

如果使用了new的内存分配没有成功,构造函数不会调用,所以虽然没有成功的创建对象,但至少没有调用构造函数并传给它一个为0的this指针。在以前的C++版本中,如果内存分配失败,一般是返回0。它将是构造函数不被调用。如果试着在一个标准的编译器中由new返回0值,则会被告知应该产生一个bad_alloc。

5.定位new和delete

重载operator new还有两个不常见的用途:

  1. 在内存的指定位置放置一个对象。
  2. 让使用new的程序员可以选择不同的内存分配方案

这两个特性可以用相同的机制实现: 重载的operator new()可以带一个或多个参数。第一个参数总是对象的长度,它在编译器内部计算出来并传递给new,但其他参数可以由我们定义:一个放置对象的地址、一个是对内存分配函数或对象的引用,或其他设置。

在调用过程中传递额外参数给operator new的方法看起来有点古怪:在关键字new后是参数表(没有size_t参数,它由编译器处理),参数后面是正在创建的对象的类名字。例如:

X* xp = new(a) X;
a作为第二个参数传递个operator new。注意,这是在operator new已经声明的情况下才有效。

//PlacementOperatorNew.cpp#include 
#include
using namespace std;class X{ int i;public: X(int ii = 0) i(ii) { cout << "this = " << this << endl; } ~X(){ cout << "X::~X(): " << this << endl; } void* operator new(size_t,void *loc){ return loc; }};int main(){ int l[10]; cout << "l = " << l << endl; X* xp = new(l) X(47); xp->X::~X(); //显式调用析构函数}

虽然本例只使用了一个附加参数,但若要实现其他目的,使用更多的参数也是可以的。

在销毁对象时会出现两难的局面。因为仅有一个版本的operator delete,所以没有办法说“对这个对象使用我的特殊的内存释放器”,所以可以调用析构函数,但不能使用动态内存释放机制释放内存,因为内存不是在堆上分配的(本例中该数组是在一个局部有效的栈上)。解决方法是非常特殊的语法:显示调用析构函数。

显示调用析构函数可能出现一些问题。由于某些人想要实时地决定对象的生存时间时,他们使用这种方法在范围结束之前销毁对象,而不是调节作用范围或者使用动态对象创建。而如果使用这种方法为在栈上的对象调用析构函数时,将会出现严重的问题,因为析构函数在对象超出作用范围时又会被调用一次。如果为在堆上创建的对象用这种方法调用析构函数,析构函数将被执行,但是内存不会被释放。用这种方法显式调用析构函数,其实只有一个原因,即支持operator new的定位语法。

还有一个定位operator delete,它仅在一个定位operator new表达式的构造函数产生一个异常信息时才被调用(因此该内存在异常处理操作中被自动清除了)。定位operator delete和定位operator new有一个对应的参数表,该定位operator new是指在构造函数产生异常信息之前被调用的那个。//TODO

转载地址:http://yeqbb.baihongyu.com/

你可能感兴趣的文章
比较两个JSON字符串是否完全相等
查看>>
删除JSONArray中的某个元素
查看>>
Linux下Tomcat重新启动
查看>>
使用HttpClient请求另一个项目接口获取内容
查看>>
HttpClient get和HttpClient Post请求的方式获取服务器的返回数据
查看>>
net.sf.json Maven依赖配置
查看>>
Could not initialize class net.sf.json.JsonConfig错误解决
查看>>
Java编程思想重点笔记(Java开发必看)
查看>>
eclipse 创建maven 项目 动态web工程完整示例
查看>>
前端JSP与Spring MVC交互实用例子
查看>>
使用maven一步一步构建spring mvc项目
查看>>
hadoop map reduce 阶段笔记
查看>>
java jackcess 操作 access
查看>>
Git问题Everything up-to-date解决
查看>>
Hadoop HDFS文件操作的Java代码
查看>>
Hadoop学习笔记—3.Hadoop RPC机制的使用
查看>>
Hadoop学习笔记—22.Hadoop2.x环境搭建与配置
查看>>
JTS Geometry关系判断和分析
查看>>
GIS基本概念
查看>>
Java文件操作①——XML文件的读取
查看>>