Effective C++: 08定制new和delete

49:了解new-handler的行为

         当operator new无法满足某一内存分配需求时,它会抛出异常(以前会返回一个null)。在抛出异常之前,它会调用一个客户指定的错误处理函数,也就是所谓的new-handler。

         客户通过调用set_new_handler来设置new-handler:

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

          set_new_handler返回之前设置的new_handler。

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

         a:让更多内存可被使用,以便使operator new下一次分配内存能够成功。实现方法之一就是程序一开始就分配一大块内存,而后当new-handler第一次被调用时,将它们还给程序使用;

         b:安装另一个new-handler:如果目前的new-handler无法获得更多内存,并且它直到另外哪个new-handler有此能力,则当前的new-handler可以安装那个new-handler以替换自己,下次当operator new调用new-handler时,就是调用最新的那个。

         c:卸载new-handler,一旦没有设置new-handler,则operator new就会在无法分配内存时抛异常;

         d:抛出bad_alloc异常;

         e:不返回,直接调用abort或exit。

         有时希望根据不同的class有不同的方式处理内存分配的情况,但是C++并不支持class专属之new-handler,但是C++支持class专属operator new,所以可以利用这点来实现。例子如下:

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;

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

          使用Widget的用户首先调用Widget::set_new_handler设置其专属new-handler。然后,在Widget的专属operator new中,调用std::set_new_handler,设置全局new-handler为Widget的专属new-handler,然后调用全局operator new,执行内存分配。分配失败时,全局operator new会调用Widget的专属new-handler。

如果全局operator new最终无法分配足够内存,会抛出一个异常,或者分配成功后,此时,必须恢复全局new-handler为原来的设置。为了实现这一点,可以使用资源管理对象的方法:

class NewHandlerHolder {
public:
  explicit NewHandlerHolder(std::new_handler nh):handler(nh) {}
  ~NewHandlerHolder()  { std::set_new_handler(handler); }
private:
  std::new_handler handler;                         // remember it

  NewHandlerHolder(const NewHandlerHolder&);    // prevent copying
  NewHandlerHolder& operator=(const NewHandlerHolder&);
};

          有了资源管理类之后,Widget::operator new的代码如下:

void * Widget::operator new(std::size_t size) throw(std::bad_alloc)
{
  //安装Widget专属new-handler,并使用NewHandlerHolder管理原有全局new-handler
  NewHandlerHolder h(std::set_new_handler(currentHandler));

  //不管分配成功还是抛出异常,NewHandlerHolder的析构函数中会恢复全局new-handler
  return ::operator new(size);                  
}  

         使用Widget的客户代码如下:

void outOfMem();
Widget::set_new_handler(outOfMem); // 设置Widget的专属new-handler

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

std::string *ps = new std::string; //如果分配失败,调用全局new-handler

Widget::set_new_handler(0); 
Widget *pw2 = new Widget; //分配失败,直接抛出异常

  

         实现class专属new-handler不会因class不同而不同,所以可以建立起一个mixin风格的base class。但是因为涉及到static成员,为了使derived class获得不同的base class属性,这里可以使用template:

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

// this initializes each currentHandler to null
template<typename T> std::new_handler NewHandlerSupport<T>::currentHandler = 0;

          有了这个模板之后,Widget可以直接继承它:

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

          这样看起来会有些奇怪,而且NewHandlerSupport模板从未使用类型参数T。这种技巧的作用,只是为了保证:继承自NewHandlerSupport的每一个class,都具有不同的NewHandlerSupport属性,也就是static成员变量currentHandler。这种技术还有自己的名字:“怪异的循环模板模式”(curiously recurring template pattern, CRTP)。

         直到1993年,C++还要求operator new在分配失败时返回null,新一代的operator new则应该抛出bad_alloc异常,但是许多C++程序是在编译器支持新规范之前写出来的,因此C++提供了另一形式的operator new,他会在分配失败时返回null:

class Widget { ... };
Widget *pw1 = new Widget; //分配失败抛出bad_alloc异常 
if (pw1 == 0) ... // 这个测试一定失败

