Effective C++ 条款49:了解 new-handler 的行为
set_new_handler允许客户指定一个在内存分配无法获得满足时被调用的函数;nothrow new是一个颇具局限的工具,因为它只适用于内存分配(operator new),后继的构造函数调用还是可能抛出异常。
一、内存分配失败会发生什么?
在 C++ 中,当我们使用 new 申请内存时,如果系统无法满足这个请求,会发生什么呢?
cpp
int* p = new int[1000000000000L]; // 申请一个巨大的数组
在传统的 C++(1993 年之前),operator new 在无法分配足够内存时会返回 null。但从现代 C++ 开始,operator new 默认会抛出 std::bad_alloc 异常。
但是,在抛出异常之前,C++ 提供了一个"补救机会"------new-handler。
二、new-handler 基础
2.1 什么是 new-handler?
new-handler 是一个由用户指定的函数,当 operator new 无法满足内存分配请求时,它会在抛出 bad_alloc 异常之前被调用。
2.2 设置 new-handler
标准库在 <new> 头文件中提供了相关接口:
cpp
namespace std {
typedef void (*new_handler)();
new_handler set_new_handler(new_handler p) noexcept; // C++11 起用 noexcept 替代 throw()
}
| 参数/返回值 | 说明 |
|---|---|
p |
指向新 new-handler 函数的指针,传入 nullptr 表示移除当前 handler |
| 返回值 | 返回之前设置的 new-handler 指针,如果没有则返回 nullptr |
2.3 基本使用示例
cpp
#include <iostream>
#include <new>
#include <cstdlib>
// 自定义的 new-handler
void outOfMemory() {
std::cerr << "【错误】内存分配失败!无法分配请求的内存。\n";
// 选择1:终止程序
std::abort();
// 选择2:抛出异常(如果不 abort)
// throw std::bad_alloc();
}
int main() {
// 设置自定义的 new-handler
std::set_new_handler(outOfMemory);
// 尝试分配巨量内存
int* pBigDataArray = new int[1000000000000L]; // 触发 new-handler
return 0;
}
运行输出:
【错误】内存分配失败!无法分配请求的内存。
三、设计良好的 new-handler 应该做什么?
一个设计良好的 new-handler 函数可以考虑以下五种策略:
策略1:释放更多内存
cpp
class MemoryPool {
static char* emergencyBuffer;
static constexpr size_t BUFFER_SIZE = 1024 * 1024; // 1MB 应急内存
public:
static void init() {
emergencyBuffer = new char[BUFFER_SIZE];
}
static void releaseEmergencyBuffer() {
delete[] emergencyBuffer;
emergencyBuffer = nullptr;
std::cout << "【new-handler】释放应急内存,重试分配...\n";
}
};
char* MemoryPool::emergencyBuffer = nullptr;
void newHandler() {
if (MemoryPool::emergencyBuffer) {
MemoryPool::releaseEmergencyBuffer();
return; // 返回后 operator new 会重试分配
}
// 没有更多内存可释放,只能终止或抛异常
std::cerr << "【new-handler】无可用内存,终止程序。\n";
std::abort();
}
策略2:安装另一个 new-handler
cpp
void aggressiveNewHandler(); // 更激进的内存回收策略
void conservativeNewHandler(); // 保守策略
void conservativeNewHandler() {
std::cerr << "【保守策略】尝试轻度回收...\n";
// 如果保守策略无效,切换到激进策略
std::set_new_handler(aggressiveNewHandler);
}
void aggressiveNewHandler() {
std::cerr << "【激进策略】尝试深度回收...\n";
// 释放所有缓存
// 如果还是不行,只能终止
std::abort();
}
策略3:卸除 new-handler
cpp
void temporaryNewHandler() {
std::cerr << "【new-handler】临时 handler 被调用,移除后抛出异常。\n";
// 移除 new-handler,下次分配失败直接抛 bad_alloc
std::set_new_handler(nullptr);
// 注意:此时返回后 operator new 会重试,
// 如果仍然失败,就会抛出 bad_alloc
}
策略4:抛出 bad_alloc 或其派生异常
cpp
class MyMemoryException : public std::bad_alloc {
public:
const char* what() const noexcept override {
return "自定义内存不足异常";
}
};
void throwingNewHandler() {
std::cerr << "【new-handler】抛出自定义异常。\n";
throw MyMemoryException();
}
策略5:不返回(调用 abort 或 exit)
cpp
void fatalNewHandler() {
std::cerr << "【致命错误】内存耗尽,程序即将终止。\n";
// 记录日志...
std::abort(); // 或 std::exit(1)
}
四、Class 专属的 new-handler
4.1 为什么需要?
有时候我们希望针对不同类使用不同的内存分配失败处理策略。例如:
- 对于缓存类:释放旧缓存条目
- 对于日志类:丢弃旧日志
- 对于关键数据类:直接终止程序
4.2 实现 Class 专属 new-handler
C++ 标准并不直接支持 class 专属的 new-handler,但我们可以通过自定义 operator new 来实现:
cpp
#include <iostream>
#include <new>
#include <memory>
class Widget {
public:
// 设置 Widget 专属的 new-handler
static std::new_handler set_new_handler(std::new_handler p) noexcept;
// 自定义 operator new
static void* operator new(std::size_t size);
// 自定义 operator new[]
static void* operator new[](std::size_t size);
// 自定义 operator delete(配对使用)
static void operator delete(void* p) noexcept;
static void operator delete[](void* p) noexcept;
Widget() { std::cout << "Widget 构造\n"; }
~Widget() { std::cout << "Widget 析构\n"; }
private:
static std::new_handler currentHandler;
int data[100];
};
// 初始化静态成员
std::new_handler Widget::currentHandler = nullptr;
std::new_handler Widget::set_new_handler(std::new_handler p) noexcept {
std::new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
4.3 关键的 RAII 辅助类
为了确保全局 new-handler 能被正确恢复,我们需要一个 RAII 辅助类:
cpp
class NewHandlerHolder {
public:
explicit NewHandlerHolder(std::new_handler nh) noexcept
: handler(nh) {}
~NewHandlerHolder() {
// 析构时恢复原来的 new-handler
std::set_new_handler(handler);
}
// 禁止拷贝
NewHandlerHolder(const NewHandlerHolder&) = delete;
NewHandlerHolder& operator=(const NewHandlerHolder&) = delete;
private:
std::new_handler handler;
};
4.4 Widget 的 operator new 实现
cpp
void* Widget::operator new(std::size_t size) {
// 步骤1:安装 Widget 专属的 new-handler 作为全局 new-handler
NewHandlerHolder h(std::set_new_handler(currentHandler));
// 步骤2:调用全局 operator new 进行实际分配
// 如果失败,会调用 currentHandler(已被设为全局 handler)
return ::operator new(size);
// 步骤3:NewHandlerHolder 析构时自动恢复原来的全局 new-handler
}
void* Widget::operator new[](std::size_t size) {
NewHandlerHolder h(std::set_new_handler(currentHandler));
return ::operator new[](size);
}
void Widget::operator delete(void* p) noexcept {
::operator delete(p);
}
void Widget::operator delete[](void* p) noexcept {
::operator delete[](p);
}
4.5 使用示例
cpp
void widgetOutOfMemory() {
std::cerr << "【Widget new-handler】Widget 内存分配失败!\n";
std::abort();
}
void globalOutOfMemory() {
std::cerr << "【全局 new-handler】全局内存分配失败!\n";
std::abort();
}
int main() {
// 设置全局 new-handler
std::set_new_handler(globalOutOfMemory);
// 设置 Widget 专属 new-handler
Widget::set_new_handler(widgetOutOfMemory);
// 分配 Widget:如果失败,调用 widgetOutOfMemory
Widget* pw1 = new Widget;
// 分配其他类型:如果失败,调用 globalOutOfMemory
std::string* ps = new std::string;
// 移除 Widget 的专属 handler
Widget::set_new_handler(nullptr);
// 再次分配 Widget:如果失败,调用全局的 globalOutOfMemory
Widget* pw2 = new Widget;
delete pw1;
delete ps;
delete pw2;
return 0;
}
五、使用 Mixin 风格复用代码
上面的实现对于每个类来说代码几乎相同,我们可以通过 CRTP(奇异递归模板模式) 来复用:
cpp
#include <iostream>
#include <new>
// Mixin 基类模板
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);
static void operator delete(void* p) noexcept;
private:
static std::new_handler currentHandler;
};
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) {
NewHandlerHolder h(std::set_new_handler(currentHandler));
return ::operator new(size);
}
template<typename T>
void NewHandlerSupport<T>::operator delete(void* p) noexcept {
::operator delete(p);
}
// 每个 T 都有独立的 currentHandler 实例
template<typename T>
std::new_handler NewHandlerSupport<T>::currentHandler = nullptr;
// ==================== 使用 ====================
class Widget : public NewHandlerSupport<Widget> {
// 不需要声明 set_new_handler 或 operator new
// 全部从基类继承
};
class BigData : public NewHandlerSupport<BigData> {
double data[10000];
};
void widgetHandler() {
std::cerr << "【Widget】释放缓存重试...\n";
std::set_new_handler(nullptr); // 下次直接抛异常
}
void bigDataHandler() {
std::cerr << "【BigData】清理临时文件...\n";
std::set_new_handler(nullptr);
}
int main() {
Widget::set_new_handler(widgetHandler);
BigData::set_new_handler(bigDataHandler);
Widget* w = new Widget; // 失败时调用 widgetHandler
BigData* bd = new BigData; // 失败时调用 bigDataHandler
delete w;
delete bd;
return 0;
}
注意 :Widget 继承自 NewHandlerSupport<Widget>,这种"以派生类作为模板参数继承基类"的模式就是 CRTP。
六、nothrow new 的局限性
6.1 什么是 nothrow new?
在 1993 年之前,new 在失败时返回 null。为了兼容旧代码,C++ 提供了 nothrow 版本:
cpp
#include <new>
// 使用 nothrow new
Widget* pw = new (std::nothrow) Widget;
if (pw == nullptr) {
std::cerr << "分配失败\n";
} else {
// 使用 pw...
delete pw;
}
6.2 局限性分析
nothrow new 有一个严重的局限性:
它只保证
operator new(内存分配)不抛出异常,但后续的构造函数仍然可能抛出异常!
cpp
class DangerousWidget {
public:
DangerousWidget() {
// 构造函数内部又分配内存
internalData = new int[1000000000000L]; // 可能抛出 bad_alloc!
}
~DangerousWidget() {
delete[] internalData;
}
private:
int* internalData;
};
// 即使使用 nothrow,构造函数仍可能抛异常
DangerousWidget* pdw = new (std::nothrow) DangerousWidget;
// 如果构造函数抛出异常,pdw 仍然会得到 nullptr 吗?
// 答案是:不会!异常会传播出来!
| new 类型 | 内存分配失败 | 构造函数异常 |
|---|---|---|
普通 new |
抛 bad_alloc |
抛异常 |
nothrow new |
返回 nullptr |
仍可能抛异常 |
6.3 正确使用 nothrow new
nothrow new 只在以下场景有用:
- 分配原始内存 (如
char数组),不涉及构造函数 - 与 placement new 配合使用
- 需要兼容旧代码,且确认构造函数不会分配资源
cpp
// 场景1:分配原始内存
char* buffer = new (std::nothrow) char[1024];
if (buffer) {
// 使用 buffer...
delete[] buffer;
}
// 场景2:与 placement new 配合
void* rawMem = std::malloc(sizeof(Widget));
if (rawMem) {
Widget* pw = new (rawMem) Widget; // placement new
pw->~Widget(); // 手动析构
std::free(rawMem);
}
七、总结
| 要点 | 说明 |
|---|---|
| new-handler | 内存分配失败、抛出异常前被调用的用户自定义函数 |
| 设置方式 | std::set_new_handler(func_ptr) |
| 设计策略 | 释放内存、切换 handler、卸除 handler、抛异常、终止程序 |
| Class 专属 | 通过自定义 operator new + RAII 辅助类实现 |
| 代码复用 | 使用 CRTP Mixin 基类模板复用 new-handler 逻辑 |
| nothrow new | 仅保证内存分配不抛异常,构造函数仍可能抛异常 |
💡 最佳实践:
- 为应用程序设置一个合理的全局 new-handler
- 对于资源敏感类,考虑实现专属的 new-handler
- 使用 RAII 确保全局 new-handler 总能被正确恢复
- 谨慎使用
nothrow new,理解其局限性
掌握 new-handler 机制,你就能在内存分配失败时优雅地处理异常,而不是让程序莫名其妙地崩溃。这是编写健壮 C++ 程序的重要一环!
参考资料:《Effective C++》第三版,Scott Meyers 著