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
}
相关推荐
晚风(●•σ )1 小时前
C++语言程序设计——【算法竞赛常用知识点】
开发语言·c++·算法
..过云雨1 小时前
14.【Linux系统编程】进程间通信详解(管道通信、System V共享内存、消息队列、信号量)
linux·c语言·c++·后端
Mr_WangAndy1 小时前
C++23新特性_#warning 预处理指令
c++·c++23·c++40周年·c++23新特性·warning预处理命令
ULTRA??1 小时前
C++拷贝构造函数的发生时机,深拷贝实现
开发语言·c++
曦樂~1 小时前
【C++11】引用折叠原理
开发语言·c++
lucky_dog2 小时前
C语言——交换数组元素🍀🍀🍀
c++
Mr_WangAndy2 小时前
C++23新特性_if consteval
c++·c++23·c++40周年·if consteval
落羽的落羽2 小时前
【Linux系统】初探 虚拟地址空间
linux·运维·服务器·c++·人工智能·学习·机器学习
Drone_xjw2 小时前
【CPP回调函数】以无人机系统为例梳理回调函数使用
c++·无人机