学完Efficient c++ (50-52)

27 篇文章 0 订阅
24 篇文章 0 订阅

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

以下是常见的替换默认operator newoperator delete的理由:

用来检测运用上的错误: 如果将“new 所得内存”delete 掉却不幸失败,会导致内存泄漏;如果在“new 所得内存”身上多次 delete 则会导致未定义行为。如果令operator new持有一串动态分配所得地址,而operator delete将地址从中移除,就很容易检测出上述错误用法。此外各式各样的编程错误可能导致 “overruns”(写入点在分配区块尾端之后) 和 “underruns”(写入点在分配区块起点之前),以额外空间放置特定的 byte pattern 签名,检查签名是否原封不动就可以检测此类错误,下面给出了一个这样的范例:

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 程序库)本质上消除了这样的额外开销。

为了弥补缺省分配器中的非最佳内存对齐(suboptimal alignment): 许多计算机体系架构要求特定的类型必须放在特定的内存地址上,如果没有奉行这个约束条件,可能导致运行期硬件异常,或者访问速度变低。std::max_align_t用来返回当前平台的最大默认内存对齐类型,对于malloc分配的内存,其对齐和max_align_t类型的对齐大小应当是一致的,但若对malloc返回的指针进行偏移,就没有办法保证内存对齐。

在 C++11 中,提供了以下内存对齐相关方法:

// alignas 用于指定栈上数据的内存对齐要求
struct alignas(8) testStruct { double data; };

// alignof 和 std::alignment_of 用于得到给定类型的内存对齐要求
std::cout << alignof(std::max_align_t) << std::endl;
std::cout << std::alignment_of<std::max_align_t>::value << std::endl;

// std::align 用于在一大块内存中获取一个符合指定内存要求的地址
char buffer[] = "memory alignment";
void* ptr = buffer;
std::size_t space = sizeof(buffer) - 1;
std::align(alignof(int), sizeof(char), ptr, space);

在 C++17 后,可以使用std::align_val_t来重载需求额外内存对齐的operator new

void* operator new(std::size_t count, std::align_val_t al);

为了将相关对象成簇集中: 如果你知道特定的某个数据结构往往被一起使用,而你又希望在处理这些数据时将“内存页错误(page faults)”的频率降至最低,那么可以考虑为此数据结构创建一个堆,将它们成簇集中在尽可能少的内存页上。一般可以使用 placement new 达成这个目标(见条款 52)。

为了获得非传统的行为: 有时候你会希望operator newoperator delete做编译器版不会做的事情,例如分配和归还共享内存(shared memory),而这些事情只能被 C API 完成,则可以将 C API 封在 C++ 的外壳里,写在定制的 new 和 delete 中。

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

我们在条款 49 中已经提到过一些operator new的规矩,比如内存不足时必须不断调用 new-handler,如果无法供应客户申请的内存,就抛出std::bad_alloc异常。C++ 还有一个奇怪的规定,即使客户需求为0字节,operator new也得返回一个合法的指针,这种看似诡异的行为其实是为了简化语言其他部分。

根据这些规约,我们可以写出非成员函数版本的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的成员函数版本一般只会分配大小刚好为类的大小的内存空间,但是情况并不总是如此,比如假设我们没有为派生类声明其自己的operator new,那么派生类会从基类继承operator new,这就导致派生类可以使用其基类的 new 分配方式,但派生类和基类的大小很多时候是不同的。

处理此情况的最佳做法是将“内存申请量错误”的调用行为改为采用标准的operator new

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

    ...
}

注意在operator new的成员函数版本中我们也不需要检测分配的大小是否为0了,因为在条款 39 中我们提到过,非附属对象必须有非零大小,所以sizeof(Base)无论如何也不能为0。

如果你打算实现operator new[],即所谓的 array new,那么你唯一要做的一件事就是分配一块未加工的原始内存,因为你无法对 array 之内迄今尚未存在的元素对象做任何事情,实际上你甚至无法计算这个 array 将含有多少元素对象。

operator delete的规约更加简单,你需要记住的唯一一件事情就是 C++ 保证 “删除空指针永远安全”

void operator delete(void* rawMemory) noexcept {
    if (rawMemory == 0) return;

    // 归还 rawMemory 所指的内存
}

operator delete的成员函数版本要多做的唯一一件事就是将大小有误的删除行为转交给标准的operator delete

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 所指的内存
}

如果即将被删除的对象派生自某个基类而后者缺少虚析构函数,那么 C++ 传给operator deletesize大小可能不正确,这或许是“为多态基类声明虚析构函数”的一个足够的理由,能作为对条款 7 的补充。

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

placement new 最初的含义指的是“接受一个指针指向对象该被构造之处”的operator new版本,它在标准库中的用途广泛,其中之一是负责在 vector 的未使用空间上创建对象,它的声明如下:

void* operator new(std::size_t, void* pMemory) noexcept;

我们此处要讨论的是广义上的 placement new,即带有附加参数的operator new,例如下面这种:

void* operator new(std::size_t, std::ostream& logStream);

auto pw = new (std::cerr) Widget;

当我们在使用 new 表达式创建对象时,共有两个函数被调用:一个是用以分配内存的operator new,一个是对象的构造函数。假设第一个函数调用成功,而第二个函数却抛出异常,那么会由 C++ runtime 调用operator delete,归还已经分配好的内存。

这一切的前提是 C++ runtime 能够找到operator new对应的operator delete,如果我们使用的是自定义的 placement new,而没有为其准备对应的 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
};

另一个要注意的问题是,由于成员函数的名称会掩盖其外部作用域中的相同名称(见条款 33),所以提供 placement new 会导致无法使用正常版本的operator new

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会掩盖全局版本和继承而得的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;              // 正确

为了避免名称遮掩问题,需要确保以下形式的operator new对于定制类型仍然可用,除非你的意图就是阻止客户使用它们:

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);
    }
};

凡是想以自定义形式扩充标准形式的客户,可以利用继承和using声明式(见条款 33)取得标准形式:

class Widget: public StandardNewDeleteForms{
public:
    using StandardNewDeleteForms::operator new;
    using StandardNewDeleteForms::operator delete;

    static void* operator new(std::size_t size, std::ostream& logStream);
    static void operator detele(std::size_t size, std::ostream& logStream) noexcept;
    ...
};
  • 29
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值