8. 定制new和delete

小结

  1. new关键字分为两步,operator new狭义placement new,前者内存分配,后者在特定内存上调用构造函数
  2. operator new又可设置std::nothrow,设置之后就代表本次分配不足直接返回nullptr,未设置则调用new-handler(),函数返回之后继续尝试重新分配,循环此过程直到成功或抛出std::bad_alloc或函数调用std::abort()终止程序
  3. 编写new和delete需要遵守一些规则,比如处理size==0以及空指针等
  4. 本章的placement new重载是广义的,专门刨除狭义placement new,如果重载了placement new,要配套对应的placement delete;另外这种可能导致遮掩,使得普通new没法使用

49. 了解new-handler的行为

本章operator new只有在没设置std::nothrow才会在分配内存失败后调用new-handler,所以此章默认不设置

  1. operator new 的设计逻辑是:

    1. 分配失败 → 调用 new_handler。(用户可以通过std::set-new-handler()来设置该函数
    • 期望 new_handler 能通过某种方式(如释放预留内存)让后续分配成功。
    • 如果 new_handler 返回后分配仍失败,则再次调用 new_handler
    1. 如果 new_handler 不改变任何状态,分配会一直失败,形成无限递归。所有需要一个好的设计:
    • 让更多内存可被使用,即释放预留内存
    • 终止程序,调用std::abort();
    • 抛出异常,throw std::bad_alloc;
    • 将函数设置成nullptr,这样系统会自动抛出异常
  2. 给每种class设置不同的new_handler,做到Widget::set_new_handler(func);Widget* ptr = new Widget();//如果分配失败调用的是func

    1. 为每种class设置不同的设置class
    • 该函数属于class,应该使用静态成员函数
    • 要给每种,可借助继承+template来为每种class具象化基类class
    1. template基类的类型参数仅仅是用于标识不同类型 ,如Widget继承NewHandlerSupport<Widget>
    2. 每次调用new,需要调用set_new_handler()设置成本类自定义的函数,分配内存之后(成与不成)都得再次调用set_new_handler()设置回去,这里借助RAII,将函数看成资源 ,这样就能在退出operator new之后自动设置回去
  3. 调用new (std::nothrow) Widget(),并不意味着不会抛出异常,因为这之后还会调用Widget构造函数,可能会继续调用new,进而抛出异常

50. 了解new和delete的合理替换时机

  1. 替换new和delete的原因有很多,比如想实现内存池、检测运用上的错误(比如多分配两个int,前后填入标识)以及收集heap使用信息等
  2. 自定义的new分为几乎行得通(逻辑没问题)和真正行得通 ,前者就是没有在意一些细节,比如new中需要循环调用new_handler,另外就是有对齐问题,::operator new肯定返回的是对齐后的地址,但是如果再基于此地址去偏移(如在前面添加标志位),然后在偏移后的地址构造就可能没有对齐进而出问题

51. 编写new和delete时需固守成规

  1. operator new应该内含无限循环,并在其中尝试分配内存,如果它无法满足内存要求,则调用new-handler。同时也要有能力处理0bytes申请。Class专属版本(因为可能存在继承,Derived调用Base的new方法)则还应该处理"比正确大小更大的(错误)申请",一般是调用::operator new
  2. operator delete应该在收到nullptr不做任何处理,Class专属版本还应该处理"比正确大小更大的申请",一般是调用::operator delete

52. 写了placement new也要写placement delete

  1. 这里术语placement new指的是operator new()参数不止是size_t,比如日志函数等,其实nothrow版本也就是placement new,此处并不包含狭义的定位new
  2. 当使用 placement new构造对象失败(构造函数抛出异常) 时,编译器会自动调用匹配的placement delete释放内存。若未定义匹配的placement delete,已分配的内存将泄漏。
  3. 如果使用placement new构造,显式调用delete p时,编译器会调用普通operator deleteoperator delete(size_t)),而非placement delete。
  4. 所以对于一个placement new,需要写operator delete(无异常情况)和placement delete(构造抛出异常)两个函数
  5. 若派生类定义了任何operator new重载 (含placement new),会遮掩基类的所有operator new以及全局的operator new版本(包括普通版本)。需显式using声明继承基类或全局版本
  6. 所以如果重构了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
}
  1. 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
}
相关推荐
superman超哥7 小时前
仓颉GC调优参数深度解析
c语言·开发语言·c++·python·仓颉
誰能久伴不乏7 小时前
Linux `epoll` 学习笔记:从原理到正确写法(含 ET 经典坑总结)
linux·服务器·网络·c++·ubuntu
(❁´◡`❁)Jimmy(❁´◡`❁)7 小时前
【算法】 二分图理论知识和判断方法
c++·算法
im_AMBER7 小时前
Leetcode 85 【滑动窗口(不定长)】最多 K 个重复元素的最长子数组
c++·笔记·学习·算法·leetcode·哈希算法
leiming68 小时前
c++ string 容器
开发语言·c++·算法
superman超哥8 小时前
仓颉Option类型的空安全处理深度解析
c语言·开发语言·c++·python·仓颉
你好音视频9 小时前
FFmpeg FLV解码器原理深度解析
c++·ffmpeg·音视频
报错小能手9 小时前
C++ STL bitset 位图
开发语言·c++
橘子真甜~9 小时前
Reids命令原理与应用1 - Redis命令与原理
数据库·c++·redis·缓存
钓鱼的肝9 小时前
GESP系列(3级)小杨的储蓄
开发语言·数据结构·c++·笔记·算法·gesp