8.5. 方案重构与经验总结

我们刚刚完成了内存分配操作符的重载,绕过了类型系统的保护机制,执行了可能导致对象错位的危险操作,并学会了如何规避这种陷阱。这确实是一次有趣的探索,但敏锐的读者可能会思考这种技巧的代价 —— 特别是在内存消耗方面。

采用“超额分配并在起始处隐藏n值”的方案,每次分配都会比使用端代码实际需求多消耗sizeof(std::max_align_t)字节。对于大对象分配,这个开销或许微不足道;但若是小对象分配,这种开销可能变得不可接受,甚至成为程序内存消耗的主导因素。

回忆第7章内容可知,C++14允许重载接受销毁对象大小参数的operator delete()。这使得在operator new()中隐藏n值的操作变得多余 —— 原本隐藏n值就是为了在operator delete()中检索该值,而现在已不再需要这么做。

既然无需隐藏n值,既能简化实现,又可显著降低内存消耗:

void *operator new(std::size_t n) {
  // 分配 n 个字节(不需要更多空间!)
  void *p = std::malloc(n);

  // 如有必要,表示未能满足后置条件
  if(!p) throw std::bad_alloc{};

  // 通知内存统计器(Accountant)这次内存分配操作
  Accountant::get().take(n);

  // 返回用户请求的内存块的起始位置
  return p;
}

void *operator new[](std::size_t n) {
  // 与上面的 operator new 完全相同
}

void operator delete(void *p, std::size_t n) noexcept {
  // 对空指针执行 delete 是一个空操作
  if(!p) return;

  // 通知内存统计器(Accountant)这次内存释放操作
  Accountant::get().give_back(n);

  // 释放内存
  std::free(p);
}

void operator delete[](void *p, std::size_t n) noexcept {
  // 与上面的 operator delete 完全相同
}

当前这个内存泄漏检测器不仅完全可用,相比之前那个较为简陋的版本而言,更是一次升级优化。