C++回顾——new和delete

本文介绍了C++中对象的创建过程,包括new运算符如何分配内存并调用构造函数,以及delete如何调用析构函数并释放内存。同时,探讨了new和delete在数组中的应用,耗尽内存时的处理方式,并详细解释了如何重载new和delete以实现自定义内存管理。

一、对象的创建
当创建一个C++对象时,会发生两件事:
1)为对象分配内存;
2)调用构造函数来初始化那个内存(C++强迫这样做是因为未初始化的对象是程序出错的主要原因)

C++把创建一个对象所需要的所有动作都结合在一个称为new的运算符里(分配内存并调用构造函数),默认的new还进行检测以确信在传递地址给构造函数之前内存分配是成功的,所以不必显示地确定调用是否成功。

delete只用于删除由new创建的对象,delete表达式首先调用析构函数,然后释放内存(经常是调用free())。正如new表达式返回一个指向对象的指针一样,delete表达式需要一个对象的地址。如果正在删除的对象的指针是0,将不发生任何事情(故经常在删除指针后立即把指针赋值为0以免对它删除两次,对一个对象删除两次可能会产生某些问题)。如果delete一个void指针,唯一发生的事就是释放内存(因为既没有类型信息也没有办法使得编译器知道要调用哪个析构函数),这将可能成为一个程序错误。如果在程序中发现内存丢失的情况,那么就搜索所有的delete语句并检查被删除指针的类型。如果是void*类型,则可能发现了引起内存丢失的某个因素(C++还有很多其他的引起内存丢失的因素)。

二、用于数组的new和delete
MyType * fp = new MyType[10];
在堆上为10个MyType对象分配了足够的内存并为每一个对象调用了构造函数。fp实际上是一个数组的起始地址,它可以用类似fp[3]的形式来选择数组的元素。
销毁数组时,使用delete []fp;空的方括号告诉编译器产生代码,该代码的任务是将从数组创建时存放在某处的对象数量取回,并为数组的所有对象调用析构函数。
如果使用delete fp;另外9个析构函数没有调用,适当数量的存储单元会被释放,但是,由于它们被分配在一个整块的内存中,所以,整个内存块的大小被分配程序在某处中断了。

三、耗尽内存
当operator new()找不到足够大的连续内存块来安排对象时,一个称为new-handler的特殊函数将会被调用,首先检查指向函数的指针,如果指针非0,那么它指向的函数将被调用。new-handler的默认动作是产生一个异常,如果已经重载了operator new()则new-handle将不会按默认调用,如果仍想调用new-handler,则需要在重载了的operator new()的代码中加上做这些工作的代码。
如果想代替new-handler,我们在程序中至少要用“内存已耗尽”的信息代替new-handler,并异常中断程序。通过包含new.h来替换new-handler,然后以想装入的函数地址为参数调用set_new_handler()函数(函数必须不带参数且其返回值为void)。

四、重载new和delete
使用了new和delete的内存分配系统是为通用目的而设计的,但是有时它不能满足需要(比如要创建和销毁一个特定的类的非常多的对象以至于这个运算变成了速度的瓶颈;分配不同大小的内存可能会产生很多碎片,以至于很快用完内存,通过为特定的类创建自己的内存分配器,可以确保这种情况不会发生),此时就需要重载new和delete。
重载operator new()和operator delete()时,只是改变了原有的内存分配方法,编译器将用重载的new代替默认的版本去分配内存,然后为那个内存调用构造函数。当重载operator new()时,也可以替换它用完内存时的行为,所以必须在operator new()里决定做什么:返回0、写一个调用new-handler的循环、再试着分配或产生一个bad_alloc的异常信息。
1、重载全局new和delete
当全局版本的new和delete不能满足整个系统时,对其重载是很极端的方法。如果重载全局版本,就使默认版本完全不能被访问,甚至在这个重新定义里也不能调用它们。
重载的new必须有一个size_t参数,它由编译器产生并传递给我们,它是要分配内存的对象的长度。必须返回一个指向等于这个长度(或大于这个长度)的对象的指针,如果没有找到存储单元(在这种情况下,构造函数不被调用),则返回一个0,调用new-handler或产生一个异常信息,通知这里存在问题。
operator new()的返回值是一个void*,而不是指向任何特定类型的指针,它仅仅是分配内存,直到构造函数调用才完成对象的创建(这是编译器确保做的动作)。
operator delete()的参数是一个指向由operator new()分配的内存的void*(因为它是在调用析构函数后得到的指针)。析构函数从存储单元里移去对象,operator delete()的返回类型是void。
简单示例如下:

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);
}

这里的内存分配使用了标准C库函数(可能默认的new和delete也使用)。

2、对于一个类重载new和delete
为一个类重载new和delete时,尽管不显示使用static,但实际上仍是创建static成员函数。当编译器看到使用new创建自己定义的类对象时,它选择成员版本的operator new()。
简单示例如下:

class Framis {
    enum { sz = 10 };
    char c[sz]; // to take up space, not used
    static unsigned char pool[];
    static bool alloc_map[];

public:
    enum { psize = 100 }; // framis allowed
    Framis() { printf("Framis()\n"); };
    ~Framis() { printf("~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] )
        {
            printf("using block %d", i);
            alloc_map[i] = true; // mark it used
            return pool + (i * sizeof(Framis));
        }
    }

    printf("out of memory\n");
    throw bad_alloc();
}

void Framis::operator delete(void* m) {
    if ( !m )
    {
        return;
    }

    // assume it was created in the pool
    // calculate which block number it is
    unsigned long block = (unsigned long)m - (unsigned long)pool;
    block /= sizeof(Framis);
    printf("free block %d\n", block);
    alloc_map[block] = false;
}

如果使用继承,该分配方案不能自动继承使用。

3、为数组重载new和delete
如果为一个类重载了operator new()和operator delete(),无论何时创建这个类的一个对象都将调用这些运算符。但是如果要创建这个类的一个对象数组时,全局operator new()就会被立即调用,用来为这个数组分配足够的内存。对此,可以通过为这个类重载运算符的数组版本operator new[]和operator delete[],来控制对象数组的内存分配。
简单示例如下:

class Widget{
    enum { sz = 10 };;
    int i[sz];

public:
    Widget() { printf("Widget()\n"); }
    ~Widget() { printf("~Widget()\n"); }

    void * operator new(size_t sz) {
        printf("Widget::new %d bytes\n", sz);
        return ::new char[sz];
    }

    void operator delete(void * p) {
        printf("Widget::delete\n");
        ::delete []p;
    }

    void * operator new[](size_t sz) {
        printf("Widget::new[] %d bytes\n", sz);
        return ::new char[sz];
    }

    void operator delete[](void * p) {
        printf("Widget::delete[]\n");
        ::delete []p;
    }
};

可以看到,语法上处理多一对括号外,数组版本的new和delete与单个对象版本的是一样的。

重载operator new()还有其他两个不常见的用途:
1)我们也许会想在内存的指定位置上放置一个对象(这对于面向硬件的内嵌系统特别重要,一个对象可能和一个特定的硬件是同义的);
2)我们也许在调用new时,能够选择不同的内存分配方案。
这两个特性可以用相同的机制实现:重载的operator new()可以带一个或多个参数。第一个参数总是对象的长度,它在内部计算出来并由编译器传递给new。但其他的参数可由我们自己定义:一个放置对象的地址、一个是对内存分配函数或对象的引用,或其他任何使我们方便的设置。此时销毁对象时需要显示地调用析构函数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值