本书至此(包括本章)我们一直强调:当operator new()和operator new[]()分配内存失败时,通常会抛出std::bad_alloc异常。这种说法在大多数情况下成立,但有个微妙细节我们尚未讨论 —— 现在需要重点解析。
设想这样一种场景:用户代码定制了内存分配函数,从预分配的数据结构中获取内存块以获得特定性能优势。假设该数据结构初始仅分配少量内存块,当初始块耗尽时才扩展容量。此时,存在两种状态:
乐观状态:快速分配初始预留块
二次机会状态:初始块耗尽后尝试扩展分配
为实现无感知的策略切换(无需用户代码显式干预),直接抛出std::bad_alloc并不理想。虽然抛出异常会终止operator new()执行,使用端代码也可捕获异常处理,但在此场景中,更希望分配失败触发特定操作后,允许分配函数在更新状态后重试。
C++通过std::new_handler处理此类场景,该类型是函数指针void(*)()的别名。其核心机制如下:
全局处理机制:程序中存在全局std::new_handler,默认值为nullptr
handler管理接口:std::set_new_handler()设置当前handler(返回被替换的旧handler),std::get_new_handler()获取当前handler
分配失败处理流程:当operator new()等分配函数失败时:
获取当前std::new_handler
若为nullptr → 抛出std::bad_alloc(默认行为)
若非空 → 调用该handler并重试分配(handler可能已更新内存状态)
标准库已实现这套算法,但截至目前,我们自定义的operator new()和operator new[]()重载尚未包含该逻辑。为演示如何利用std::new_handler,实现一个模拟的“两阶段分配”场景。
这个示例性实现将:
为类型X重载成员版本的分配操作符
初始设定仅支持分配limit个X对象(实际工程中需真实管理内存,第10章将展示完整案例)
std::new_handler会:
触发时,将limit提升至更大数值
重置当前handler为nullptr,使后续分配失败时抛出std::bad_alloc
#include <new>
#include <vector>
#include <iostream>
struct X {
// 简单示例,非线程安全
static inline int limit = 5;
void* operator new(std::size_t n) {
std::cout << "X::operator new() called with "
<< limit << " blocks left\n";
while (limit <= 0) {
if (auto hdl = std::get_new_handler(); hdl)
hdl();
else
throw std::bad_alloc{};
}
--limit;
return ::operator new(n);
}
void operator delete(void* p) {
std::cout << "X::operator delete()\n";
::operator delete(p);
}
// 数组版本的 new/delete 同理
};
int main() {
std::set_new_handler([]() noexcept {
std::cout << "allocation failure, "
"fetching more memory\n";
X::limit = 10;
std::set_new_handler(nullptr); // 将 handler 重置为默认
});
std::vector<X*> v;
v.reserve(100);
try {
for (int i = 0; i != 10; ++i)
v.emplace_back(new X);
} catch(...) {
// 由于 new_handler 的存在,此异常处理块在此程序中永远不会执行
std::cerr << "out of memory\n";
}
for (auto p : v) delete p;
}
X::operator new()处理失败的方式:当发现无法满足后置条件时,会获取当前std::new_handler —— 若非空则调用该handler后重试分配。
所以,handler调用时:
成功路径:修改内存状态使后续分配可能成功(如:扩展内存池/释放闲置内存)
失败路径:将handler重置为nullptr以触发std::bad_alloc
违反这些规则会导致无限循环(伴随灾难性后果)。示例中main()安装的handler行为如下:
调用时提升X::limit数值(模拟“二次机会”资源扩展)
立即通过std::set_new_handler(nullptr)禁用后续重试
若二次机会资源仍不足,则触发异常终止(工程术语称为“toast”状态)
虽然我们将std::new_handler描述为void(*)()函数指针的别名,但在示例中却安装了一个lambda表达式。这之所以可行,是因为无状态lambda(即捕获列表为空的lambda)能隐式转换为具有相同调用签名的函数指针。这一特性在多种场景中非常实用,例如:
编写需要与C代码交互的C++程序
调用操作系统原生API接口
适配传统回调函数机制
接下来我们将进入本章最具技术深度的部分 —— 探讨如何运用C++处理非常规内存。