《Effective C++》:条款50:了解new和delete的合理替换时机

有人会想要替换掉编译器提供的operator new或operator delete,因为

  • 用来检测运用上的错误。如果delete new的内存失败,会导致内存泄漏。如果在new所得内存多次delete会导致不确定行为。使用编译器提供的operator new和operator delete不能检测上述行为。如果operator new持有一个链表,其存储动态分配所得内存,operator delete则将内存从链表删除,这样就能呢检测上述错误用法。如果编程错误,可能在分配内存的之前区域或之后区域写入数据;这时可以自己定义operator new分配超额内存,在多出部分写上特定byte patterns(即签名,signature),自己定义operator delete检测签名是否更改。
  • 为了强化效能。operator new和operator delete如果开辟大内存、小内存,持续这样做会造成内存碎片,这在服务器的后台程序上,可能会导致无法满足大区快内存需求,即使有足够但分散的小区块自由内存。使用自己定制的operator new和operator delete可以避免这样的问题。针对特定的需求,有时还可以提升性能。
  • 为收集使用上的统计数据。在定制operator new和operator delete之前,应该首先了解软件如何使用动态内存。分配区块如何分布?寿命如何?它们是FIFO先进先出还是LIFO后进先出,或随机分配和归还?软件在不同执行阶段有不同的分配归还形态吗?任何时刻使用的最大动态分配量是多少?自己定义的operator new和operator delete可以轻松收集到这些信息。

写个定制的operator new和operator delete并不难。例如,写个global operator new,用于检测在分配区块的后面或前面写入数据。下面是个初步版本,有小错误,后面在完善。

static const int signature=0xDEADBEEF;
typedef unsigned char Byte;
//下面代码有些小错误
void* operator new(std::size_t size) throw(std::bad_alloc)
{
    using namespace std;
    size_t realSize=size+2*sizeof(int);//增加大小,塞入两个sinature

    void* pMem=malloc(realSize);
    if(!pMem) throw bad_alloc();

    //将signarure写入内存最前后最后
    *(static_cast<int*>(pMem))=signarure;
    *(reinterpret_cast<int*>(static_cast<Byte*>(pMem)+realSize-sizeof(int)))=signature;

    return static_cast<Byte*>(pMem)+sizeof(int);
}

暂且忽略之前所说的operator new内应该有个循环,反复调用new-handling。来说一下另外一个主题:对齐(alignment)。

许多计算机体系结构要求特定的类型必须放在特定的内存地址上。例如可能是指针的地址必须是4的倍数(four-byte aligned)或double的地址是8的倍数(eight-byte aligned)。没有这些约束可能会导致运行期硬件异常。有些体系结构要求没这么严格,没有字节对齐不会导运行效率低下。

C++要求所有operator new返回的指针都有适当的对齐(取决于数据类型)。malloc就是在这样的要求下工作。所以令operator new返回一个得自malloc的指针是安全的。但是上面实现中,我们偏移了一个int的大小,就不能保证其安全了。例如,如果返回double指针,就不是8字节对齐了。

像对齐这类技术细节,可以区分内存管理器的质量。写一个能够运行的内存管理器并不难,难的是让它总是能够高效优良的运作。一般来说,若非必要,不要去写内存管理器。

很多时候也是非必要的。有些编译器已经在它们的内存管理函数中切换至调试状态(enable debugging)和志记状态(logging)。许多平台上有商业产品可以代替编译器自带的内存管理器,可以用它们来提高机能和改善效率。

另外一个选择是开源领域中的内存管理器。它们对许多平台都可以用。Boost程序库(条款 55)的Pool就是这样的一个分配器,它对常见的分配大量小内存很有帮助。一些小型开源内存分配器大多都不完整,缺少移植、线程安全、对齐等考虑。

本条款是在探讨何时需要在全局性的活class专属的基础上合理替换掉缺省的new和delete,前面说到了3点。这里继续。

  • 为了增加分配和归还的速度。使用定制的针对特定类型对象的分配器,可以提高效率。例如,Boost提供的Pool程序库便是。如果在单线程程序中,你的编译器所带的内存管理具备线程安全,你可以写个不具备线程安全的分配器而大幅度改善速度。
  • 为了降低缺省内存管理器带来的空间额外开销。泛用型分配器往往(虽然并非总是)不只比定制型慢,还使用更多空间,因为它们常常在每一个分配区块上招引某些额外开销。针对小型对象开放的分配器,例如Boost库的Pool,本质上消除了这样的额外开销。
  • 为了弥补缺省分配器的非最佳对齐(suboptimal alignment)。X86体系结构上的double访问最快–如果它们是8-byte对齐。但是编译器自带的operator new并不保证分配double是8-byte对齐。
  • 为了将相关对象成簇集中。如果特定的某个数据结构往往被一起使用,我们希望在处理这些数据时将“内存页错误”(page faults)的频率降至最低,那么为此数据结构创建另一个heap就有意义,这样就可以将它们成簇集中到尽可能少的内存也上。
  • 为了获得非传统的行为。有时候我们需要做operator new和delete没做的事。例如,在归还内存时将其数据覆盖为0,以此增加应用程序的数据安全。

