重载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 <cstdlib>
#include <cstdio>
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函数库的malloc
和free
(可能默认的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 <cstddef> //Size_t
#include <fstream>
#include <iostream>
#include <new>
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 <new> //size_t definitoin
#include <fstream>
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 " <<endl;
Widget* w = new Widget;
trace << "\ndelete Widget" << endl;
delete w;
trace << "\nnew Widget[25]" << endl;
widget* wa = new Widget[25];
trace << "\ndelete []Widget" << endl;
delete []wa;
}
输出信息:
new Widget
Widget::new: 40 bytes
*
delete Widget
~Widget::delete
new 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还有两个不常见的用途:
- 在内存的指定位置放置一个对象。
- 让使用new的程序员可以选择不同的内存分配方案
这两个特性可以用相同的机制实现: 重载的operator new()可以带一个或多个参数。第一个参数总是对象的长度,它在编译器内部计算出来并传递给new,但其他参数可以由我们定义:一个放置对象的地址、一个是对内存分配函数或对象的引用,或其他设置。
在调用过程中传递额外参数给operator new的方法看起来有点古怪:在关键字new后是参数表(没有size_t参数,它由编译器处理),参数后面是正在创建的对象的类名字。例如:
X* xp = new(a) X;
将a
作为第二个参数传递个operator new。注意,这是在operator new已经声明的情况下才有效。
//PlacementOperatorNew.cpp
#include <cstddef>
#include <iostream>
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