双重删除会发生什么?

34
Obj *op = new Obj;
Obj *op2 = op;
delete op;
delete op2; // What happens here?

当你意外地双重删除时,最糟糕的情况是什么?这有关紧要吗?编译器会报错吗?

7
可以有影响,但编译器不会“抛出错误”。只是最好不要这么做 :) - paulsm4
16
实际上,我认为最可能发生的情况是内存分配器出现问题。这会(a)迫使您花费很长时间来调试程序,因为症状与原因无关,并且(b)在软件中创建了严重的安全漏洞,您的雇主因此而被起诉,输掉官司,破产,导致您的合同被一位名叫克莱夫的人拥有,他按卡车装货的固定价格做清理工作。他会强迫您在离职期间修复日本触手色情角色扮演游戏的错误。 - Steve Jessop
不要也永远不要这样做。这是未定义的行为。在这种情况下,C++的实现可以做任何事情。它可能会真的做一些坏事,比如破坏你的堆,对堆上的其他对象进行任意和奇怪的更改,甚至可能做更糟的事情。如果您使用原始指针,在第一次删除后最好将其设置为null指针,因为删除null指针是安全的,否则请在现代C++中使用智能指针。 - Destructor
6
这是一个很好的学术问题,大家都说“不要这样做”,但我认为OP已经意识到他们不应该这样做了。重要的不是他们是否应该这样做,而是他们想从低级别的角度了解会发生什么,这确实值得知道,以便更好地理解这门语言。 - Petr
8个回答

32

这会导致未定义的行为。任何事情都可能发生。实际上,我想最有可能发生的是运行时崩溃。


8
有一个案例是因为未定义的行为,导致某人的显示器着火。 - Enn Michael
4
我尝试了OP的示例,只是为了看看结果。我没有遇到运行时崩溃。另一方面,我的办公大楼开始呼吸起奶酪来了。请注意:任何事情都有可能发生! - Michael Krebs
2
如果你非常幸运,你只会立即运行时崩溃。更常见的情况是,你会删除一些其他无辜的对象或者破坏已经被重用的内存,从而杀死小狗。(也许在未来某个未知的时间,当你的程序做出一些奇怪的事情时,这种情况可能完全不可检测,而不仅仅是崩溃。)最糟糕的部分是它会级联——如果双重删除确实成功删除了另一个意外的对象,那么当最终该对象达到其有意的删除时,它将成为另一个双重删除,你就杀死了更多的小狗。 - Miral

25

未定义行为。 标准没有做出任何保证。 可能您的操作系统会提供一些保证,例如“您不会破坏其他进程”,但这对您的程序帮助不大。

您的程序可能会崩溃。 您的数据可能会被损坏。 您下一个工资的直接存款可能会取走500万美元。


3
它能修我的车吗? - JobHunter69

21
这是未定义行为,因此实际结果取决于编译器和运行时环境。
在大多数情况下,编译器不会注意到这个问题。但在许多,如果不是大多数的情况下,运行时内存管理库将崩溃。
在底层,任何内存管理器都必须维护有关其分配的每个数据块的一些元数据,以一种允许它从malloc/new返回的指针中查找元数据的方式。通常,这采用位于分配块前的固定偏移的结构形式。该结构可以包含一个 "幻数" - 一个纯粹由机会发生的常数。如果内存管理器在预期位置看到了魔法数字,则它知道提供给free/delete的指针很可能是有效的。如果它没有看到魔法数字,或者看到了一个不同的数字意味着 "最近已经释放了这个指针",则它可以静默地忽略free请求,或者可以打印一个有用的消息并中止程序。任何一种方法都是合法的,并且有正反两方面的论据。
如果内存管理器在元数据块中没有保留魔数,或者没有检查元数据的完整性,则任何事情都可能发生。根据内存管理器的实现方式,结果很可能是一个崩溃而没有有用的消息,要么立即在内存管理器逻辑中,要么稍后在下一次内存管理器尝试分配或释放内存时,或者更久远和更远的将来,当程序的两个不同部分都认为它们拥有相同的内存块时。
让我们试试吧。将代码转换为名为so.cpp的完整程序:
class Obj
{
public:
    int x;
};

