最近开始看《Effective C++》,为了方便以后回顾,特意做了笔记。若本人对书中的知识点理解有误的话,望请指正!!!
条款50 讨论了什么时候需要自定义 operator new 和 operator delete,现在本条款来讨论在自定义时需要遵守的规则。
从operator new开始:
- 实现一致性 operator new 必须返回正确的值
- 内存不足时必须调用 new-handling 函数(见条款49)
- 必须有对付零内存需求的准备
- 避免不慎掩盖正常形式的 new
这比较偏近 class 接口的要求而非实现要求。正常形式的 new 描述于条款 52。
operator new 的返回值十分单纯。如果申请内存成功,就返回指向那块内存的指针,失败则遵循条款 49描述的规则,并抛出 bad_alloc 异常。
然而也不是非常单纯。因为 operator new 实际上不止一次尝试分配内存,并在每次失败后都调用 new-handling 函数。这里假设 new-handling 函数能做某些动作将一些内存释放出来。只有当指向 new-handling 函数的指针为 null,operator new 才会抛出异常。但C++规定,即使客户要求0 byte,operator new 也要返回一个合法指针。下面是个non-member operator new的伪代码:
void* operator new(std::size_t size) throw(std::bad_alloc) { 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)(); //执行函数指针globalHandler指向的函数 else throw std::bad_alloc(); } }
对于 0 byte 的内存申请视为 1 byte 的内存申请,做法简单、合法、可行。其中将 new-handling 函数指针设为 null 而后又立刻恢复原样,是因为我们没有任何办法可以直接取得 new-handling 函数指针,所以利用 set_new_handler 函数的返回值是前一个 new-handling 函数指针的特性。这种方法在单线程环境下很有效,但在多线程环境下,还需要某种锁机制,以便处理 new-handling 函数背后的(global)数据结构。
条款49提到 operator new 内含一个无穷循环,而上述代码中的第 6 行(while(true))就是那个无穷循环。退出此循环唯一办法是:内存分配成功或 new-handling 函数做了一件描述于条款49的事:让更多内存可用、安装另一个 new-handler、卸载 new-handler、抛出 bad_alloc异常(或其派生类),或承认失败直接 return。
上面的 operator new 成员函数可能会被derived classes继承。注意分配内存大小size,它是函数接收的实参。条款50提到,定制内存分配器往往是为了特定的 class 对象,以此来优化,而不是为了该 class 的任何 派生类。也就是说,针对 class X 而设计的 operator new ,其行为只为大小刚好为 sizeof(X) 的对象而设计。然而一旦被继承,有可能 基类 的 operator new 被调用用于分配 派生类 对象:
class base{ public: static void* operator new(std::size_t size) throw(std::bad_alloc); ... }; class Derived:public base{ ... }; //假设派生类未定义 operator new Derived* p = new Derived; //这里调用了base::operator new
如果 基类 专属的 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 ... //否则在这处理 }
如果你打算控制 class 专属的 “arrays 内存分配行为”,那么你需要实现 operator new[](这个函数通常被称为 “array new”)。编写operator new[] 时,唯一要做的事就是分配一块未加工的内存,因为你无法对 array 之内迄今尚未存在的元素对象做任何事。甚至我们无法知道这个 array 含有多少个元素对象。首先你不知道每个对象多大,毕竟 基类 的 operator new[] 有可能经由继承被调用,将内存分配给 “元素为 派生类 对象” 的 array使用。
因此,你不能在 base::operator new[] 中假设 array 的每个元素对象大小是 sizeof(base),这样就是说你不能假设 array 元素个数是(bytes申请数 / sizeof(base))。此外,传递给 operator new[] 的 size_t 参数,其值有可能比“将被填以对象”的内存更大,因为条款 16提过,动态分配的 arrays 可能包含额外空间用来存放元素个数。
上面就是自定义 operator new 需要遵守的规矩。operator delete 情况更简单,你需要记住的唯一事情就是C++保证删除 null 指针永远安全。下面是 non-member operator delete的伪代码:
void operator delete(void* rawMemory) throw(){ if(rawMemory == 0) return; //如果被删除的是个 null 指针,那就什么都不做 现在, 归还 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) //检测是否为 null 指针 return; if(size != sizeof(base)) { //如果大小错误,让标准 operator delete 处理此一申请 ::operator delete(rawMemory); return; } 现在,归还rawMemory所指内存; return ; }
如果即将被删除的对象派生自某个 基类 ,而后者没有 虚析构函数,那么 C++ 传给 operator delete 的 size_t 数值可能不正确。也就是说,如果 基类 遗漏 虚析构函数,operator delete 可能无法正常运作。
Note:
- operator new 应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用 new-handler。它也应该有能力处理 0 bytes 申请。class 专属版本的还应该处理“比正确大小更大的(错误)申请”
- operator delete 应该在收到 null 指针时不做任何事。class专属版本则还应该处理“比正确大小更大的(错误)申请”
条款52:写了placement new 也要写 placement delete
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)