为什么要replace默认的新的和删除操作符?

为什么要用一个自定义的newdelete操作符来replace默认的操作符newdelete

这是在重载 C ++ FAQ中重载和删除的延续:
运算符重载。

此常见问题解答的后续条目是:
我应该如何编写符合ISO C ++标准的自定义newdelete操作符?

注意:答案是基于Scott Meyers的“更有效的C ++”的教训。
(注意:这是一个Stack Overflow的C ++常见问题解答的入口,如果你想批评在这个表单中提供FAQ的想法,那么在这个开始所有这些的meta上的贴子将是这个地方的答案。那个问题在C ++聊天室中进行监控,常见问题解决scheme首先出现,所以你的答案很可能会被那些提出这个想法的人阅读)。

人们可能会尝试更换newdelete操作员的原因有很多,即:

检测使用错误:

newdelete不正确的使用可能会导致未定义的行为内存泄漏的可怕的怪物有很多种方法。 各自的例子是:
new编辑的内存上使用多个delete ,而不是在使用new分配的内存上调用delete操作。
重载的操作符new可以保留分配的地址列表,重载的operator delete可以从列表中删除地址,那么很容易检测到这种使用错误。

类似地,各种编程错误可能导致数据溢出 (在分配块的末尾写入)和欠载 (在分配块的开始之前写入)。
重载操作符new可以在客户端提供的内存之前和之后过度分配块并放置已知的字节模式(“签名”)。 重载的操作符删除可以检查签名是否仍然完好无损。 因此,通过检查这些签名是否不完整,可以确定在分配的块的生命周期中某个时间发生溢出或溢出,并且操作员删除可以logging该事实以及有问题的指针的值,从而帮助提供良好的诊断信息。


提高效率(速度和记忆):

newdelete操作员为大家合理地工作,但是为最佳为人。 这种行为是由于它们仅为通用目的而devise的。 他们必须适应分配模式,从计划期间存在的几个块的dynamic分配到大量短期对象的不断分配和重新分配。 最终,编译器附带的operator new和operator delete采取中间策略。

如果你对程序的dynamic内存使用模式有了很好的理解,你可以经常发现operator new和operator delete的自定义版本比缺省版本性能更好(性能更快,或者占用50%的内存)。 当然,除非你确定自己在做什么,否则不是一个好主意(如果你不了解错综复杂的情况,甚至不要去尝试这个)。


收集使用统计信息:

在考虑如#2所提到的为了提高效率而更换newdelete之前,你应该收集关于你的应用程序/程序如何使用dynamic分配的信息。 您可能想收集有关以下内容的信息:
分配块的分配,
一生的分配,
分配顺序(FIFO或LIFO或随机),
了解使用模式在一段时间内的变化,使用的最大dynamic内存量等

另外,有时您可能需要收集使用信息,例如:
计算一个类的dynamic对象的数量,
限制使用dynamic分配等创build的对象的数量

所有这些信息都可以通过replace自定义的newdelete添加,并在重载的newdelete添加诊断收集机制。


为了弥补new内存不匹配:

许多计算机体系结构要求将特定types的数据放置在特定types的地址的存储器中。 例如,体系结构可能要求指针出现在4的倍数(即,四字节alignment)或双倍必须出现在8的倍数(即,八字节alignment)的地址处。 如果不遵循这些约束,可能会在运行时导致硬件exception。 其他体系结构更宽容,并可能允许它在降低性能的同时工作。带有一些编译器的new运算符不保证dynamic分配双精度的八字节alignment方式。 在这种情况下,将默认操作符newreplace为保证八字节alignment的默认操作符可以大大提高程序性能,并且可以取代new操作符和delete操作符。


将相关对象聚集在一起:

如果您知道特定的数据结构通常一起使用,并且希望在处理数据时最大限度地减less页面错误的频率,那么为数据结构创build一个单独的堆是有意义的,这样就可以将它们聚集在一起页面尽可能。 自定义Placement版本的newdelete可以实现这样的集群。


获得非常规行为:

