第八章、定制new和delete

条款49:了解new-handler的行为

当operator new 抛出异常以反映一个未获满足的内存需求之前,它会先调用一个客户指定的错误函数,一个所谓的new-handler。
代码示例如下:

namespace std {
    typedef void (*new_handler) ();
    new_handler set_new_handler(new_handler p) throw();
}

set_new_handler 的参数是个指针,指向operator new无法分配足够内存时该被调用的函数。其返回值也是个指针,指向set_new_handler 被调用前正在执行(但马上就要被替换)的那个new-handler函数。
你可以这样使用set_new_handler:

//以下是当operator new 无法分配足够内存时,该被调用的函数
void outOfMem(){
    std::cerr<<"unable to satisfy request for memory \n";
    std::abort();
}
int main()
{
    std::set_new_handler(outOfMem);
    int * pBigDataArray = new int[100000000L]l
    //...        
    
    
}

一个设计良好的new-handler函数必须做以下事情:
1、让更多内存可被使用。
2、安装另一个new-handler。
3、卸除new-handler。
4、抛出bad_alloc(或派生自bad_alloc)的异常。
5、不返回,通常调用abort或exit 。

有时候你或许希望以不同的方式处理内存分配失败情况,你希望视被分配物属于哪个class而定:

class X
{
public:
    static void outOfMemory();
    //...
};
class Y
{
public:
    static void outOfMemory();
    //...
};


    X * p1 =new X;//如果分配不成功,调用X::outOfMemory
    Y * p2 =new Y;//如果分配不成功,调用Y::outOfMemory

C++ 并不支持class 专属之new-handlers ,但其实也不需要。

现在,假设你打算处理Widget class 的内存分配失败情况。首先你必须登录“ 当operator new 无法为一个Widget 对象分配足够内存时” 调用的函数,所以你需要声明一个类型为new_handler 的static 成员,用以指向class Widget 的new-handler 。看起来像这样:

class Widget
{
public:
    static std::new_handler set_new_handler(std::new_handler p) throw();
    static void * operator new(std::size_t size) throw(std::bad_alloc);
    
private:
    static std::new_handler currentHandler;    
};

static成员必须在class定义式之外被定义(除非它们是const 而且是整数型,见条款2),所以需要这么写:

  std::new_handler Widget::currentHandler= 0;//在class 实现文件内初始化为null   

Widget内的set_new_handler 函数会将它获得的指针存储起来,然后返回先前(在此调用之前)存储的指针,这也正是标准版 set_new_handler 的行为:

std::new_handler Widget ::set_new_handler(std::new_handler p) throw()
{
    std::new_handler oldHandler = currentHandler;
    currentHandler = p;
    return  oldHandler;
}

最后,Widget的operator new 做以下事情:
1、调用标准set_new_handler ,告知Widget 的错误处理函数。
2、调用global operator new ,执行实际之内存分配。
3、如果global operator new 能够分配足够一个Widget 对象所用的内存,Widget 的operator new 会返回一个指针,指向分配所得。

下面代码阐述一遍。我将从资源处理类开始,那里面只有基础性RAII操作,在构造过程中获得一笔资源,并在析构过程中释还:


class NewHandlerHolder
{
public:
    explicit NewHandlerHolder(std::new_handler nh):handler(nh){}//取得目前的new-handler
    ~NewHandlerHolder()
    {
        std::set_new_handler(handler);//释放它
    }
        
private:
    std::new_handler handler;//记录下来
    NewHandlerHolder(const NewHandlerHolder&);//阻止copying
    NewHandlerHolder& operator=(const NewHandlerHolder&);
        
};

这就使得Widget 的operator new的实现相当简单:

void *Widget::operator new(std::size_t size) throw(std::bad_alloc)
{
    NewHandlerHolder h(std::set_new_handler(currentHandler));//安装Widget的new-handler.
    return  ::operator new(size);//分配内存或抛出异常 恢复global new-handler
}

Widget的客户应该类似这样适用其new-handler:

    void outOfMem();//函数声明,此函数在Widget对象分配失败时被调用
    Widget::set_new_handler(outOfMem);//设定outOfMem为Widget的new-handler函数
    Widget *pw1 =new Widget;//如果内存分配失败,调用outOfMem
    
    std::string* ps =new std::string;//如果内存分配失败,调用global new-handling函数(如果有的话)
    
    Widget::set_new_handler(0);//设定Widget专属的new-handling 函数为null
    
    Widget* pw2 =new Widget;//如果内存分配失败,立刻抛出异常,(class Widget 并没有专属的new-handling函数)
    

实现这一方案的代码并不因class的不同而不同,因此在它处加以复用是个合理的构想。

下面的代码的类可以被任何有所需要的class使用:

template<typename T>  //"mixin"风格的base class,用以支持class专属的set_new_handler
class NewHandlerSupport
{
public:
    static std::new_handler set_new_handler(std::new_handler p) throw();
    static void * operator new(std::size_t size) throw(std::bad_alloc);
        //... 其它的operator new版本——见条款52
private:
    static std::new_handler currentHandler;
};
template<typename T> 
std::new_handler
NewHandlerSupport<T>::set_new_handler(std::new_handler p) throw()
{
    std::new_handler oldHandler = currentHandler;
    currentHandler = p;
    return oldHandler;
}
template<typename T> 
void *NewHandlerSupport<T>::operator new(std::size_t size)
throw(std::bad_alloc)
{
    NewHandlerSupport h(std::set_new_handler(currentHandler));
    return ::operator new(size);
}
//以下将每一个currentHandler 初始化为null
template<typename T> 
std::new_handler NewHandlerSupport<T>::currentHandler = 0 ;

有了这个class template ,为Widget 添加set_new_handler 支持能力就轻而易举了:只要令Widget 继承自NewHandlerSupport< Widget > 就好,像下面这样。看起来似乎很奇妙:

class Widget:public NewHandlerSupport<Widget>
{
    //...和先前一样,但不必声明
    //set_new_handler 或operator new
};

这就是Widget 为了提供“class 专属之set_new_handler”所需要做的全部动作。

另一个形式的operator new ,负责供应传统的 “分配失败便返回null行为 ”。这个形式被称为“ nothrow ”形式——某种程度上是因为他们在new 的使用场合用了 nothrow对象(定义于头文件< new >):

class Widge 
{
    //...和先前一样,但不必声明
    //set_new_handler 或operator new
};

 
    Widget *pw1 =new Widget;//如果分配失败,抛出bad_alloc
    if(pw1 == 0)
    {
        //...  //这个测试一定失败
    }
    Widget *pw2 =new (std::nothrow)Widget;//如果分配Widget失败,返回0
    if(pw2 == 0)
    {
        //...  //这个测试可能成功
    }
    

Nothrow new 对异常的强制保证性并不高。
使用nothrow new 只能保证operator new 不抛掷异常,不保证像“ new (std::nothrow) Widget ”这样的表达式绝不导致异常. 因此你其实没有运用nothrow new的需要。

请记住:
1、set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用。
2、Nothrow new 是一个颇为局限的工具,因为它只适用于内存分配;后继的构造函数调用还是可能抛出异常。

条款50:了解new 和delete 的合理替换时机

想要替换编译器提供的operator new 或operator delete ,一般有以下三个理由:
1、用来检测运用上的错误。
2、为了强化效能。
3、为了收集使用上的统计数据。

下面的代码是个快递发展得出的初阶段global operator new ,促进并协助检测“overruns ”(写入点在分配区块尾端之后)或“ underruns”(写入点在分配区块起点之前。)

static const int signature  = 0xDEADBEEF;
typedef unsigned char Byte;
//这段代码还有若干小错误,详细如下
void * operator new(std::size_t size) throw(std::bad_alloc)
{
    using namespace std;
    size_t realSize = size +2*sizeof (int);//增加大小,使能够塞入两个signature
    void *pMem = malloc(realSize);// 调用malloc 取得内存
    if(!pMem) throw bad_alloc();
    
    //将 signature写入内存的最前段落和最后段落
    *(static_cast<int*>(pMem)) =signature;
    *(reinterpret_cast<int*>(static_cast<Byte*>(pMem)+realSize-sizeof (int))) = signature;
    
    //返回指针,指向恰位于第一个 signature 之后的内存位置
    return static_cast<Byte*>(pMem) +sizeof (int);
}

这个operator new的缺点主要在于它疏忽了身为这个特殊函数所应该具备的“坚持C++规则”的态度。

齐位的意义:C++要求所有operator new 返回的指针都有适当的对齐(取决于数据类型)。malloc 就是在这样的要求下工作,所以令operator new 返回一个得自malloc 的指针是安全的。然而上述operator new 中我并未返回一个得自malloc 的指针,而是返回一个得自malloc 且偏移一个int 大小的指针。如果客户端调用 operator new 企图获取足够给一个double 所用的内存(或者如果我们写个operator new[] ,元素类型是double ),而我们在一部“int 为4bytes 且double 必须8bytes齐位”的机器上跑,我们可能获得一个未有适当齐位的指针。那可能会造成程序崩溃或执行速度变慢。

了解何时可在“全局性”或“class专属”基础上合理替换缺省的new 和delete。有如下用途:
1、用来检测运用上的错误。
2、为了收集动态分配内存之使用统计信息。
3、为了增加分配和归还的速度。
4、为了降低缺省内存管理器带来的空间额外开销。
5、为了弥补缺省分配器中的最佳齐位。
6、为了将相关对象成簇集中。
7、为了获得非传统的行为。