int main( int argc, char* argv[] )
{
    Obj *op = new Obj;
    Obj *op2 = op;
    delete op;
    delete op2;

    return 0;
}

编译它 (我在使用Mac OSX 10.6.8上的gcc 4.2.1,但可能因人而异):

russell@Silverback ~: g++ so.cpp

运行它:

russell@Silverback ~: ./a.out
a.out(1965) malloc: *** error for object 0x100100080: pointer being freed was not allocated
*** set a breakpoint in malloc_error_break to debug
Abort trap

看这里,gcc运行时实际上会检测到双重删除并在崩溃之前提供相当有帮助的信息。


如果有可能发生“双重删除”的情况,使用智能指针通常是最好的解决方案。 - paul23
不错的回答,但如果你更加强调未定义行为,我会更加高兴。 - Mooing Duck
3
我很确定其他答案已经涵盖了“未定义行为”的概念。 - Russell Borogove
这个答案的问题在于,在我的电脑上,相同的代码可以正常运行而没有崩溃。 - Jesse Good
3
给你点赞,@Russell,因为你的评论太棒了 ;) 哦,如果你认为 C++ 不是“真正的编程语言”,那么你可以将其添加到你忽略标签的列表中。之后我们会一起用十六进制编辑器编写一些机器码。 - Ben Voigt

8

虽然这是未定义的:

int* a = new int;
delete a;
delete a; // same as your code

这是一个明确定义的内容:

int* a = new int;
delete a;
a = nullptr; // or just NULL or 0 if your compiler doesn't support c++11
delete a; // nothing happens!

我觉得应该发布这篇文章,因为没有人提到它。


1
我想知道为什么编译器在delete之后不会将nullptr分配给指针。这是为了暴露双重删除错误吗?删除后指针的实际值是多少? - Vlad
2
@Vlad 在 delete 之后,指针的值与 delete 之前相同——换句话说,指针内的 地址 仍然相同,只是该地址处的数据现已被释放。(因此,尝试通过解引用指针访问该数据将导致未定义的行为。)编译器不会自动将已删除的指针分配为 nullptr 的原因很可能是历史和性能方面的考虑。 - Enn Michael
1
@EnnMichael:实际上,在删除之后,不能保证存储在用于删除它的指针内部地址或任何指向同一对象的其他指针。它可能与之前相同,也可能更改为nullptr。你不知道,并且根据标准,你无法知道,因为即使从这样的无效指针中读取值(而不进行解引用),也是未定义行为。 - Ben Voigt

4
编译器可能会发出警告或其他提示,特别是在明显的情况下(例如在您的示例中),但它不可能总是检测到。 (您可以使用类似于valgrind的工具,在运行时可以检测到它)。至于行为,它可以是任何东西。一些安全库可能会进行检查,并正确处理 -- 但其他运行时(为了速度)将假定您的调用是正确的(实际上并不是),然后崩溃或更糟。运行时允许假设您没有双重删除(即使双重删除可能会导致一些不良后果,例如崩溃计算机)。

