如何将new[]与delete配对可能导致内存泄漏?

29
首先,根据C++标准,对使用new[]分配的任何内容使用delete都是未定义行为。
在Visual C++7中,这种配对可能会导致两种结果之一。
如果new[]分配的类型具有平凡的构造函数和析构函数,VC++会简单地使用new而不是new[],对该块使用delete是可以正常工作的 - new只调用“分配内存”,delete只调用“释放内存”。
如果new[]分配的类型具有非平凡的构造函数或析构函数,则无法使用上述技巧 - VC++7必须调用恰好正确数量的析构函数。因此,它在数组前面添加了一个size_t,存储元素的数量。现在new[]返回的地址指向第一个元素,而不是整个块的开头。因此,如果使用delete,它仅调用第一个元素的析构函数,并以与“分配内存”返回的地址不同的地址调用“释放内存”,这会导致HeapFree()中出现一些错误指示,我怀疑它指的是堆损坏。
然而,偶尔会读到错误的说法,即在new[]后使用delete会导致内存泄漏。我怀疑,任何堆损坏的大小都比只调用第一个元素的析构函数更重要,可能没有调用析构函数释放堆分配的子对象。
在某些C++实现中,如何可能仅通过在new[]后使用delete导致内存泄漏呢?

6
各位回答者:问题是它如何只导致内存泄漏,即它可能如何不会导致堆损坏。 - Thomas
2
非常容易。这完全取决于内存管理的编写方式。由于标准未定义,所有答案都只是猜测(但我确信我可以编写一个不会崩溃堆但会泄漏内存的版本)。内存管理子系统尽可能快速和高效。标准已经给出了一组前置条件和后置条件,可以在这些条件下优化子系统。打破这些条件,就会产生未定义的行为(可能是堆损坏)。在调试中,内存子系统的目标是稳定性而不是速度。因此泄漏更有可能发生。 - Martin York
https://dev59.com/JHI_5IYBdhLWcg3wDOdC#1553407 - sbi
10个回答

31
假设我是一款C++编译器,我的内存管理实现方式如下:在每个预留内存块之前加上该内存块的大小(以字节为单位)。类似于这样:
| size | data ... |
         ^
         pointer returned by new and new[]

请注意,就内存分配而言,newnew[] 没有区别:两者都只是分配一块特定大小的内存块。
那么,delete[] 如何知道数组的大小,以便调用正确数量的析构函数?只需将内存块的 size 除以 sizeof(T),其中 T 是数组元素的类型。
现在假设我将 delete 实现为仅调用一次析构函数,然后释放 size 字节,则后续元素的析构函数将永远不会被调用。这将导致泄漏由后续元素分配的资源。然而,因为我确实释放了 size 字节(而不是 sizeof(T) 字节),所以不会发生堆破坏。

点赞。正如你所说,OP假设new和new[]的处理方式不同,但这可能并非如此。 "new"可能只是在前面加上一个值为1的size_t的"new[]"。 - Merlyn Morgan-Graham
我实际上是想让 size 表示字节数,而不是元素个数。这就像 malloc 函数所做的那样。我会稍微编辑一下我的帖子,以明确这一点。 - Thomas
3
如果使用那种内存管理技术,但是你需要额外开销 x 字节来保存大小,对于小对象增加了 100% 的开销。如果我们想要弥补糟糕的程序员所造成的影响,那么我们可以承担这个代价。但我不想为了支持“sharptooth”而付出这个代价,因此我希望内存管理非常高效(即使对于小类型也是如此)。因此,标准不要求并且大多数实现在发布版本中不会在 new 操作时添加大小信息。尽管一些实现在调试版本中会添加大小信息以方便调试/分析。 - Martin York
1
@Thomas:是的,但这种实现内存管理的方法非常人为、强制性,并且在实践中从未被使用过。它肯定不能作为解释流行的“内存泄漏”传说产生的原因的依据。 - AnT stands with Russia
@MikeCollins,运行时的堆管理系统不是会跟踪所有堆块吗?这意味着块的大小或起始和结束位置应该以某种方式存储,以便堆分配首先基于已分配空间信息发生! - Smart Humanism
显示剩余2条评论

16
关于使用new[]delete混用会导致内存泄漏的传说只是一个童话故事,完全没有实际依据。我不知道它来自于何处,但现在它已经变成了一个独立的存在,并像病毒一样通过口耳相传从一个初学者传播到另一个。
这种无稽之谈的最可能原因是,从天真的角度来看,deletedelete[]之间的区别只是delete用于销毁一个对象,而delete[]则用于销毁一个对象数组("多个"对象)。通常从这种幼稚的结论中得出的一个结论是,delete将销毁数组的第一个元素,而其余部分将保留下来,从而创建所谓的 "内存泄漏"。当然,任何具有至少基本了解典型堆实现的程序员都会立即理解,最可能的结果是堆损坏,而不是 "内存泄漏"。
另一个流行的解释是,由于调用了错误数量的析构函数,数组中的对象所拥有的辅助内存没有被释放。这可能是正确的,但显然是一种非常勉强的解释,在面对堆损坏的更严重问题时几乎没有任何相关性。
简而言之,混合使用不同的分配函数是导致可靠、不可预测和非常实用的未定义行为的错误之一。任何试图对这种未定义行为的表现施加一些具体限制的尝试都只是浪费时间,并且是基本理解缺失的明显标志。
无需多言,new/deletenew[]/delete[]实际上是两种独立的内存管理机制,它们可以独立定制。一旦它们被定制(通过替换原始内存管理函数),就绝对没有办法开始预测如果它们被混合使用会发生什么。