总结

  • 有许多理由需要写个自定的new和delete,包括改善效能、对heap运用错误进行调试、收集heap使用信息。
时间: 2024-11-06 07:08:03

《Effective C++》:条款50:了解new和delete的合理替换时机的相关文章

Effective C++ 条款50 了解new和delete的合理替换时机

1. 替换标准库提供的operator new或operator delete通常基于以下三个理由: 1). 用来检测运行上的错误.将"new 所得内存"delete掉却不幸失败会导致内存泄露,多次对同一块"new所得内存"施行delete会导致未定义行为,如果让operator new持有一串动态分配所得地址,而operator delete将地址从中移走,就可以很容易检测出上述错误;各式各样的变成错误会导致数据"overruns"(写入点在分

Effective C++ 条款50

了解new和delete的合理替换时机 如题所述,我们在本节中只是了解一下什么时候需要自己写new和delete,为什么要重新写new和delete,出于什么样的动机等等. 本文重在论述,至于作者提供的程序代码也具有漏洞,本节的目的就是对new和delete有一个宏观的认识. 下面是替换的原因: 1.用来检测运用上的错误.如果delete new的内存失败,会导致内存泄漏.如果在new所得内存多次delete会导致不确定行为.使用编译器提供的operator new和operator delet

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

(一) 为什么有人想要替换operator new 和 operator delete呢?三个常见的理由: (1)用来检测运用上的错误. (2)为了强化效果. (3)为了收集使用上的统计数据. (二) 下面是个快速发展得出的初阶段global operator new,促进并协助检测"overruns"或"underruns". static const int signature = 0xDEADBEEF; typedef unsigned char Byte;

Effective C++ 条款51 编写new和delete时需固守常规

1. 实现定制的operator new和operator delete需要满足一定的要求. 以operator new而言:实现一致性operator new必须返回正确的值;内存不足时必得调用new-handling函数;必须有对付零内存需求的准备;需避免不慎掩盖正常形式的new;如果有能力供应客户申请的内存,就返回一个指针指向该内存,反之就遵循条款49的规则并抛出bad_alloc异常;应该内含一个无限循环,知道成功分配内存或new-handling完成其功能...... 以上的要求中"必

Effective C++ 条款15、16 在资源管理类中提供对原始资源的访问||成对使用new 与 delete要采取相同形式

1.在资源管理类中提供对原始资源的访问     前几个条款很棒,它们是对抗资源泄露的壁垒,但很多APIs直接指向 资源,这个时候,我们需要直接访问原始资源.     这里,有两种方法解决上述问题,我们可将RAII对象转换为原始资源.通过 显式转换与隐式转换.     通常,tr1:: shared_ptr 和 auto_ptr 都提供一个get成员函数,用来执行显式转换,也就是返回智能指针内部的原始指针的复件.因为它也重载了指针取值操作符* –>.当然也可以通过隐式转换为底部原始指针.     

More Effective C++ 条款35 让自己习惯于标准C++ 语言

(由于本书出版于1996年,因此当时的新特性现在来说可能已经习以为常,但现在重新了解反而会起到了解C++变迁的作用) 1. 1990年后C++的重要改变 1). 增加了新的语言特性:RTTI,namespaces,bool,关键词mutable和explicit,enums作为重载函数之自变量所引发的类型晋升转换,以及"在class 定义区内直接为整数型(intergral) const static class members设定初值"的能力. 2). 扩充了Templates的特性

More Effective C++ 条款34 如何在一个程序中结合C++和C

1. C++和C混合使用的前提之一就是编译器产生兼容的目标文件(.lib和.dll等).所谓"兼容",指的是编译器在"预编译器相依的特性上"一致,如int和double大小,参数压栈机制等,只有在这个基础上才能讨论结合使用C++和C模块的问题. 2. 在1的基础上,要结合使用C++和C的模块,主要有以下几点需要注意: 1). name mangling(名称重整) Name mangling是C++用于支持函数重载的机制,它对重载的函数名称进行一定改变,使得每个函数

effective c++ 条款13 use object to manage resources.

请求的系统资源需要最终还回系统,为了避免遗忘返还这个动作,可以利用析构函数在object销毁时自动调用的特点来实现. 简单说就是用object来管理资源. 以内存资源为例 class Investment {}; Investment* creatInvestment(){...} // factory function to produce investment object void main() { Investment* pInv = creatInvestment();//call t

Effective C++ 条款11,12 在operator= 中处理&ldquo;自我赋值&rdquo; || 复制对象时不要忘记每一个成分

1.潜在的自我赋值     a[i] = a[j];     *px = *py; 当两个对象来自同一个继承体系时,他们甚至不需要声明为相同类型就可能造成别名. 现在担心的问题是:假如指向同一个对象,当其中一个对象被删,另一个也被删,这会造成不想要的结果. 该怎么办? 比如:   widget& widget:: operator+ (const widget& rhs) {    delete pd;    pd = new bitmap(*rhs.pb);    return *thi