您的位置:首页 > 编程语言 > C语言/C++

(Effective C++)第八章 定制new和delete(Customizing new and delete)

2011-10-15 20:33 381 查看

10.1 条款49:了解new-handler行为 (Understand the behavior of the new-handler)

当operator new抛出异常以反映一个未获满足的内存需求之前,它会调用一个客户指定的错误处理函数,一个所谓的new-handler。标准程序库函数<new>的set_new_handler接口:

namespace std {

    typedef void (*new_handler)();

    new_handler set_new_handler(new_handler p) throw();

}

如上new_handler是个typedef,定义出一个指针指向函数,该函数没有参数也不返回任何东西。set_new_handler则是“获得一个new_handler并返回一个new_handler”的函数。Set_new_handler声明式尾端的“throw()”是一份异常明细,表示该函数不抛出任何异常。

Set_new_handler的参数是一个指针,指向operator new无法分配足够内存时该被调用的函数。其返回值也是个指针,指向Set_new_handler被调用前正在指向的那个new_handler函数。

设计一个良好的new_handler函数必须做到以下事情:

1)    让更多的内存可被使用。比如,程序一开始执行就分配一大块内存,而后当new_handler第一次被调用,将它们释放还给程序使用。

2)    安装另一个new_handler。寻找一个可以获得更多内存的new_handler。

3)    卸除new_handler,也就是将null指针传给set_new_handler。一旦不安装任何new_handler,operator new会在内存分配不成功时抛出异常。

4)    抛出bad_alloc(或派生自bad_alloc)的异常。

5)    不返回,通常调用abort或exit。

C++并不支持class专属之new_handlers,你需要设计一个Widget class来处理这种需求。

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定义式之外被定义,在class实现文件内初始化为null

std::new_handler Widget::currentHandler = 0;

static std::new_handler Widget::set_new_handler(std::new_handler p) throw()

{

std::new_handler oldhandler = currentHandler;

currentHandler = p;

return oldhandler;

}

void* Widget::operator new (std::size_t size) throw(std::bad_alloc)

{  //安装Widget的new_hander

NewHandlerHolder h(std::new_handler(currentHandler));

return ::operator new(size);  //分配内存或抛出异常恢复global new_handler

}

示例10-1-1 class专属的new_handler上章

Widget的operator new做以下事情:

1)    调用标准set_new_handler,告知Widget的错误处理函数。

2)    调用global operator new,执行实际内存分配。如果分配失败,global operator new会调用Widget的new_handler。如果global operator new最终无法分配足够内存,会抛出一个bad_alloc异常。

3)    如果global operator new能够分配足够一个Widget对象所用的内存,Widget的operator new会返回一个指针,执行分配所得。

以下以C++代码再阐述一次:

    

class NewHandlerHolder {

public:

explicit NewHandlerHolder(std::new_handler nh):handler(nh){}  //取得目前的new_handler

~ NewHandlerHolder()

{ std::set_new_handler(handler);}  //释放它

  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:

std::new_handler handler;

NewHandlerHolder(const NewHandlerHolder&);  //阻止coping行为,条款4

NewHandlerHolder& operator=(const NewHandlerHolder&);

};

//static成员必须在class定义式之外被定义,在class实现文件内初始化为null

std::new_handler Widget::currentHandler = 0;

static std::new_handler Widget::set_new_handler(std::new_handler p) throw()

{

std::new_handler oldhandler = currentHandler;

currentHandler = p;

return oldhandler;

}

void outOfMem();

Widget::set_new_handler(outOfMem);

Widget *pw1 = new Widget; //如果分配失败,调用outOfMem

Std::string *ps = new std::string; //如果分配失败,调用global new_handler函数

Widget::set_new_handler(0);

Widget *pw2 = new Widget; //如果分配失败,立即抛出异常

示例10-1-2 class专属的new_handler下章

假设上述的方案改为base class继承derived class,然后将这个base class转为template,如此每个derived class将获得实体互异的class data复件。这个设计的base class部分让derived classes继承它们所需要的set_new_handler和operator new,而template部分则去报每一个derived class获得一个实体互异currentHandler成员变量。如下:

