条款 49:了解 new - handler 的行为。
当operator new抛出异常反应一个未满足的内存需求之前,它会先调用一个客户指定的错误处理函数,一个所谓的new-handler。设置错误处理的方式是调用set_new_handler。set_new_handler的标准库信息:
namespace std {
typedef void (*new_handler)();
new_handler set_new_handler(new_handler p) throw();
}
set_new_handler参数是一个指针,指向operator new无法分配足够内存时被调用的函数。其返回值也是一个指针,指向set_new_handler被调用前正在被执行(但是马上要被替换)的那个new-handler函数。
使用示例:
// 以下是当operator new 无法分配足够内存时,该被调用的函数
void outOfMem()
{
std::cerr << "Unable to statisfy request for memeory" << std::endl;
std::abort();
}
int main()
{
std::set_new_handler(outOfMem);
int *pBigDataArray = new int[100000000L];
...
}
一个设计良好的new-handler函数必须做如下事情:
- 让更多内存可被使用
书中给出的一个方式是程序一开始执行就分配一大块内存,而后当new-handler第一次被调用,将它们释还给程序使用。
还可以尝试如下方式:
通过释放当前进程中暂时不需要的内存资源(如缓存数据、临时对象、冗余容器等),为后续的内存分配腾出空间。
具体做法:清理全局 / 静态缓存(如std::vector::clear()
+shrink_to_fit()
)、释放不常用的大型对象、关闭不必要的资源句柄等。
关键:释放的内存必须是可被operator new
重新分配的(即归还给系统或进程的内存池)。
- 安装另一个new-handler
若当前new_handler
无法释放足够内存(如自身逻辑已穷尽),可替换为另一个更适合的处理函数(例如更激进的内存释放策略)。
目的:让下一次内存分配失败时,由新的new_handler
尝试补救,增加分配成功的可能性。
示例:从 “温和释放缓存” 切换到 “释放所有非核心数据” 的处理函数。
- 卸除new-handler
通过std::set_new_handler(nullptr)
将处理函数设为空,意味着后续内存分配失败时不再调用任何new_handler
,直接由operator new
抛出std::bad_alloc
异常。
适用场景:当前new_handler
已确认无法释放内存,且无其他有效处理函数可用,此时应终止补救流程,交由上层代码处理异常。
- 抛出bad_alloc(或派生自bad_alloc)的异常
new_handler
可以主动抛出std::bad_alloc
或其派生类异常(需保证异常可被operator new
捕获)。此时operator new
会终止分配尝试,并将异常传递给new
的调用者。
注意:若抛出非bad_alloc
家族的异常,会导致operator new
无法处理,最终调用std::terminate
终止程序。
- 不返回
通过调用std::abort()
、std::exit()
等函数直接终止程序,避免在内存严重不足的状态下继续运行(可能导致数据损坏、逻辑混乱等更严重问题)。
适用场景:内存分配失败属于 “不可恢复的致命错误”(如嵌入式系统内存耗尽、核心服务内存不足),此时终止程序是更安全的选择。
书中还描述了如何定制类对应的new_handler,暂时用不上,先略过。
条款 50:了解 new 和 delete 的合理替换时机。
替换编译器提供的operator new和operator delete的常见理由:
- 用来检测运用上的错误
通过额外的内存空间标记监测是否被踩等场景。
- 为了强化效能
编译器指定的new和delete过于中庸,因为考虑到普适性,在特定场景下不一定是最优解。
- 为了收集使用上的统计数据
自定义便于收集不同维度的内存使用情况。
下面是一段定制new的实现实例(有些小错误)
static const int signature = 0xDEADBEEF;
typedef unsigned char Byte;
// 这段代码还有若干小错误
void* operator new(std::size_t size) throw(std::bad_alloc)
{
using namespace std;
size_t realSize = size + 2 * sizeof(int); // 增加大小使能够塞入两个signatures
void *pMem = malloc(realSize);
if (!pMem) throw bad_alloc();
// 将signature写入内存的最前段落和最后段落
*(static_cast<int*>(pMem)) = signature;
*(reinterpret_cast<int*>(static_cast<Byte*>(pMem) + realSize - sizeof(int))) = signature;
// 返回指针,指向恰位于第一个signature之后的内存位置
return static_cast<Byte*>(pMem) + sizeof(int);
}
上述示例没有考虑齐位问题,尽管有很多理由重写new和delete但是仍然不推荐。
条款 51:编写 new 和 delete 时需固守常规。
这里只需要记住如下原则:
- operator new应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用new-handler.它也应该有能力处理0 bytes申请。Class专属版本则还应该处理"比正确大小更大的(错误)申请"。
- operator delete应该在收到null指针时不做任何事。Class专属版本则还应该处理比正确大小更大的(错误)申请(通过监测是否是本Class大小的申请)。
条款 52:写了 placement new 也要写 placement delete。
带参场景的new和delete实现。先可以做了解。
- 当你写一个placement operator new,请确定也写出了对应的placement operator delete.如果没有这样做,你的程序可能会发生隐微而时断时续的内存泄露。
- 当你声明placement new和placement delet,请确定不要无意识(非故意)地遮掩了它们的正常版本。