有时你希望操作符new和delete做一些编译器提供的版本不提供的东西。
例如:您可以编写一个自定义操作符delete ,用零覆盖释放的内存,以提高应用程序数据的安全性。

首先,真的有一些不同的newdelete操作符(真的是一个任意数字)。

首先,有::operator new::operator new[]::operator delete::operator delete[] 。 其次,对于任何类X ,都有X::operator newX::operator new[]X::operator deleteX::operator delete[]

在这些之间,比全局操作符重载类特定的操作符更为常见 – 特定类的内存使用遵循足够的特定模式是相当常见的,您可以编写操作符来对默认值进行实质性改进。 在全球范围内准确或专门预测内存使用情况通常要困难得多。

可能还值得一提的是,尽pipeoperator newoperator new[]是相互独立的(对于任何X::operator newX::operator new[] )也是如此,两者的要求没有区别。 一个将被调用来分配一个对象,另一个分配一个对象数组,但是每个对象仍然只是接收到所需要的内存量,并且需要返回一个内存块的地址(至less)。

说到要求,可能值得回顾一下其他的要求1 :全球运营商必须是真正的全球化 – 你不能把一个名字空间放在一个名字空间里面, 或者在一个特定的翻译单位中放一个名字空间。 换句话说,只有两个层次可以发生重载:一个特定于类的重载或一个全局重载。 诸如“命名空间X中的所有类”或“翻译单元Y中的所有分配”之间的中间点不被允许。 特定于类的运算符必须是static – 但实际上并不需要将它们声明为静态的 – 无论您是否显式声明它们,它们都将是静态的。 正式的,全球运营商很多的内存都alignment,所以它可以用于任何types的对象。 从非官方的angular度来看,这里有一个小小的摆动空间:如果你得到一个小块的请求(例如2个字节),你只需要为这个大小的对象提供内存alignment,因为试图存储更大的东西无论如何会导致未定义的行为。

在介绍了这些预备知识之后,我们回到最初的问题, 就是为什么您要重载这些运营商。 首先,我要指出,全球运营商超载的原因往往与特定类运营商超载的原因有很大差异。

由于比较常见,我会首先讨论特定于类的操作符。 特定于类别的内存pipe理的主要原因是性能。 这通常以两种forms(或两种forms)出现:提高速度或减less碎片。 由于内存pipe理器处理特定大小的块,因此速度得到了改善,所以它可以返回任何空闲块的地址,而不是花时间检查块是否足够大,如果块是分块(大多数情况下)以相同的方式减less碎片 – 例如,预先分配足够大的N个对象的块给出N个对象所需的空间; 分配一个对象的内存值将为一个对象分配恰好的空间,而不是多个单独的字节。

全局内存pipe理操作员负担过重的原因有很多种。 其中许多是面向debugging或检测的,例如跟踪应用程序所需的总内存(例如,准备移植到embedded式系统),或者通过显示分配和释放内存之间的不匹配来debugging内存问题。 另一个常见策略是在每个请求块的边界之前和之后分配额外的内存,并在这些区域中写入独特的模式。 在执行结束时(也可能在其他时间),检查这些区域以查看代码是否已写入分配的边界之外。 还有一种是尝试通过自动化至less某些内存分配或删除方面来提高易用性,例如使用自动垃圾收集器 。

非默认的全局分配器可以用来提高性能。 一个典型的例子是replace一个默认的分配器,这个分配器在一般情况下只是很慢(例如,4.x版本的MS VC ++至less有一些版本会为每个分配/删除操作调用系统的HeapAllocHeapFree函数)。 我在实践中看到的另一种可能性是在使用SSE操作时发生在Intel处理器上。 这些操作在128位数据上。 虽然这些操作不pipealignment如何,但是当数据与128位边界alignment时,速度会得到改善。 一些编译器(例如MS VC ++)不一定强制alignment到更大的边界,所以即使使用默认分配器的代码可以工作,replace分配也可以为这些操作提供显着的速度改进。


  1. C ++标准的§3.7.3和§18.4(或C ++ 0x的§3.7.4和§18.6,至lessN3291)覆盖了大部分的要求。
  2. 我不得不指出,我不打算select微软的编译器 – 我怀疑这个问题有不寻常的数目,但是我碰巧使用了很多,所以我倾向于意识到它的问题。