template<typename T>

class NewHandlerSupport { //class专属的set_new_handler

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:

std::new_handler handler;

};

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>::operatornew (std::size_t size) throw(std::bad_alloc)

{  //安装Widget的new_hander

NewHandlerHolder h(std::new_handler(currentHandler));

return ::operator new(size);  //分配内存或抛出异常恢复global new_handler

}

//以下每一个currentHandler初始化null

template<typename T>

std::new_handler NewHandlerSupport<T>::currentHandler = 0;

示例10-1-3 class专属的new_handler template

有了这个class template为Widget添加set_new_handler支持能力就容易多了。只要Widget继承NewhandlerSupport<Widget>就ok。

class Widget:: public NewHandlerSupport<Widget>

{ //和先前一样,但不必声明set_new_handler或operator new



};

Template机制会自动为每一个T生成一份currentHandler。

C++标准委员会提供了一种形式的operator new,负责供应传统的“分配失败就返回null”的行为。这个形式被称为“nothrow”形式。

Widget *pw1 = new Widget; //如果分配失败,抛出bad-alloc

if (pw1 == NULL) //这个测试一定失败

Widget *pw2 = new (std::nothrow) Widget; //如果分配失败,返回0

if (pw2 == NULL) //这个测试可能成功
表达式“new(std::nothrow)Widget ”发生两件事,第一,nothrow版的operator new被调用,如果分配失败,返回null。第二,Widget构造函数导致异常。因此,其实没有必要使用nothrow new的需要。

10.2 条款50:了解new和delete的合理替换时机 (Understand when it makes sense to replace new and delete)

替换编译器提供的operator new或operator delete的常见理由:

1)    用来检测运用上的错误。如果我们自行一个operator news,便可超额分配内存,以额外的空间放置特定的byte pattern(即签名,signature)。Operator deletes便得以检测上述签名是否原封不动。

2)    为了强化效能。 如果你对程序的动态内存分配有深刻了解,可以自己定制。

3)    为了收集统计数据。

4)    为了增加分配和归还的速度。泛用型的分配器往往比定制性分配器慢。

5)    为了降低内存管理器带来的空间额外开销。泛用型内存管理往往还是用更多的内存来区分类型。

6)    为了弥补缺省分配器中的非最佳齐位(suboptional alignment)。

7)    为了将相关对象成簇集中。

8)    为了获得非传统的行为。比如分配和归还共享内存(share memory)。使用C++封装C API。

10.3 条款51:编写new和delete时需要固守成规 (Be aware of template metaprogramming)

先从operator new谈起。实现一致性operator new必得返回正确的值,内存不足时必得调用new handling函数,必须有对付零内存需求的准备,还需要避免不慎掩盖正常形式的new。

Operator new的返回值,要么返回一个指针指向那块内存,要么抛出一个bad_alloc异常。Operator new实际上不止尝试一次分配内存,并每次失败后调用new_handing函数,当指向new_handing函数的指针是null,operator new才会抛出异常。Operator new内含一个无穷循环,退出循环的唯一办法是:内存分配成功,安装另一个new_handler,卸载new_handler,抛出bad_alloc异常,或是承认失败而直接return。条款49。

客户要求0bytes时,operator new也得返回一个合法指针,指向1字节的空间。

假设operator new成员函数会被derived classes继承。例如

class Base {

public:

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

。。。

};

class Derived:public Base {。。。}; //Derived没有声明operator new

Derived *pd = new Derived;  //错误,这里调用的是Base的operator new

示例10-3-1 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[],唯一需要做的一件事就是分配一块未加工内存(raw memory)。

C++保证“删除null指针永远安全”,所以non-member operator delete的伪码:

void operator delete(void *pMem) throw ()

{

    if (pMem== 0) return;

    //现在归还内存

    …

}

这个函数的member版本如下:

void operator Base::delete(void *pMem, size_t size) throw ()

{

    if (pMem== 0) return;

if (size != sizeof(Base)){

     ::operator delete(size);

return;

}

//现在归还内存

    …

}

