小结
new关键字分为两步,operator new和狭义placement new,前者内存分配,后者在特定内存上调用构造函数operator new又可设置std::nothrow,设置之后就代表本次分配不足直接返回nullptr,未设置则调用new-handler(),函数返回之后继续尝试重新分配,循环此过程直到成功或抛出std::bad_alloc或函数调用std::abort()终止程序- 编写new和delete需要遵守一些规则,比如处理
size==0以及空指针等 - 本章的
placement new重载是广义的,专门刨除狭义placement new,如果重载了placement new,要配套对应的placement delete;另外这种可能导致遮掩,使得普通new没法使用
49. 了解new-handler的行为
本章operator new只有在没设置std::nothrow才会在分配内存失败后调用new-handler,所以此章默认不设置
-
operator new的设计逻辑是:- 分配失败 → 调用
new_handler。(用户可以通过std::set-new-handler()来设置该函数)
- 期望
new_handler能通过某种方式(如释放预留内存)让后续分配成功。 - 如果
new_handler返回后分配仍失败,则再次调用new_handler。
- 如果
new_handler不改变任何状态,分配会一直失败,形成无限递归。所有需要一个好的设计:
- 让更多内存可被使用,即释放预留内存
- 终止程序,调用std::abort();
- 抛出异常,throw std::bad_alloc;
- 将函数设置成nullptr,这样系统会自动抛出异常
- 分配失败 → 调用
-
给每种class设置不同的
new_handler,做到Widget::set_new_handler(func);Widget* ptr = new Widget();//如果分配失败调用的是func- 为每种class设置不同的设置class
- 该函数属于class,应该使用静态成员函数
- 要给每种,可借助继承+template来为每种class具象化基类class
- template基类的类型参数仅仅是用于标识不同类型 ,如
Widget继承NewHandlerSupport<Widget> - 每次调用new,需要调用set_new_handler()设置成本类自定义的函数,分配内存之后(成与不成)都得再次调用set_new_handler()设置回去,这里借助RAII,将函数看成资源 ,这样就能在退出
operator new之后自动设置回去
-
调用
new (std::nothrow) Widget(),并不意味着不会抛出异常,因为这之后还会调用Widget构造函数,可能会继续调用new,进而抛出异常
50. 了解new和delete的合理替换时机
- 替换new和delete的原因有很多,比如想实现内存池、检测运用上的错误(比如多分配两个int,前后填入标识)以及收集heap使用信息等
- 自定义的new分为几乎行得通(逻辑没问题)和真正行得通 ,前者就是没有在意一些细节,比如new中需要循环调用new_handler,另外就是有对齐问题,
::operator new肯定返回的是对齐后的地址,但是如果再基于此地址去偏移(如在前面添加标志位),然后在偏移后的地址构造就可能没有对齐进而出问题
51. 编写new和delete时需固守成规
- operator new应该内含无限循环,并在其中尝试分配内存,如果它无法满足内存要求,则调用new-handler。同时也要有能力处理0bytes申请。Class专属版本(因为可能存在继承,Derived调用Base的new方法)则还应该处理"比正确大小更大的(错误)申请",一般是调用
::operator new - operator delete应该在收到nullptr不做任何处理,Class专属版本还应该处理"比正确大小更大的申请",一般是调用
::operator delete
52. 写了placement new也要写placement delete
- 这里术语placement new指的是operator new()参数不止是size_t,比如日志函数等,其实nothrow版本也就是placement new,此处并不包含狭义的定位new
- 当使用 placement new构造对象失败(构造函数抛出异常) 时,编译器会自动调用匹配的placement delete释放内存。若未定义匹配的placement delete,已分配的内存将泄漏。
- 如果使用placement new构造,显式调用
delete p时,编译器会调用普通operator delete (operator delete(size_t)),而非placement delete。 - 所以对于一个placement new,需要写operator delete(无异常情况)和placement delete(构造抛出异常)两个函数
- 若派生类定义了任何operator new重载 (含placement new),会遮掩基类的所有operator new以及全局的operator new版本(包括普通版本)。需显式using声明继承基类或全局版本
- 所以如果重构了placement new而没有重构普通版本,直接调用
new Widget()报错,因为被遮掩了
cpp
#include <iostream>
class Widget {
public:
// 自定义 placement new(带额外参数)
static void* operator new(size_t size, int extra_arg) {
std::cout << "调用自定义 placement new\n";
return ::operator new(size); // 委托全局 operator new
}
// ❌ 未显式保留普通 operator new
};
int main() {
Widget* p1 = new(42) Widget(); // ✅ 调用自定义 placement new
Widget* p2 = new Widget(); // ❌ 报错:找不到普通 operator new
}
- cpp全局标准的operator new,如果定义了class专属new,切记导致的遮掩现象
cpp
#include <new> // 为了不抛错以及定位new
void* operator new(std::size_t size);
void* operator new(std::size_t size, const std::nothrow_t&) noexcept; // 不抛错的版本
void* operator new(std::size_t size, void* ptr) noexcept;//此版本是标准的placement new,不允许被重构
int main(){
int* p = new int; // 底层调用 operator new(sizeof(int))
int* p = new (std::nothrow) int; // 失败时 p == nullptr
char buffer[sizeof(int)];
int* p = new (buffer) int(42); // 在 buffer 上构造 int
}