Widget *pw2 = new (std::nothrow) Widget; // 分配失败返回NULL
if (pw2 == 0) ... // 这个测试可能成功

          虽然nothrow版的operator new不抛异常,但是接下来Widget的构造函数调用时,内部可能又会new一些内存,而这次不一定会在使用nothrow new。所以,使用nothrow new只能保证operator new不抛异常,不保证像new (std::nothrow) Widget这样的表达式不抛异常,因此其实没有使用nothrow new的需要。

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

         以下是替换编译器提供的operator new或operator delete的几个最常见的理由:

         a:检测运用错误:内存相关的错误包括内存泄漏、多次delete等,使operator new持有一串地址,而operator delete将地址从中移走,可以很容易检测出上述错误用法。另外,编程错误可能导致数据写入点在分配区块之后(overruns)或之前(underrun),可以自定义一个operator new,超额分配内存,在额外空间放置特定的签名,operator delete可以检查签名是否正确来判断是否发生了overrun或underrun。

         b:为了强化性能:编译器提供的operator new和operator delete主要用于一般目的,它们的工作对所有人都适度的好,但某些情况下,定制版的operator new和operator delete性能可以胜过缺省版本。要么是速度快,要么是更省内存。

         c:为了收集使用数据;

         d:为了弥补缺省分配器中的非最佳齐位,比如如果double是8-byte齐位的则访问速度最快,但编译器自带的operator new并不一定保证这一点,此时可以替换operator new为一个8-byte齐位的版本,使得程序效率大大提升;

         e:为了将相关对象成簇几种:如果某种数据结构往往一起使用,你希望在处理这些数据时将内存页错误的频率降至最低,则new和delete的placement版本有可能完成这样的任务;

         f:为了获得非传统的行为;

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

编写operator new时需要注意:必得返回正确的值,内存不足时需要调用new-handling函数,必须有对付零内存需求的准备,还需避免不慎掩盖正常形式的new;operator new的返回值十分单纯。如果它有能力供应客户申请的内存,就返回一个指针指向那块内存。如果没有那个能力,则抛出一个bad alloc异常;operator new实际上不只一次尝试分配内存,并在每次失败后调用new-handling函数。这里假设new-handling函数也许能够做某些动作将某些内存释放出来。只有当指向new-handling函数的指针是null,operator new才会抛出异常。

下面是一个非成员函数的operator new伪码:

void * operator new(std::size_t size) throw(std::bad_alloc) 
{                               //你的operator new可能有更多的参数
  using namespace std;          
  if (size == 0) {              //处理0内存需求,将其视为1Byte申请
    size = 1;                   
  }                            

  while (true) {
    attempt to allocate size bytes;
    if (the allocation was successful)
       return (a pointer to the memory);

    // 申请失败,找到当前的new-handler
    new_handler globalHandler = set_new_handler(0);
    set_new_handler(globalHandler);

    if (globalHandler) (*globalHandler)();
    else throw std::bad_alloc();
  }
}

 因为没有办法可以直接取得当前的new-handler,所以先调用set_new_handler将其找出来。

对于operator new的成员函数版本,因为该函数会被derived classes继承,所以需要考虑的更周全些。因为写出定制型内存管理器的一个最常见理由是为针对某特定class的对象分配行为提供最优化,却不是为了该class的任何derived classes。也就是说,针对class X而设计的operator new,其行为很典型地只为大小刚好为sizeof(X)的对象而设计。然而一旦被继承下去,有可能base class的operator new被调用用以分配derived class对象。处理这种情况的最佳做法是将内存申请量错误的调用行为改为标准operator new:

void * Base::operator new(std::size_t size) throw(std::bad_alloc)
{
  if (size != sizeof(Base)) 
     return ::operator new(size); 
  ... 
}

 上面的代码并没有检测size等于0的情况,是因为C++保证所有独立式对象必须具有非0大小,所以sizeof(Base)不会等于0。   

如果你打算控制class专属之“arrays内存分配行为”,那么你需要实现operator new [ ],唯一需要做的一件事就是分配一块未加工内存,因为你无法对array之内迄今尚未存在的元素对象做任何事情。实际上你甚至无法计算这个array将含多少个元素对象。首先你不知道每个对象多大,毕竟base class的operator new [ ]有可能经由继承被调用,将内存分配给“元素为derived class对象”的array使用。此外,传递给operator new[]的size t参数,其值有可能比“将被填以对象”的内存数量更多,因为条款16说过,动态分配的arrays可能包含额外空间用来存放元素个数。

operator delete情况更简单,需要记住的唯一事情就是C++保证“删除null指针永远安全”,下面是伪码:

void operator delete(void *rawMemory) throw()
{
  if (rawMemory == 0) return;
  deallocate the memory pointed to by rawMemory;
}

 这个函数的member版本也很简单,只需要多加一个动作检查删除数量。万一你的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();
  ...
};

void Base::operator delete(void *rawMemory, std::size_t size) throw()
{
  if (rawMemory == 0) return;

  if (size != sizeof(Base)) { 
     ::operator delete(rawMemory); 
     return; 
  }

  deallocate the memory pointed to by rawMemory;
  return;
}

 有趣的是,如果即将被删除的对象派生自某个base class而后者欠缺virtual析构函数,那么C++传给operator delete的size_t可能不正确。

 52:写了placement new也要写placement delete

         当你写一个new表达式:  Widget* pw=new Widget; 这种情况下共有两个函数被调用,一个是用以分配内存的operator new,一个是Widget的default构造函数。

假设operator new调用成功,构造函数却抛出异常。这种情况下,内存分配所得必须取消并恢复旧观,否则会造成内存泄漏。回收内存的责任是由C++运行期系统完成的。

运行期系统会调用operator new的相应operator delete版本,前提是它必须知道哪一个(因为可能有许多个)operator delete该被调用。如果目前面对的是拥有正常签名式的new和delete,这并不是问题,因为正常的operator new对应于正常的operator delete:

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

void operator delete(void *rawMemory) throw();  // 全局域
void operator delete(void *rawMemory, std::size_t size) throw(); // 类专属

 因此,当只使用正常形式的new和delete,运行期系统毫无问题可以找出那个“知道如何取消new所作所为并恢复旧观”的delete。然而当你开始声明非正常形式的operator new,也就是带有附加参数的operator new,“究竟哪一个delete伴随这个new”的问题便浮现了。

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

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

 这个版本的new已被纳入C++标准程序库,只要#include <new>就可以取用它。这个new的用途之一是负责在vector的未使用空间上创建对象。它同时也是最早的placement new版本。实际上它正是这个函数的命名根据:一个特定位置上的new。以上说明意味术语placement new有多重定义。当人们谈到placement new,大多数时候他们谈的是此一特定版本,少数时候才是指接受任意额外实参之operator new。因此一般性术语“placement new”意味带任意额外参数的new,因为另一个术语“placement delete”直接派生自它,operator delete如果接受额外参数,便称为placement deletes。

假设写一个class专属的operator new,要求接受一个ostream,用来记录相关分配信息,同时又写了一个正常形式的class专属operator 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, std::size_t size) throw(); 
  ...
};
Widget *pw = new (std::cerr) Widget; 

 如果内存分配成功,而Widget构造函数抛出异常,运行期系统需要取消operator new的分配并恢复旧观。然而运行期系统无法知道真正被调用的那个operator new如何运作,因此运行期系统寻找“参数个数和类型都与operator new相同”的某个operator delete。所以对应的operator delete就应该是:

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

 现在,既然Widget没有声明placement版本的operator delete,所以运行期系统不知道如何取消并恢复原先对placement new的调用。于是什么也不做,这就造成了内存泄漏。

规则很简单:如果一个带额外参数的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 *pw = new(std::cerr) Widget中构造函数抛出异常,则对应的placement delete就会被调用。

如果手动delete:delete pw; 这种情况下,调用的是正常形式的operator delete,而非其placement版本。placement delete只有在“伴随placement new调用而触发的构造函数”出现异常时才会调用,而delete一个指针永远不会调用placement delete。这就表示必须提供一个正常的operator delete,以及一个对应placement new的placement delete版本。

另外,由于成员函数的名称会掩盖外围作用域中的相同名称,因此需要避免让class专属的new掩盖客户期望的其他new,比如:

class Base {
public:
  ...
  static void* operator new(std::size_t size, std::ostream& logStream)
                                        throw(std::bad_alloc);  
  ...
};

Base *pb = new Base; // 错误,正常的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)  // 重新定义正常的operator new
      throw(std::bad_alloc); 
  ...
};
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(); // see Item 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()
  { ::operator delete(pMemory); }
};

 当想自定义operator new和delete时,可以利用继承和using:

class Widget: public StandardNewDeleteForms {           // inherit std forms
public:
   using StandardNewDeleteForms::operator new;          // make those
   using StandardNewDeleteForms::operator delete;       // forms visible

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

猜你喜欢

转载自www.cnblogs.com/gqtcgq/p/9393944.html
今日推荐