10.4 条款52:写了placement new也要写placement delete (Be aware of template metaprogramming)

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

Widget *pw = new Widget;

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

假设其中第一个函数调用成功,第二函数却抛出异常,此时防止内存泄露的责任落到C++运行期系统身上。运行期系统调用步骤一所调用的operator new的相应operator delete版本,前提是它必须知道哪一个operator delete该被调用。

正常的operator new:

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

对应于正常的operator delete:

void *operator delete(void* rawMem) throw (std::bad_alloc);  //global作用域

void *operator new(void* rawMem,  std::size_t size) throw (std::bad_alloc); //class作用域

如果operator new接受的参数除了一定会有的那个size_t之外还有其他,这便是所谓的placement new。众多placement new版本中特别有用的一个是“接受一个指针指向对象该被构造之处”,如下:

void *operator new(std::size_t, void* pMem) throw();

这个版本的new被纳入了C++标准程序库。一般性术语,“placement new”意味带任意额外参数的new,因为另一个术语“placement delete”直接派生自它。

假设你写了一个class专属的operator new,如下:

class Widget {

public:



static void *operator new(std::size_t size, std::ostream & logStream)

   throw(std::bad_alloc);         //非正常形式的new

static void operator delete(void *pMem, std::size_t size);

  throw();                   //正常的class专属delete

};

//调用operator new并传递cerr为其ostream实参,

//这个动作会在widget构造函数抛出异常时泄露内存

Widget *pw = new (std::cerr) Widget;

示例10-4-1 class专属的operator new

运行期系统寻找“参数个数和类型都与operator new相同”的某个operator delete。如果找到,就调用它的调用对象。本例的operator new接受类型为ostream&的额外实参,所以对应的operator delete应该是:

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

类似于new的placement版本,operator delete如果接受额外参数,便称为placement deletes。

为此class Widget有必要声明一个placement delete,对应与那个有记录日志功能的placement new。

这样改变之后,有了如下的情况:

Widget *pw = new (std::cerr) Widget;

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

如上情况,调用的是正常形式的operator delete,而非placement 版本。Placement delete版本只有在“伴随placement new调用而触发的构造函数”出现异常时才会被调用。

另外,由于成员函数的名称会遮掩其外围作用域的相同名称(条款33),你必须小心避免让class的专属的news掩盖客户期望的其他news(包括正常版本)。

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

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; //正确,调用Base的operator new

示例10-4-2 名称被遮掩

条款33,对于撰写分配函数,缺省情况下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

class StandardBase {

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* pMem) throw()

{return ::operator delete(pMem);}    

// placement new/delete

static void *operator new(std::size_t size,void*ptr) throw()

{ return ::operator new(size, ptr);}

static void operator delete(void* pMem, void*ptr) throw()

{ return ::operator delete(pMem,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* pMem, const std::nothrow_t &nt) throw()

{ return ::operator delete(pMem);}

};

class Widget:public StandardBase {

public:

using StandardBase::operator new;

using StandardBase::operator delete;

static void *operator new(std::size_t size, std::ostream & logStream)

   throw(std::bad_alloc);         //非正常形式的new

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

。。。

};

Widget *pw = new (std::clog) Widget;  //OK

Widget *pw = new Widget; //正确

示例10-4-3 class专属new用法

这就意味着,如果要对所有与placement new相关的内存泄露宣战,必须同时提供一个正常的operator delete(用于构造期间无任何异常被抛出)和一个placement版本(用于构造期间有异常抛出)。后者的额外参数必须和operator new一样。

11 其他(Others)

11.1 C++关键字explicit

C++提供了关键字explicit,可以阻止不应该允许的经过转换构造函数进行的隐式转换的发生。声明为explicit的构造函数不能在隐式转换中使用。

  C++中, 一个参数的构造函数, 承担了两个角色。 1 是个构造器 2 是个默认且隐含的类型转换操作符。

  所以, 有时候在我们写下如 AAA = XXX, 这样的代码, 且恰好XXX的类型正好是AAA单参数构造器的参数类型, 这时候编译器就自动调用这个构造器, 创建一个AAA的对象。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息