许多计算机体系结构要求将特定types的数据放置在特定types的地址的存储器中。 例如,体系结构可能要求指针出现在4的倍数(即,四字节alignment)或双倍必须出现在8的倍数(即,八字节alignment)的地址处。 如果不遵循这些约束,可能会在运行时导致硬件exception。 其他体系结构更为宽容,可能会让性能下降。

为了澄清:如果一个架构要求例如double数据是八字节alignment的,那么没有什么可以优化的。 任何forms的适当大小的dynamic分配(例如, malloc(size)operator new(size)operator new[](size)new char[size] ,其中size >= sizeof(double) )保证被正确alignment。 如果一个实现没有做出这个保证,那就不符合。 在这种情况下,改变operator new以做“正确的事情”将会是“修复”实施的尝试,而不是优化。

另一方面,一些体系结构允许对一种或多种数据types进行不同(或全部)alignment,但根据这些相同types的alignment性提供不同的性能保证。 然后一个实现可能会返回内存(同样,假设一个合适大小的请求),这个内存是次最佳alignment的,并且仍然是一致的。 这就是这个例子。

带有一些编译器的新运算符不能保证dynamic分配双精度的八字节alignment。

请引用。 通常,默认的new运算符只比malloc包装稍微复杂一些,而标准的malloc包装会返回适合目标体系结构支持的任何数据types的内存。

不是说我没有很好的理由来为自己的类重载新的和删除的东西,而且你在这里触及了几个合法的东西,但是上面的东西不是其中之一。

与使用情况统计信息相关:按子系统进行预算。 例如,在一个基于控制台的游戏中,您可能需要为3D模型几何体预留一部分内存,一些用于纹理,一些用于声音,一些用于游戏脚本等。自定义分配程序可以通过子系统标记每个分配,并发出一个警告当个人预算超出。

我用它来分配特定的共享内存舞台上的对象。 (这与@Russell Borogove提到的类似。)

几年前我为CAVE开发了软件。 这是一个多墙VR系统。 它使用一台电脑来驱动每台投影机; 6是最大(4墙壁,地板和天花板),而3是最常见的(2墙壁和地板)。 机器通过特殊的共享存储器硬件进行通讯

为了支持它,我从我的正常(非CAVE)场景类派生出一个新的“新”,将场景信息直接放在共享内存区域。 然后,我将这个指针传递给不同机器上的从属渲染器。

似乎值得重复从我的回答列表“任何理由重载全球新和删除? 在这里 – 查看答案(或者其他答案 )以获得更详细的讨论,参考和其他原因。 这些原因通常适用于本地运算符重载以及默认/全局运算符,以及C malloc / calloc / realloc / free重载或挂钩。

在我工作的地方,我们重载全局新的和删除操作符的原因很多:

  • 汇集所有小的分配 – 减less开销,减less碎片,可以提高小型应用程序的性能
  • 用已知的生命周期来分配分配 – 直到这个时期结束时,忽略所有的释放,然后把所有的释放放在一起(当然,我们用本地操作符重载来做比全局更多的事情)
  • alignment调整 – caching线边界等
  • alloc fill – 帮助公开使用未初始化的variables
  • 自由填充 – 帮助公开以前删除的内存的使用
  • 免费延迟 – 增加免费补充的效力,偶尔会提高性能
  • 哨兵fenceposts – 帮助揭露缓冲区超支,underruns和偶尔的野生指针
  • redirect分配 – 考虑到NUMA,特殊内存区域,甚至将单独的系统分开存储(例如embedded式脚本语言或DSL)
  • 垃圾收集或清理 – 对于那些embedded式脚本语言来说也是有用的
  • 堆validation – 你可以遍历堆数据结构每N分配/释放,以确保一切看起来不错
  • 会计 ,包括泄漏跟踪使用快照/统计 (堆栈,分配年龄等)