重载new和delete

重载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函数库的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 <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还有两个不常见的用途:

  1. 在内存的指定位置放置一个对象。
  2. 让使用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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值