effective c++ 笔记 条款49-52

条款 49:了解 new-handler 的行为

当operator new无法满足某一内存分配需求时,会不断调用一个客户指定的错误处理函数,即所谓的 new-handler,直到找到足够内存为止
new-handler 是一个 typedef,指向一个无参数值无返回值的函数。可以通过 set_new_handler 函数去指定客户想要的 new-handler。
set_new_handler 函数接受一个新的 new-handler 参数,返回被替换掉的 new-handler 函数

namespace std {
    using new_handler = void(*)();
    new_handler set_new_handler(new_handler) noexcept;    // 返回值为原来持有的 new-handler
}

设计良好的 new-handler 函数:

  1. 让更多的内存可被使用: 可以让程序一开始执行就分配一大块内存,而后当 new-handler 第一次被调用,将它们释还给程序使用,造成operator new的下一次内存分配动作可能成功。
  2. 安装另一个 new-handler:如果当前的new-handler不能够为你提供更多的内存,可能另外一个new-handler可以。即在当前的new-handler的位置上安装另外一个new-handler(通过调用set_new_handler)。下次operator new调用new-handler函数的时候,它会调用最近安装的。这需要让new_handler修改会影响new-handler行为的static数据,命名空间数据或者全局数据
  3. 卸除 new-handler: 将nullptr传给set_new_handler,使operator new在内存分配不成功时抛出异常
  4. 出 bad_alloc(或派生自 bad_alloc)的异常: 这样的异常不会被operator new捕捉,会被传播到内存分配处
  5. 不返回: 通常调用std::abort或std::exit

以不同的方式处理内存分配的情况,比如按不同的 class 进行处理。c++ 并不支持为每一个 class 提供专属版本的 new_handler,要用静态成员

public:
    static std::new_handler set_new_handler(std::new_handler p) noexcept;
    static void* operator new(std::size_t size);
private:
    static std::new_handler currentHandler;
};
// 做和 std::set_new_handler 相同的事情
std::new_handler Widget::set_new_handler(std::new_handler p) noexcept {
    std::new_handler oldHandler = currentHandler;
    currentHandler = p;
    return oldHandler; 
}
void* Widget::operator new(std::size_t size) {
    auto globalHandler = std::set_new_handler(currentHandler);  // 切换至 Widget 的专属 new-handler
    void* ptr = ::operator new(size);                           // 分配内存或抛出异常
    std::set_new_handler(globalHandler);                        // 切换回全局的 new-handler
    return globalHandler;
}
std::new_handler Widget::currentHandler = nullptr;

以对象管理资源的方法:

class NewHandlerHolder {
public:
    explicit NewHandlerHolder(std::new_handler nh): handler(nh) {}~NewHandlerHolder() {
        std::set_new_handler(handler);
    }private:
    std::new_handler handler;
};

Widget::operator new的实现可改为

void* Widget::operator new(std::size_t size) noexcept{
    NewHandlerHolder h(std::set_new_handler(currentHandler));
    return ::operator new(size);
}

Widget的客户调用 :

void OutOfMem();
Widget::set_new_handler(OutOfMem);
auto pw1 = new Widget;              // 若分配失败,则调用 OutOfMem
Widget::set_new_handler(nullptr);
auto pw2 = new Widget;              // 若分配失败,则抛出异常

上述代码每个class要实现自己的set_new_handler和operator new。可以用template。建立起一个“mixin”风格的基类,让其派生类继承它们所需的set_new_handler和operator new,并且使用模板确保每一个派生类获得一个实体互异的currentHandler成员变量

template<typename T>
class NewHandlerSupport {       // “mixin”风格的基类
public:
    static std::new_handler set_new_handler(std::new_handler p) noexcept;
    static void* operator new(std::size_t size);
    ...                         // 其它的 operator new 版本,见条款 52
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) {
    auto globalHandler = std::set_new_handler(currentHandler);
    void* ptr = ::operator new(size);
    std::set_new_handler(globalHandler);
    return globalHandler;
}

template<typename T>
std::new_handler NewHandlerSupport<T>::currentHandler = nullptr;

class Widget : public NewHandlerSupport<Widget> {
public:
    ...                         // 不必再声明 set_new_handler 和 operator new
};

此处的模板参数T并没有真正被当成类型使用,而仅仅是用来区分不同的派生类,使得模板机制为每个派生类具现化出一份对应的currentHandler
即 CRTP(curious recurring template pattern,奇异递归模板模式),也被用于实现静态多态

