条款49-52

条款49:了解new_handler的行为

        operator new抛出异常以反映一个未获满足的内存需求之前,它会先调用一个用户指定的错误处理函数,一个所谓的new_handler。为了指定这个函数,客户必须调用set_new_handler

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

        该函数的参数是operator new无法满足内存分配时所需要调用的函数的指针,返回值是set_new_handler被调用前正在执行(但马上将被替换)的那个new_handler函数

        当operator new无法满足内存申请时,会不断调用new_handler函数,直到找到足够内存。因此设计一个良好的new_handler函数,必须做一下事情:

  • 让更多内存可被使用:实现方法是,程序一开始就分配一大块内存,当new_handler第一次被调用时,将申请的内存分配给程序使用
  • 安装另一个new_handler:如果当前new_handler无法取得足够内存,或许它知道另外哪个new_handler有此能力,就使用set_new_handler将当前new_handler替换掉。当下次operator new调用new_handler时,调用的就是新设置的那个new_handler。实现方法是,令new_handler修改会影响new_handler行为的static数据、namespace数据或global数据
  • 卸除new_handler:将null指针传递给set_new_handler,一旦没有安装任何new_handler,operator new会在分配失败时抛出异常
  • 抛出bad_alloc(或者派生自bad_alloc)的异常:这样的异常不会被new捕捉,因此会被传播到内存索求处
  • 不返回:通常调用abort或exit

        如果希望对不同的class内存分配失败以不同的方式处理,可令每一个class提供自己的set_new_handler以指定专属的new_handler,至于operator new则确保在分配class对象时,以class专属的new_handler替换global 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;
};
std::new_handler Widget::currentHandler=0;

        Widget内的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将做以下事情:

  • 调用标准set_new_handler,告知Widget的错误处理函数。这会将new_handler安装为标准的global new_handler
  • 调用global operator new,执行实际的内存分配。如果分配失败,global operator new会调用Widget的new_handler。如果最终无法完成内存分配,会抛出一个bad_alloc异常。此情况下,Widget的operator new必须恢复原本的global new_handler,然后传播该异常
  • 如果global operator new能够分配足够一个Widget对象所用的内存,Widget的operator new会返回一个指针,指向分配所得。Widget析构函数会管理global new_handler,它会自动将Widget's operator new被调用前的那个global new_handler恢复回来

        Widget's operator new的实现:

void* Widget::operator new(std::size_t size) throw(std::bad_alloc)
{
NewHandlerHolder h(std::set_new_handler(currentHandler));        //资源管理类
return ::operator new(size);
}

        在此基础上,我们设计template class,实现不同类的不同处理方式:

template<typename T>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);
...
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)
{
NewHandlerHolder h(std::set_new_handler(currentHandler));
return ::operator new(size);
}
template<typename T>std::new_handler NewHandlerSupport<T>::currentHandler=0;

    有了这个class template,为Widget添加set_new_handler支持能力就轻而易举了:只要令Widget继承自NewHandlerSupport<Widget>就好:

class Widget:public NewHandlerSupport<Widget>{
...
};

        负责供应传统的”分配失败便返回null”行为的形式成为“nothrow”形式:

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

        但nothrow仅仅能保证operator new不抛出异常,但是后续使用Widget构造函数时可能new一些内存,这些new操作符可能会抛出异常。由此可见,使用nothrow只能保证operator new不抛出异常,不保证像“new (std::nothrow) Widget”这样的表达式绝不导致异常

请记住:

        set_new_handler允许客户指定一个函数,在分配内存无法获得满足时被调用

        Nothrow new是一个颇为局限的工具,因为它只适用于内存分配;后续的构造函数调用还是肯能抛出异常


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

        让我们从operator new开始,如果能提供申请的内存就返回一个指针指向分配的内存,如果没有能力,就抛出一个bad_alloc异常。operator new不止一次尝试内存分配,并在每次内存分配失败后调用new_handling函数。只有当指向new_handling函数的指针时null,operator new才会抛出异常

        C++规定即使用户要求0bytes,operator new也得返回一个指针:

void* operator new(std::size_t size)throw(bad_alloc)
{
using namespace std;
if(size==0) size=1;
while(true)
{
尝试分配 size bytes;
if(分配成功)
return (一个指针指向分配内存);
//分配失败;找出目前new_handling函数
new_handler globalHandler=set_new_handler(0);
set_new_handler(globalHadler);
if(globalHandler)(*globalHandler)();
else throw std::bad_alloc();
}
}

        注意代码中将new_handling函数指针设为null而后又恢复原样,那是由于没有办法直观的取出new_handling函数指针,所以必须调用set_new_handler找出它。

        条款49谈到operator new内含一个无限循环,上述伪代码明白表明出这个循环while(true),退出循环的唯一办法是:内存被成功分配或new_handling函数做了一件描述于条款49的事情:让更多内存可用、安装另一个new_handler、卸除new_handler、抛出bad_alloc异常,或是承认失败而直接return。现在可以了解,new_handler为什么必须做其中某些事,不然while循环永不结束。

        注意到为base class而设计的operator new有可能被derived class误用:

class Base{
public:
static void* operator new(std::size_t size)throw(bad_alloc);
...
};
class Derived:public Base
{...};
Derived *p=new Derived;        //这里调用的是base::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处理
...
}

        上述代码中,将size等于0和size与sizeof(Base)的检测融合在一起了,因为它裁定所有非附属(独立式)对象必须有非零大小,因此sizeof(Base)无论如何不会为0,所以如果size=0,这就会被转交到::operator new手上。

        当你打算撰写operator new[]时,你不知道每个对象多大,因为base class的operator new []有可能经由继承被调用,将内存分配给“元素为derived class对象”。以上就是编写operator new时,需要注意的问题。

        对于operator delete的编写较为简单:

class Base{
public:
static void operator delete(void* rawMemory,std::size_t size)throw();
...
};
void Base::operator delete(void *rawMemory,std::size_t size)throw()
{
if(rawMemory==0) return;
if(size!=sizeof(Base))
{
::operator delete(rawMemory);        //大小错误,调用global operator delete
return;
}
//归还rawMemory所指内存
return;
}
请记住:

        operator new内应含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就应该调用new_handler。它也应该有能力处理0 bytes申请。class的专属版本还应该处理“比正确大小更大的(错误)申请”

        operator delete应该在收到null指针时,不作任何事情。class的专属版本还应该处理“比正确大小更大的(错误)申请”


条款52:写了placement new也要写placement delete
Widget *pw=new Widget;

        上述代码中有两个函数被调用:一是内存分配的operator new,一个是Widget的默认构造函数。假设第一个函数调用成功,但第二个函数却抛出异常。那么第一部所分配的内存必须返还并且恢复初始状态,否则会造成内存泄漏。在这个时候用户没有能力返还内存,因为构造失败,pw尚未被赋值,客户手上就没有指针指向该被归还的内存,这一责任就落到了C++运行期系统身上

        如果使用正常形式的operator new,运行期系统可以找到对应的正常形式的operator delete以取消operator new的影响。但是当使用非正常形式的operator new,也就是带附加参数的operator new,究竟哪一个delete伴随这个new的问题就浮现了

void* operator new(std::size_t size)throw(std::bad_alloc);        //正常形式的new
void operator delete(void *rawMemory)throw();                     //global作用域中的正常形式delete
void operator delete(void *rawMemory,std::size_t size)throw();    //class专属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 *pMemory,std::size_t size)throw();               //正常形式的class专属delete
...
};

        如果operator new接受的参数除了一定会有的那个size_t之外还有其他,这边是所谓的placement new。上述operator new便是一个placement new,并且是一个特殊的placement new。它接受一个指针指向对象该被构造之处。一般谈到placement new都是指的这一特殊placement new。

Widget *pw=new (std::cerr)Widget;        //调用operator new,并将cerr作为ostream实参;这会导致在构造函数抛出异常时泄露内存

        当上式构造函数抛出异常时,运行期系统寻找参数个数和类型都与operator new相同的某个operator delete来恢复operator new所造成的影响。上述式应寻找接受ostream&额外实参的delete:

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

        类似于new的placement版本,delete如果接受额外实参便成为placement delete。现在Widget没有声明placement版本的operator delete,所以运行期系统不知道如何恢复,于是什么也不做

        因此写了placement new也要写一个placement delete:

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

        这样改变之后,当构造函数出现异常时,placement delete自动被调用,不会泄露内存。然而,如果没有抛出异常,编写以下代码时会调用正常版本的delete:

delete pw;

        可见,placement delete只有在伴随placement new调用而触发的构造函数出现异常时才会被调用对一个指针试行delete绝不会导致调用placement delete

        这意味着我们必须同时提供一个正常的operator delete(用于构造期间无任何异常抛出)和一个placement版本(用于构造期间有异常抛出)。placement版本的额外参数必须与operator new一样。

        但是这会导致成员函数名称遮盖外围作用域中相同名称(条款33),必须避免class专属new遮盖其他new:

class Base{
public:
static void* operator new(std::size_t size,std::ostream &logStream)throw(std::bad_alloc);        //会遮掩global new
...
};
Base *pb=new Base;                //错误,正常形式的new被遮盖
Base *pb=new(std::cerr)Base;      //正确,调用Base的placement new

        同样,derived class的operator new会遮盖global和base class中的operator new:

class Derived:public Base{
public:
static void* operator new(std::size_t size)throw(std::bad_alloc);
...
};
Derived *pd=new Derived;                //正确,调用derived的operator new
Derived *pd=new (std::cerr)Derived;     //错误,Base的placement new被遮盖了

        条款33更详细的描述了名字遮盖问题,C++在global作用域提供以下形式的operator new

void* operator new(std::size_t)throw(std::bad_alloc);
void* operator new(std::size_t,void*)throw();
void* operator new(std::size_t,const std::nothrow_t&)throw();

        在自定义的class中声明任何的operator new都会遮盖上述标准形式,解决办法是:将标准形式定义在一个base class中,令含自定义operator new的class继承该base class,并使用using声明使标准形式可见:

class StandardNewDeleteForms{
public:
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);}

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

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()
{::operator delete(pMemory);}
};
class Widget:public StandardNewDeleteForms{
public:
using StandardNewDeleteForms::operator new;
using StandardNewDeleteForms::operator delete;
static void* operator new(std::size_t size,std::ostream &logStream)throw(std::bad_alloc);
static void operator delete(void *pMemory,std::ostream &logStream)throw();
...
};

请记住:

        当你写一个placement operator new,请你确保也写出了对应的placement operator delete。如果没有这样做,你的程序可能会发生隐微而时断时续的内存泄漏

        当你声明placement new和placement delete,请不要无意识(非故意)地遮盖了它们的正常版本

/*    至此effective C++就完结了,后面三条是偏向于构架,自身层次不够就不撰写了,又吸收了大牛的著作,还是很开心    */

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值