请记住:
1、有许多理由需要写个自定的new 和 delete ,包括改善效能、对heap运用错误进行调试、收集heap 使用信息。

条款51:编写new 和delete 时需要固守常规

奇怪的C++规定,即使客户要求0 bytes , operator new 也得返回一个合法指针。这种看似诡异的行为其实是为了简化语言其它部分。下面是个non-member operator new 伪码(pseudocode):

void operator new(std::size_t size) throw(std::bad_alloc)
{       //你的operator new 可能接受额外参数
    using namespace std;
    if(size == 0) //处理0 byte申请
    {               //将它视为1-byte申请
        size = 1;
    }
    while (true) {
        //尝试分配 size bytes
        if(分配成功)
            return (一个指针,指向分配得来的内存);
        //分配失败 ;找出目前的new-handling 函数(见下)
        new_handler globalHandler = set_new_handler(0);
        set_new_handler(globalHandler);
        
        if(globalHandler) (*globalHandler)();
        else {
            throw std::bad_alloc();
        }
        
    }
    
}

这里的技俩是把0 bytes申请量视为1 byte 申请量。

下面的代码,base class 的operator new 被调用用以分配derived class对象:

class Base{
public:
    static void* operator new(std::size_t size) throw(std::bad_alloc);
    //...
};
class Derived :public Base//假设Derived 未声明operator new
{
    //。。。
};
Derived* p= new Derived;//这里调用的是Base::operator new
//如果Base class 专属的operator new 并非被设计用来对付上述情况(实际上往往如此),
//处理此情势的最佳做法是将“内存申请量错误”的调用行为改采标准operator new,像这样
void *Base::operator new(std::size_t size) throw(std::bad_alloc)
{
    if(size ! = sizeof (Base))//如果大小错误
        return ::operator new(size);//令标准的operator new 起而处理
    //。。。       //否则在这里处理。
}

C++裁定所有非附属(独立式)对象必须有非零大小(见条款39).因此sizeof(Base) 无论如何不能为零,所以如果size 是0,这份申请会被转交到 ::operator new 手上,后者有责任以某种合理方式对待这份申请。
【这里所谓非附属/独立式对象,指的是不以“某对象之base class成分”存在的对象】

如果你决定写个operator new [ ] ,唯一要做的事情的一件事就是分配一块未加工内存,因为你无法对array 之内迄今尚未存在的元素对象做任何事情。
动态分配的arrays 可能包含额外空间用来存放元素个数。
operator new 的要求是保证“删除null 指针永远安全”,下面是non-member operator delete 的伪码:

void operator delete (void *rawMemory) throw()
{
    if(rawMemory == 0)//如果被删除的是个Null指针
        return;// 那就什么都不做
    //现在归还rawMemory所指的内存
}

多加一个动作检测删除数量。万一你的class 专属的operator new 将大小有误的分配行为转交 :: operator new执行,你必须将大小有误的删除行为转交 :: operator delete :

class Base{
public:
    static void* operator new(std::size_t size) throw(std::bad_alloc);
    //...
    static void operator delete(void *rawMemory,std::size_t size) throw(std::bad_alloc);
};

void Base::operator delete(void *rawMemory,std::size_t size) throw(std::bad_alloc);
{
    if(rawMemory == 0)//如果被删除的是个Null指针
        return;// 那就什么都不做
    if(size != sizeof(Base))//如果大小错误,令标准版operator delete 处理此一申请
    {
        ::operator delete(rawMemory);
        return;
    }
    //现在,归还rawMemory所指的内存
       return;
}

请记住:
1、operator new 应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用new-handler .。它应该有能力处理0 bytes申请。class 专属版本则还应该处理“比正确大小更大的(错误申请)”。
2、operator delete 应该在收到null 指针时不做任何事情,class专属版本还应该处理“比正确大小更大的(错误申请)”。

条款52:写了placement new也要写placement delete

当你写一个new 表达式像这样:

Widget* pw = new Widget;

有两个函数被调用:一个是用以分配内存的operator new , 一个是Widget 的 default 构造函数。

如果目前面对的是拥有正常签名式的new和delete ,这并不是问题,因为正常的operator new:

void * operator new(std::size_t) throw(std::bad_alloc);

对于正常的operator delete:

void operator delete(void *rawMemory) throw();//global 作用域中的正常签名式
void operator delete(void *rawMemory,std::size_t size) throw();//class 作用域中典型的签名式

假设你写了一个class专属的operator new ,要求接受一个ostream , 用来志记(logged)相关分配信息,同时又写了一个正常形式的class 专属operator delete :

class Widget
{

public:
    static void * operator new(std::size_t size,std::ostream & logStream) throw(std::bad_alloc);
    //非正常形式的new
    
    static void operator delete(void *rawMemory,std::size_t size) throw();//正常的class 专属delete
    //...
    
};

