49:了解new-handler的行为
new
申请内存失败会抛出bad alloc
的异常,此前会调用一个错误处理函数,此函数由std::set_new_handler()
指定set::set_new_handler()
- 接受一个错误处理函数,返回旧的错误处理函数
throw
表示可能抛出的异常类型,参数为空表示不抛出任何异常
1 2
typedef 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::exit
abort
会设置程序非正常退出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 31
template <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 15
void* 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 12
class 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 8
void 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 6
class 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 3
void* 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 22
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); } }; 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 };