template <class Derived> 
struct Base {
    void Interface() {
        static_cast<Derived*>(this)->Implementation();      // 在基类中暴露接口
    }
};
struct Derived : Base<Derived> {
    void Implementation();                                  // 在派生类中提供实现
};

C++ 保留了传统的“分配失败便返回空指针”的operator new,称为 nothrow new,通过std::nothrow对象来使用

Widget* pw1 = new Widget;                   // 如果分配失败,抛出 bad_alloc
if (pw1 == nullptr) ...                     // 这个测试一定失败
Widget* pw2 = new (std::nothrow) Widget;    // 如果分配失败,返回空指针
if (pw2 == nullptr) ...                     // 这个测试可能成功

条款50:了解new和delete的合理替换时机

定制operator new和operator delete的理由:
用来检测运行上的错误:如果将“new 所得内存”delete 掉却不幸失败,会导致内存泄漏;如果在“new 所得内存”身上多次 delete 则会导致未定义行为。如果令operator new持有一串动态分配所得地址,而operator delete将地址从中移除,就很容易检测出上述错误用法
另外自定义new分配超额内存,在额外空间放置特定签名/byte pattern。在delete时检查是否不变;反之,肯定存在“overruns”(写入点在分配区块尾部之后)或“unferruns”(写入点在分配区块头部之前),delete也可log那个指针。例如:

static const int signature = 0xDEADBEEF;              // 调试“魔数”
using Byte = unsigned char;

void* operator new(std::size_t size) {
    using namespace std;
    size_t realSize = size + 2 * sizeof(int);         // 分配额外空间以塞入两个签名

    void* pMem = malloc(realSize);                    // 调用 malloc 取得内存
    if (!pMem) throw bad_alloc();

    // 将签名写入内存的起点和尾端
    *(static_cast<int*>(pMem)) = signature;
    *(reinterpret_cast<int*>(static_cast<Byte*>(pMem) + realSize - sizeof(int))) = signature;

    return static_cast<Byte*>(pMem) + sizeof(int);    // 返回指针指向第一个签名后的内存位置
}

这段代码不保证内存对齐,许多地方不遵守c++规范,见条款51
收集使用上的统计数据: 定制 new 和 delete 动态内存的相关信息:分配区块的大小分布,寿命分布,FIFO(先进先出)、LIFO(后进先出)或随机次序的倾向性,不同的分配/归还形态,使用的最大动态分配量等等。
增加分配和归还的速度:泛用型分配器通常比定制分配器慢。类专属的分配器可以做到“区块尺寸固定”,例如 Boost 提供的 Pool 程序库。又例如,编译器所带的内存管理器是线程安全的,但如果你的程序是单线程的,你也可以考虑写一个不线程安全的分配器来提高速度
降低缺省内存管理器带来的空间额外开销:泛用型分配器通常比定制分配器使用更多内存。因为常常在每一个分配区块身上招引某些额外开销。针对小型对象而开发的分配器(例如 Boost 的 Pool 程序库)本质上消除了这样的额外开销
弥补缺省分配器中的非最佳内存对齐:许多计算机体系架构要求特定的类型必须放在特定的内存地址上,如果没有奉行这个约束条件,可能导致运行期硬件异常,或者访问速度变低。std::max_align_t用来返回当前平台的最大默认内存对齐类型,对于malloc分配的内存,其对齐和max_align_t类型的对齐大小应当是一致的,但若对malloc返回的指针进行偏移,就没有办法保证内存对齐
C++11 中,内存对齐相关方法
将相关对象成簇集中:如果知道特定的某个数据结构往往被一起使用,又希望在处理这些数据时将“内存页错误(page faults)”的频率降至最低,可以考虑为此数据结构创建一个堆,将它们成簇集中在尽可能少的内存页上。一般可以使用 placement new 达成这个目标条款52
获得非传统的行为:如分配和归还共享内存,这些事情只能被 C API 完成,则可以将 C API 封在 C++ 的外壳里,写在定制的 new 和 delete 中

条款 51:编写 new 和 delete 时需固守常规

编写自己的new:存不足时必须不断调用 new-handler,如果无法供应客户申请的内存,就抛出std::bad_alloc异常。即使客户需求为0字节,operator new也得返回一个合法的指针