3
每个人都告诉过你不应该这样做,否则会导致未定义的行为。这是众所周知的,因此让我们在更低的层次上详细解释一下,看看实际发生了什么。
标准普遍的答案是任何事情都可能发生,但这并不完全正确。例如,计算机不会因为你这样做而试图杀死你(除非你正在为机器人编写AI):)
之所以没有任何通用答案,是因为这是未定义的,它可能因编译器而异,甚至在同一编译器的不同版本之间也可能存在差异。
但这大致是在大多数情况下会发生的事情:
delete包括两个主要操作:
- 如果已定义,则调用析构函数。 - 它会以某种方式释放分配给对象的内存。
因此,如果您的析构函数包含访问已被删除的类的任何数据的代码,则可能会出现段错误或者(最有可能)读取到一些无意义的数据。如果这些删除的数据是指针,则很可能会出现段错误,因为您将尝试访问包含其他内容或不属于您的内存的内存。
如果您的构造函数不涉及任何数据或不存在(为简单起见,我们不考虑虚拟析构函数),则在大多数编译器实现中,这可能不是崩溃的原因。但是,在这里调用析构函数并不是唯一要发生的操作。
需要释放内存。如何完成取决于编译器的实现,但它也可能执行一些类似于free的函数,为其提供指针和对象的大小。在已经被删除的内存上调用free可能会崩溃,因为该内存可能不再属于您。如果确实属于您,则可能不会立刻崩溃,但它可能会覆盖已分配给程序某个不同对象的内存。
这意味着你的一个或多个内存结构已经损坏,你的程序很快就会崩溃,或者它可能会表现出令人难以置信的怪异行为。您的调试器中不会显然显示原因,您可能需要花费几周的时间来弄清楚到底发生了什么。
因此,正如其他人所说,这通常是一个糟糕的想法,但我想你已经知道了。不过,别担心,即使你两次删除一个对象,无辜的小猫咪也很可能不会死。
以下是错误的示例代码,可能也能正常工作(在linux上使用GCC可以正常工作):
class a {};

int main()
{
    a *test = new a();
    delete test;
    a *test2 = new a();
    delete test;
    return 0;
}

如果我在删除之间不创建该类的中间实例,则会按预期在同一内存上发生2次free调用:
*** Error in `./a.out': double free or corruption (fasttop): 0x000000000111a010 ***

直接回答您的问题:
“最坏的情况是什么”:
理论上,您的程序会导致一些致命后果。在极端情况下,它甚至可能随机尝试擦除您的硬盘。这取决于您的程序是什么(内核驱动程序?用户空间程序?)。
实际上,它很可能只会由于段错误而崩溃。但更糟糕的情况可能会发生。
“编译器是否会抛出错误”?
不应该。

我认为大多数人都知道双重删除会导致意外行为。但是这个答案最好地描述了幕后发生的事情,以及为什么我们实际上会看到由两个删除引起的崩溃。+1 - Nikola Malešević
在某些情况下,它确实可能会清除您的硬盘驱动器:https://kristerw.blogspot.com/2017/09/why-undefined-behavior-may-call-never.html - Jerry Jeremiah

2
不,删除相同指针两次是不安全的。根据C++标准,这是未定义的行为。
从C++ FAQ中可以了解到:访问此链接
例如,以下代码将导致灾难性后果:
class Foo { /*...*/ };
void yourCode()
{
  Foo* p = new Foo();
  delete p;
  delete p;  // DISASTER!
  // ...
}

那个第二个删除p行可能会对您造成很糟糕的影响。它可能会,根据月相的不同,破坏您的堆、崩溃您的程序、对已经存在于堆上的对象进行任意和奇怪的更改等等。不幸的是,这些症状可能会随机出现和消失。根据墨菲定律,在最糟糕的时刻(当客户在看、高价值交易正在尝试发布等等),你会受到最严重的打击。 注意:一些运行时系统将保护您免受某些非常简单的双重删除情况的影响。根据具体情况,如果您恰好在其中一个系统上运行,并且从未有人将您的代码部署到处理方式不同的另一个系统上,并且您要删除的内容没有析构函数,并且在两次删除之间不进行任何重要操作,并且从未有人更改您的代码以在两次删除之间执行重要操作,并且您的线程调度程序(您很可能无法控制!)恰好没有在两次删除之间交换线程,那么您可能会没事。所以回到墨菲:因为它可能出错,所以它会在最糟糕的时刻出错。 一个非崩溃并不能证明错误的不存在;它只是未能证明错误的存在。 相信我:双重删除是不好的,不好的,不好的。拒绝双重删除。

1
在GCC 7.2,Clang 7.0和Zapcc 5.0上,第9次删除会导致程序中止。
#include <iostream>

int main() { 
  int *x = new int;
  for(size_t n = 1;;n++) {
    std::cout << "delete x " << n << " time(s)\n";
    delete x;
  }
  return 0;
}

2
这很美丽。 - Pnelego

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