49:了解new-handler的行为
new申请内存失败会抛出bad alloc的异常,此前会调用一个错误处理函数,此函数由std::set_new_handler()指定set::set_new_handler()- 接受一个错误处理函数,返回旧的错误处理函数
throw表示可能抛出的异常类型,参数为空表示不抛出任何异常
1 2typedef void (*new_handler)(); // 无形参,返回值为void的函数指针 new_handler set_new_handler(new_handler f) throw();- 当new申请不到足够的内存时,会不断调用错误处理函数f,因此错误处理函数应该进行下面的处理之一:
- 提供更多可用的内存
- 向
set_new_handler中传入一个新的错误处理函数 set_new_handler函数中传入一个空指针,因此内存分配失败时不进行处理,直接抛出异常- 抛出
bad_alloc的异常 - 不返回:调用
std::abort或std::exitabort会设置程序非正常退出exit会设置程序正常退出,当存在未处理异常时,会调用terminate,内部回调set::set_terminate设置的回调函数,默认会调用abort
- 类型相关错误处理
- 为不同的类分配对象时,使用不同的错误处理函数
- 重载
set_new_handler和operator new,重载为static成员 - 可以写成模板
- 此处的模板参数
T并没有真正被当成类型使用,而仅仅是用来区分不同的派生类,使得模板机制为每个派生类具现化出一份对应的currentHandler - 这个做法用到了所谓的 CRTP(curious recurring template pattern,奇异递归模板模式),也常被用于静态多态
- 此处的模板参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31template <typename T> class NewHandlerSupport { public: static std::new_handler set_new_handler(std::new_handler p) noexcept; static void* operator new(std::size_t size); ~NewHandlerSupport() {std::set_new_handler(currentHandler);} private: NewHandlerSupport(const NewHandlerSupport&); // 阻止拷贝构造 NewHandlerSupport& operator=(const NewHandlerSupport&); // 阻止拷贝复制 static std::new_handler currentHandler; }; template <typename T> std::new_handler NewHandlerSupport<T>::currentHandler = nullptr; template <typename T> std::new_handler NewHandlerSupport<T>::set_new_handler(std::new_handler p) noexcept { std::new_handler oldHandler = currentHandler; currentHandler = p; return oldHandler; } template <typename T> void* NewHandlerSupport<T>::operator new(std::size_t size) { NewHandlerSupport h(std::set_new_handler(currentHandler)); // 返回的函数指针初始化了一个对象h,在退出函数时,执行h的析构过程,即将原来的handle恢复 return ::operator new(size); } // 使用 class Widget: public NewHandlerSupport<Widget>{ ... }; - new分配失败后,可能不会抛出异常,而是返回null,这种称为
nothrow new- 例子:
new (std::nothrow) int[10]; nothrow new只能保证内存分配错误时不抛出异常,无法保证对象的构造函数不抛出异常
- 例子:
50: 了解new和delete的合理替换时机
- 为什么需要自定义
operator new- 检测使用错误:检测多次delete,检测越界
- 提高效率:手动维护更适合应用场景的存储策略
- 比如针对特定类型,增加分配和归还的速度
- 比如将相关对象集成到簇中(即尽量分配到一个内存页上)
- 收集使用的统计信息
- 其他原因:比如安全性(将申请到的内存初始化为0),字节对齐等
51: 编写new和delete时需固守常规
operator new需要无限循环地获取资源,如果没能获取则调用"new handler",不存在"new handler"时应该抛出异常;operator new应该处理size == 0的情况;operator delete应该兼容空指针;operator new/delete作为成员函数应该处理size > sizeof(Base)的情况(因为继承的存在)。外部(非成员函数的)
operator new:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15void* operator new(std::size_t size) throw(std::bad_alloc){ if(size == 0) size = 1; // size=0时,返回合法的指针就说明成功分配了内存 while(true){ void *p = malloc(size); if(p) return p; // 申请失败,获得new handler,多线程需要加锁 new_handler h = set_new_handler(0); set_new_handler(h); // auto h = get_new_handler(); // C++11方式 if(h) (*h)(); // new-handler应该实现item49中描述的五种行为之一,否则,此处陷入死循环 else throw bad_alloc(); } }成员
operator new- 如果
operator new是针对基类的,也就是说operator new是针对大小为sizeof(Base)的内存进行优化的 - 一般来说派生类不应该使用基类的
operator new,因为派生类对象大小与基类对象大小一般不同
1 2 3 4 5 6 7 8 9 10 11 12class Base{ public: static void* operator new(std::size_t size); }; void* Base::operator new(std::size_t size) { if(size != sizeof(Base)) // sizeof(Base)永远不会为0(至少为1),因为空对象至少会插入一个char return ::operator new(size); // 使用全局的operator new ... } class Derived: public Base { ... };operator new[]与operator new有相同的参数和返回值,只需要分配一块原始内存
- 如果
delete- delete
- 惯例:delete一个空指针是安全的
外部
operator delete
| |
- 成员
operator delete- 如果基类的析构函数不是虚函数,则size大小为静态类型的大小;
- 比如
Base* p = new Derived; delete p;中,很可能派生类大小大于基类大小,因此存在内存泄露
- 比如
- 否则size为动态类型的大小
1 2 3 4 5 6 7 8void Base::operator delete(void* rawMemory, std::size_t size) noexcept { if (rawMemory == 0) return; if (size != sizeof(Base)) { ::operator delete(rawMemory); // 转交给标准的 operator delete 进行处理 return; } // 释放 rawMemory 所指的内存 } - 如果基类的析构函数不是虚函数,则size大小为静态类型的大小;
52: 写了placement new也要写`palcement delete
placement new:广义上指拥有额外参数的operator new- 背景:
- 在使用new创建对象时,往往进行了两个函数的调用:一个是
operator new,进行内存分配;一个是对象的构造函数 - 如果构造失败,此时对象没有被创建,对象无法被析构,且此时还没有拿到分配内存的地址
- 因此需要运行时系统进行delete,运行时系统需要知道使用的是哪一种
operator new,因此调用对应的operator delete- 如果没有对应的
operator delete函数,则运行时系统什么都不做,导致内存泄露
- 如果没有对应的
- 在使用new创建对象时,往往进行了两个函数的调用:一个是
- 当定义了
placement new时,同时也要定义对应的placement delete- 用户直接调用
delete时,运行时系统不会将其解释为placement delete,因此还需要定义一个正常的delete
1 2 3 4 5 6class Widget{ public: static void* operator new(std::size_t size, std::ostream& log) throw(std::bad_alloc); static void operator delete(void *mem, std::ostream& log); static void operator delete(void *mem) throw(); };- 名称隐藏:类中的名称会隐藏类外的名称,子类的名称会隐藏父类的名称
- 三种全局new
1 2 3void* operator(std::size_t) throw(std::bad_alloc); // normal new void* operator(std::size_t, void*) noexcept; // placement new void* operator(std::size_t, const std::nothrow_t&) noexcept; // nothrow new
- 三种全局new
- 用户直接调用
- 最佳实践:
- 将全局版本new在一个基类中进行重载,内部调用全局new进行实现
- 然后在自定义类Widget中,public继承,并使用using声明使得三种new和三种delete对Widget可见,因此同时Widget可以定义自己版本的
placement new
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22class 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); } }; class Widget: public StandardNewDeleteForms { public: using StandardNewDeleteForms::operator new; using StandardNewDeleteForms::operator delete; static void* operator new(std::size_t size, std::ostream& log) throw(std::bad_alloc); // 自定义 placement new static void operator delete(void *pMemory, std::ostream& logStream) throw(); // 对应的 placement delete };