下面是一个placement版本“ 接受一个指针指向对象该被构造之处”,具体代码如下:

void * operator new(std::size_t,void *pMemory) throw();//placement new

这个版本的new属于C++ 标准库。

下面代码是,在动态创建一个Widget时将相关的分配信息志记(logs)于cerr:

    Widget* pw = new (std::cerr)Widget;//调用operator new并传递cerr为其ostream 实参,这个动作会在Widget
    //构造函数抛出异常时泄漏内存

如果内存分配成功,而Wdget构造函数抛出异常,运行期系统有责任取消operator new的分配并恢复旧观。

由于上面的operator new接受类型为ostream& 的额外实参,所以
下面是一个对应的operator delete:

void operator delete(void * ,std::ostream&) throw();

如果一个带额外参数的operator new 没有“带相同额外参数” 的对应版 operator delete ,那么当new 的内存分配动作需要取消并恢复旧观时就没有任何operator delete 会被调用。为了消除稍早代码中的内存泄漏,Widget 有必要声明一个 placement delete ,对应于那个有志记功能的placement new:

class Widget
{

public:
    static void * operator new(std::size_t size,std::ostream & logStream) throw(std::bad_alloc);

    
    static void operator delete(void *pMemory) throw();
    static void operator delete(void *pMemory,std::ostream & logStream) throw();
    //...
    
};

这样改变之后,如果以下语句引发Widget构造函数抛出异常:

Widget* pw = new (std::cerr)Widget;//一如既往,但这次不再泄漏

对应的placement delete会被自动调用,让Widget有机会确保不泄漏内存任何内存。
然而如果没有抛出异常,而客户代码中有个对应的delete ,会发生什么事情。

delete pw;//调用正常的operator delete

假设你有一个base class ,其中声明唯一一个placement operator new,客户端会发现他们无法使用正常形式的new :

class Base
{
public:
    static void* operator new(std::size_t size,std::ostream & logStream) throw(std::bad_alloc);
    //这个new会遮掩正常的global形式
    
   //。。。   
}


    Base *pb = new Base;//错误!因为正常形式的operator new 被遮掩
    Base *pb = new (std::cerr)Base;//正确,调用Base 的placement new

同样道理,derived classes中的operator new会掩盖global 版本和继承而得的operator new版本:

class Derived:public Base
{
public:

    static void* operator new(std::size_t size) throw(std::bad_alloc);
    //重新声明正常形式的new 
    //...
    
};

    Derived* pd = new (std::clog) Derived;//错误!因为Base的placement new被掩盖了
    Derived* pd = new Derived;//没问题,调用Derived 的operator new

对于撰写内存分配函数,你需要记住的是,缺省情况下C++在global 作用域内提供以下形式的operator new:

void* operator new(std::size_t ) throw(std::bad_alloc);//normal new
void* operator new(std::size_t ,void *) throw();//placement new
void* operator new(std::size_t ,const std::nothrow_t&) throw();//nothrow new 见条款49

如果你在class 内声明任何operator new , 它会遮掩上述这些标准形式。

建立一个base class ,内含所有正常形式的new 和delete :

class StandardNewDeleteForms
{
public:
    // normal new/delete
    static void * operator new(std::size_t size) throw(std::bad_alloc)
    {
        return ::operator new(size);
    }
    static void operator delete(void * pMemory) throw()
    {
        ::operator delete(pMemory);
    }
    
    //placement new/delete
    static void * operator new(std::size_t size,void *ptr) throw()
    {
        return ::operator new(size,ptr);
    }
    static void operator delete(void *pMemory,void *ptr) throw()
    {
        return ::operator delete(pMemory,ptr);
    }
    //nothrow new/delete
    static void * operator new(std::size_t size,const std::nothrow_t& nt) throw()
    {
        return ::operator new(size,nt);
    }
    static void operator delete(void *pMemory,const std::nothrow_t& ) throw()
    {
        return ::operator delete(pMemory);
    }
    
       
    
}

凡是想以自定形式扩充标准形式的客户,可利用继承机制及using 声明式取得标准形式:

class Widget:public StandardNewDeleteForms //继承标准形式
{

public:
    using StandardNewDeleteForms::operator new;//让这些形式可见
    using StandardNewDeleteForms::operator delete;//让这些形式可见
    
    static void * operator new(std::size_t size, //添加一个自定的placement new
                               std::ostream & logStream) throw(std::bad_alloc);



    static void operator delete(void *pMemory,//添加一个对应的placement delete
                                std::ostream & logStream) throw();
    //...

};

请记住:
1、当你写一个placement operator new ,请确定也写出了对应的placement operator delete 。如果没有这样做,你的程序可能会发生隐微而时断时续的内存泄漏。
2、当你声明placement new和placement delete ,请确定不要无意识(非故意)地遮掩了它们的正常版本。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值