7
看起来你的问题实际上是“为什么不会发生堆损坏?”。答案是“因为堆管理器跟踪分配块的大小”。让我们回到C语言:如果你想在C语言中分配一个整数,你可以这样做:int* p = malloc(sizeof(int));如果你想分配大小为n的数组,你可以写成int* p = malloc(n*sizeof(int))int* p = calloc(n, sizeof(int))。但无论如何,你都会通过free(p)来释放它,无论你如何分配它。你永远不会向free()传递大小,free()只是“知道”要释放多少内存,因为malloc()分配的块的大小保存在块的“前面”的某个地方。回到C++,new/delete和new[]/delete[]通常是基于malloc实现的(虽然不一定是这样,你不应该依赖于它)。这就是为什么new[]/delete组合不会破坏堆的原因——delete会释放正确数量的内存,但正如其他人所解释的那样,如果没有调用正确数量的析构函数,就可能出现泄漏。

话虽如此,在C++中推理未定义的行为总是毫无意义的。如果new[]/delete组合恰好起作用,或者“只是”泄漏或导致堆损坏,这有什么关系呢?你不应该这样编码!在实践中,尽可能避免手动内存管理——STL和boost存在的原因就在于此。


4
如果非平凡析构函数仅对数组中的第一个元素调用,而其他元素则未被调用,则如果这些对象释放了一些内存,则会出现内存泄漏,因为这些对象没有得到适当的清理。

3
除了导致未定义的行为之外,泄漏的最直接原因在于实现未调用数组中除第一个对象以外的所有对象的析构函数。如果这些对象已经分配了资源,则显然会导致泄漏。
这是我能想到的导致此行为的最简单的类:
 struct A { 
       char* ch;
       A(): ch( new char ){}
       ~A(){ delete ch; }
    };

A* as = new A[10]; // ten times the A::ch pointer is allocated

delete as; // only one of the A::ch pointers is freed.

PS:请注意,在许多其他编程错误中,构造函数也会失败调用:非虚基类析构函数,过分依赖智能指针等。


@Suma:我在这里试图展示的问题是只有第一个对象的析构函数被调用,导致了9个泄漏的块,其中包含1个char。你对于A元素数组的观点是正确的,但那不是问题的关键。 - xtofl
@Suma:指出解释有点隐晦并没有什么不好。感谢您的批评,我们需要这样的反馈! - xtofl

3

如果析构函数释放内存,那么这将导致所有C++实现中的泄漏问题,因为析构函数从未被调用。

在某些情况下,它可能会引起更严重的错误。


1
一个专业的答案应该引用参考资料。例如,S.Meyers 的 Effective C++ 第5条:“如果您使用 [] …会发生什么?结果是未定义的…即使对于内置类型也是如此…规则很简单:如果您在调用 new 时使用 [],则调用 delete 时必须使用 []。如果您在调用 new 时没有使用 [],则调用 delete 时不要使用 []。” - Valentin H

3

如果重载了new()操作符但没有重载new[],那么可能会发生内存泄漏。同样的,delete/delete[]操作符也是如此。


3
晚了一步,但是...
如果你的删除机制仅仅是调用析构函数并将被释放的指针与由sizeof所隐含的大小一起放到一个自由栈上,那么在使用new[]分配的内存块上调用delete将导致内存丢失--但不会出现损坏。更复杂的malloc结构可能会在这种行为上出现损坏或检测到它。

2

为什么答案不能是两者都导致呢?

显然,无论是否发生堆损坏,内存都会泄漏。

或者说,既然我可以重新实现new和delete......它不可能完全没有任何影响。从技术上讲,我可以让new和delete执行new[]和delete[]。

因此:未定义行为。


是的,它可以同时引起两者,但在我看来,一旦出现堆损坏,您就不再关心内存泄漏了。 - sharptooth
1
问题的关键在于它是未定义的。绝大多数编译器的已知答案并不重要。假如恰好有一款编译器以完全相同的方式实现new和new[],delete和delete[],那么就永远不会发生堆损坏。因此,对于这个问题的答案是,如果编译器实现的方式避免了堆损坏,那么它可能只会导致内存泄漏。除非编译器采取了同时避免两者的方式。因此,回答这样的问题是没有意义的,除非我们参考特定的编译器。 - Lee Louviere

1
我正在回答一个被标记为重复的问题,因此我将在此处复制它以防万一有所用处。早在我之前就已经说过了内存分配的工作方式,我只是解释一下原因和影响。
从谷歌上找到了一个小东西:http://en.cppreference.com/w/cpp/memory/new/operator_delete 无论如何,delete是用于单个对象的函数。它释放指针中的实例,然后离开;
delete[]是用于释放数组的函数。也就是说,它不仅会释放指针,还会声明该数组的整个内存块为垃圾。
这在实践中非常酷,但你告诉我你的应用程序可行。你可能想知道...为什么?
解决方案是C++不能修复内存泄漏问题。如果您使用不带括号的delete,则只会将数组作为对象删除——这可能会导致内存泄漏。
很酷的故事,内存泄漏,我为什么要在意?
内存泄漏是指分配的内存没有被释放。这些内存占用了不必要的磁盘空间,这样你就会失去有用的内存,而且没有任何理由。这是一种不好的编程方式,你应该在你的系统中修复它。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接