void* operator new(std::size_t size) {
    using namespace std;

    if (size == 0)      // 处理0字节申请
        size = 1;       // 将其视为1字节申请

    while (true) {
        if (...)        // 如果分配成功
            return ...; // 返回指针指向分配得到的内存

        // 如果分配失败,调用目前的 new-handler
        auto globalHandler = get_new_handler(); // since C++11

        if (globalHandler) (*globalHandler)();
        else throw std::bad_alloc();
    }
}

如果子类未声明自己的operator new,会从父类继承过来,使的子类使用了父类new分配方式。但子类与父类的大小多数时候是不同的,因此成员函数版本:

void* Base::operator new(std::size_t size) {
    if (size != sizeof(Base))
        return ::operator new(size);    // 转交给标准的 operator new 进行处理
    ...
}

此时无需检测大小是否为0,因为类必须有非零大小条款39
如果要实现operator new[],即array new,唯一要做的就是分配一块未加工的原始内存。因为无法对尚未存在的元素对象做任何事,甚至无法计算含有多少个对象
编写自己的delete:删除空指针永远安全

void operator delete(void* rawMemory) noexcept {
    if (rawMemory == 0) return;
    // 归还 rawMemory 所指的内存
}

成员函数版本

void Base::operator delete(void* rawMemory, std::size_t size) noexcept {
    if (rawMemory == 0) return;
    if (size != sizeof(Base)) {
        ::operator delete(rawMemory);    // 转交给标准的 operator delete 进行处理
        return;
    }
    // 归还 rawMemory 所指的内存
}

条款 52:写了 placement new 也要写 placement delete

placement new:如果你的new接收的参数除了必定有的size_t外还有其他

void* operator new(std::size_t, std::ostream& logStream);
auto pw = new (std::cerr) Widget;

elete同理
当创建对象时,会先进行new函数,然后调用构造函数,如果构造出现异常,就需要delete,否则内存泄漏但客户手上的指针仍未指向该被归还的内存,因此由c++系统本身调用delete。系统需要知道哪个delete该被调用
当抛出异常时,运行期系统会寻找参数个数和类型都与 operator new 相同的某个 operator delete。 placement delete 只有在 placement new 的调用构造函数异常时才会被系统调用,即使对一个用 placement new 申请出的指针使用 delete,也绝不会调用 placement delete。因此如果要处理 placement new 相关的内存泄漏问题,我们必须同时提供一个正常版本的 delete 和 placement 版本的 delete。前者用于构造期间无异常抛出,后者用于构造期间有异常抛出

class Widget {
public:
    static void* operator new(std::size_t size, std::ostream& logStream);   // placement new
    static void operator delete(void* pMemory);                             // delete 时调用的正常 operator delete
    static void operator delete(void* pMemory, std::ostream& logStream);    // placement delete
};

还要注意同名函数遮掩调用的问题

class Base {
public:
    static void* operator new(std::size_t size, std::ostream& logStream);
    ...
};
auto pb = new Base;             // 无法通过编译!
auto pb = new (std::cerr) Base; // 正确

同理,子类的operator new会遮掩global和父类继承的operator new版本:

class Derived : public Base {
public:
    static void* operator new(std::size_t size);
    ...
};

auto pd = new (std::clog) Derived;  // 无法通过编译!
auto pd = new Derived;              // 正确

除非目的就是禁用,否则要确保这些默认形式对定制类型依然可用

void* operator(std::size_t) throw(std::bad_alloc);           // normal new
void* operator(std::size_t, void*) noexcept;                 // placement new
void* operator(std::size_t, const std::nothrow_t&) noexcept; // nothrow new

可以准备一个基类,包含所有的正常版本new和delete

class StadardNewDeleteForms{
public:
    // normal new/delete
    static void* operator new(std::size_t size){
        return ::operator new(size);
    }
    static void operator delete(void* pMemory) noexcept {
        ::operator delete(pMemory);
    }

    // placement new/delete
    static void* operator new(std::size_t size, void* ptr) {
        return ::operator new(size, ptr);
    }
    static void operator delete(void* pMemory, void* ptr) noexcept {
        ::operator delete(pMemory, ptr);
    }

    // nothrow new/delete
    static void* operator new(std::size_t size, const std::nothrow_t& nt) {
        return ::operator new(size,nt);
    }
    static void operator delete(void* pMemory,const std::nothrow_t&) noexcept {
        ::operator delete(pMemory);
    }
};

凡是需要自定义的class,可以继承该类并使用using声明式条款33

  • 20
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值