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

为什么 应该要用定制的 newdelete操作符来替换默认操作符 newdelete

这是 重载新建并删除在极具启发性的 C + + 常见问题解答中的延续:
运算符重载

这个常见问题的后续条目是:
如何编写符合 ISO C + + 标准的自定义 newdelete操作符?

注意: 答案来自 Scott Meyers 的《更有效的 C + + 》。 (注意: 这是“堆栈溢出的 C + + 常见问题解答”( https://stackoverflow.com/questions/tagged/c++-faq )的一个条目。如果你想评论一下在这种形式下提供常见问题解答的想法,那么【在 meta 上发布这些问题的 https://meta.stackexchange.com/questions/68647/setting-up-a-FAQ-for-the-c-tag 】就是一个不错的选择。这个问题的答案在[ c + + 聊天室]( https://chat.stackoverflow.com/rooms/10/c-lounge )中进行监控,FAQ 的想法最初就是在这里出现的,所以你的答案很可能会被想出这个想法的人看到。)我不知道
18820 次浏览

人们可能由于以下原因而试图取代 newdelete操作员:

检测使用错误:

newdelete的不正确使用可能会导致 未定义行为内存泄漏的可怕的野兽。 每个例子分别是:
newed 内存上使用多个 delete,不在使用 new分配的内存上调用 delete
重载操作符 new可以保存已分配地址的列表,重载操作符 delete可以从列表中删除地址,这样就很容易检测到这种使用错误。

类似地,各种各样的编程错误可能导致 数据溢出(写入超过已分配块的末尾)和 跑偏了(在已分配块的开始之前写入)。
Overload 操作符 new可以在客户端可用的内存之前和之后过度分配块并放置已知的字节模式(“签名”)。重载操作符删除可以检查签名是否仍然完整。 因此,通过检查这些签名是否完整,可以确定在所分配的块的生命周期中的某个时候发生了溢出或欠运行,运算符 delete 可以记录这一事实以及违规指针的值,从而帮助提供良好的诊断信息。


提高效率(速度和内存) :

newdelete操作符适用于所有人,但最适用于任何人。这种行为产生于这样一个事实,即它们仅被设计用于通用目的。它们必须适应各种分配模式,从程序持续期间存在的几个块的动态分配到大量短寿命对象的常量分配和释放。最终,与编译器一起运行的操作员 new和操作员 delete采取中间策略。

如果您对程序的动态内存使用模式有很好的了解,您通常会发现自定义版本的 new 操作符和操作符 delete 的性能优于默认版本(性能更快,或者需要更少的内存,最多可达50%)。当然,除非你确定自己在做什么,否则这样做不是一个好主意(如果你不了解其中的复杂性,就不要尝试这样做)。


收集使用情况统计数字:

在考虑替换 newdelete以提高效率之前,您应该收集有关应用程序/程序如何使用动态分配的信息。你可能需要收集以下方面的信息:
分配块的分布,
寿命分布,
分配顺序(FIFO 或 LIFO 或随机) ,
了解使用模式在一段时间内的变化,最大动态内存使用量等。

此外,有时你可能需要收集使用信息,例如:
计算一个类的动态对象的数量,
限制使用动态分配创建的对象数量等。

所有这些信息都可以通过替换定制的 newdelete并在重载的 newdelete中添加诊断收集机制来收集。


为了补偿 new中的次优内存对齐:

许多计算机体系结构要求将特定类型的数据放在特定类型地址的内存中。例如,体系结构可能要求指针出现在4的倍数(即4字节对齐)的地址上,或者双精度指针必须出现在8的倍数(即8字节对齐)的地址上。不遵守这些约束可能会导致运行时硬件异常。其他体系结构更加宽容,可能允许它在降低性能的情况下工作。某些编译器附带的操作符 new不能保证动态对齐的8字节对齐 双打分配。在这种情况下,用一个能够保证8字节对齐的操作符取代默认的 new可以大大提高程序性能,并且可以成为替换 newdelete操作符的一个很好的理由。


把相关的物体聚集在一起:

如果您知道特定的数据结构通常一起使用,并且希望在处理数据时尽量减少页面错误的频率,那么为数据结构创建一个单独的堆,以便将它们集中在尽可能少的页面上是有意义的。newdelete的自定义布局版本可以实现这样的集群。


获得非常规行为:

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

许多计算机体系结构要求将特定类型的数据放在特定类型地址的内存中。例如,体系结构可能要求指针出现在4的倍数(即4字节对齐)的地址上,或者双精度指针必须出现在8的倍数(即8字节对齐)的地址上。不遵守这些约束可能会导致运行时硬件异常。其他体系结构更加宽容,可能允许它在降低性能的情况下工作。

澄清一下: 例如,如果一个体系结构 需要double数据是8字节对齐的,那么就没有什么需要优化的了。任何一种适当大小的动态分配(例如 malloc(size)operator new(size)operator new[](size)new char[size],其中 size >= sizeof(double))都能保证正确对齐。如果一个实现没有做出这样的保证,那么它就不符合。在这种情况下,改变 operator new来做“正确的事情”将是一种“修复”实现的尝试,而不是一种优化。

另一方面,一些体系结构允许对一个或多个数据类型进行不同(或所有)类型的对齐,但是根据这些相同类型的对齐情况提供不同的性能保证。然后,实现可能返回对齐次优的内存(同样,假设有适当大小的请求) ,并且仍然保持一致。这就是这个例子的内容。

首先,实际上有许多不同的 newdelete操作符(实际上是任意数字)。

第一,有 ::operator new::operator new[]::operator delete::operator delete[]。第二,对于任何类别的 X,都有 X::operator newX::operator new[]X::operator deleteX::operator delete[]

在这两者之间,重载特定于类的操作符比全局操作符更为常见——特定类的内存使用遵循特定的足够模式,以便可以编写相对于默认值提供实质性改进的操作符,这种情况相当常见。一般来说,在全球范围内准确或具体地预测内存使用要困难得多。

值得一提的是,尽管 operator newoperator new[]彼此独立(对于任何 X::operator newX::operator new[]来说也是如此) ,但是两者的需求没有区别。一个被调用来分配一个对象,另一个被调用来分配一个对象数组,但是每个对象仍然只是接收需要的内存量,并且需要返回(至少)那么大的内存块的地址。

说到需求,可能值得回顾一下其他需求: 全局操作符必须是真正的全局操作符——您可能不会将其放在名称空间 或者中,而将其静态放在特定的翻译单元中。换句话说,只有两个级别可以发生重载: 类特定的重载或全局重载。中间点,如“名称空间 X 中的所有类”或“转换单元 Y 中的所有分配”是不允许的。特定于类的操作符必须是 static——但实际上并不需要将它们声明为 static ——无论是否显式声明它们,它们都是静态的 威尔。在正式情况下,全局运算符会返回对齐的内存,以便可以用于任何类型的对象。在非官方的情况下,有一个小的回旋余地,如果你得到一个小块的请求(例如,2字节) ,你只需要提供内存对齐的对象达到这个大小,因为尝试存储任何更大的东西都会导致未定义行为。

在讨论了这些初步问题之后,让我们回到关于 为什么的最初问题,您希望让这些运算符超载。首先,我应该指出,重载全局运算符的原因往往与重载特定于类的运算符的原因大不相同。

由于它更常见,我将首先讨论特定于类的运算符。特定于类的内存管理的主要原因是性能。这通常有两种形式: 提高速度或减少碎片化。内存管理器将 只有处理特定大小的块,从而提高了速度,因此它可以返回任何空闲块的地址,而不必花费任何时间检查一个块是否足够大,如果一个块太大,则将其一分为二,等等。分段的减少方式(大多数情况下)是相同的——例如,预先分配一个足够大的块以容纳 N 个对象,这样就给出了 N 个对象所需要的空间; 分配一个对象的内存值将为一个对象分配 没错的空间,而不会多分配一个字节。

导致全局内存管理操作符超载的原因要多得多。其中许多是面向调试或检测的,比如跟踪应用程序所需的总内存(例如,准备移植到嵌入式系统) ,或者通过显示分配和释放内存之间的不匹配来调试内存问题。另一个常见的策略是在每个请求块的边界之前和之后分配额外的内存,并将唯一的模式写入这些区域。在执行结束时(可能在其他时候也是如此) ,将检查这些区域,以确定代码是否已编写到分配的边界之外。还有一种方法是尝试通过自动化内存分配或删除的至少一些方面来提高易用性,例如使用 自动垃圾收集器自动垃圾收集器

使用非默认的全局分配器 可以也可以提高性能。一个典型的例子就是替换一个默认的分配器,这个默认的分配器一般都比较慢(例如,至少有一些版本的 MS VC + + 在4.x 左右会调用系统 HeapAllocHeapFree函数来执行 每个分配/删除操作)。我在实践中看到的另一种可能性发生在使用 SSE 操作的 Intel 处理器上。它们对128位数据进行操作。虽然无论对齐方式如何,操作都会正常工作,但当数据对齐到128位边界时,速度会得到提高。一些编译器(例如,MS VC + + 再次 2)不一定强制对那个更大的边界进行对齐,所以即使使用默认分配器的代码可以工作,替换分配可以为这些操作提供实质性的速度改进。


  1. 大多数需求都涵盖在 C + + 标准的3.7.3和18.4中(或者 C + + 0x 中的3.7.4和18.6,至少在 N3291中是这样)。< br >
  2. 我觉得有必要指出,我并不打算挑剔微软的编译器——我怀疑它有很多这样的问题,但我碰巧经常使用它,所以我倾向于相当清楚它的问题。

与使用统计有关的: 按子系统分列的预算编制。例如,在一个基于控制台的游戏中,您可能需要为3D 模型几何保留一部分内存,一部分用于纹理,一部分用于声音,一部分用于游戏脚本,等等。自定义分配器可以按子系统标记每个分配,并在超出单个预算时发出警告。

一些编译器提供的新操作符不能保证双精度浮点数动态分配的8字节对齐。

请给我传票。通常,默认的 new 操作符只比 malloc 包装器稍微复杂一点,根据标准,malloc 包装器返回适合目标体系结构支持的 任何数据类型的对齐内存。

我并不是说没有充分的理由为自己的类重载 new 并删除它们... ... 在这里您已经提到了几个合法的类,但是上面并不是其中之一。

我用它来在一个特定的共享内存领域分配对象(这与@Russell Borogove 提到的类似)

几年前我开发了 洞穴的软件。这是个多壁虚拟现实系统。它使用一台计算机来驱动每个投影仪; 6是最大值(4面墙,地板和天花板) ,而3是更常见的(2面墙和地板)。机器通过特殊的共享内存硬件进行通信。

为了支持它,我从我的正常(非 CAVE)场景类派生使用一个新的“新”,把场景信息直接放在共享内存领域。然后,我将该指针传递给不同计算机上的从呈现程序。

这里似乎值得重复 我的回答是“有什么理由让全局新建和删除超载吗?”的列表——有关更详细的讨论、参考文献和其他原因,请参阅该答案(或实际上是 这个问题的其他答案)。这些原因通常适用于本地操作符重载以及缺省/全局重载,也适用于 Cmalloc/calloc/realloc/free重载或挂钩。

在我为许多人工作的地方,我们使全局的 new 和 delete 操作符超载 原因:

  • 所有小分配-减少开销,减少碎片,可以提高小分配重的应用程序的性能
  • 具有已知生命周期的 分配——忽略所有的空闲,直到这个周期的最后,然后释放所有的空闲 一起(不可否认,我们更多地使用本地操作符重载 全球)
  • 对齐 调整——到缓冲线边界等
  • Alloc fill ——帮助公开未初始化变量的使用
  • 空闲填充 ——帮助公开以前删除的内存的使用情况
  • 延迟自由 -增加自由填充的有效性,偶尔提高性能
  • 哨兵 围栏——帮助暴露缓冲区溢出、溢出和偶尔的野生指针
  • 重定向 分配——以考虑 NUMA、特殊内存区域,甚至在内存中保持独立的系统(例如。 嵌入式脚本语言或 DSL)
  • 垃圾收集 或清理——对于那些嵌入式脚本语言同样有用
  • 堆验证 ——您可以遍历堆数据结构,每个 N 分配/释放一次,以确保一切正常
  • 会计 ,包括 泄漏追踪使用情况快照/统计数字(堆栈、分配年龄等)