关于c ++:双删除会发生什么?

What happens in a double delete?

1
2
3
4
Obj *op = new Obj;
Obj *op2 = op;
delete op;
delete op2; // What happens here?

当您意外地双重删除时,最糟糕的情况是什么?重要吗?编译器会抛出错误吗?


未定义的行为。本标准不作任何保证。也许你的操作系统会做出一些保证,比如"你不会破坏另一个进程",但这对你的程序没有多大帮助。

你的程序可能会崩溃。您的数据可能已损坏。下一份薪水支票的直接存款可以从你的帐户中取出500万美元。


它会导致不明确的行为。任何事情都可能发生。实际上,运行时崩溃可能是我所期望的。


它是未定义的行为,因此实际结果将根据编译器运行时环境而变化。

在大多数情况下,编译器不会注意到。在许多情况下,如果不是大多数情况,运行时内存管理库将崩溃。

在引擎盖下,任何内存管理器都必须维护它所分配的每个数据块的一些元数据,这种方式允许它从malloc/new返回的指针中查找元数据。通常情况下,这是在分配的块之前以固定偏移量的结构形式出现的。这个结构可以包含一个"幻数"——一个不太可能纯粹偶然发生的常数。如果内存管理器在预期位置看到幻数,它知道提供给free/delete的指针最有可能有效。如果它看不到幻数,或者它看到一个不同的数字,这意味着"这个指针最近被释放了",它可以静默地忽略自由请求,或者它可以打印一条有用的消息并中止。根据规范,这两种方法都是合法的,并且对这两种方法都有赞成/反对的理由。

如果内存管理器没有在元数据块中保留幻数,或者没有检查元数据的健全性,那么任何事情都可能发生。根据内存管理器的实现方式,结果很可能是一个没有有用消息的崩溃,要么立即出现在内存管理器逻辑中,在下次内存管理器尝试分配或释放内存时稍晚一些,或者在程序的两个不同部分都认为它们拥有一块内存。

让我们试试看。在so.cpp中将代码转换为完整的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Obj
{
public:
    int x;
};

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

    return 0;
}

编译它(我在OSX 10.6.8上使用gcc 4.2.1,但使用ymmv):

1
russell@Silverback ~: g++ so.cpp

运行它:

1
2
3
4
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运行时实际上检测到它是一个双重删除,并且在崩溃之前非常有用。


编译器可能会给出警告或其他东西,特别是在明显的地方(如您的示例中),但它不可能总是检测到。(您可以使用Valgrind之类的工具,在运行时可以检测到它)。至于行为,它可以是任何东西。一些安全的库可能会进行检查,并且处理得很好——但是其他运行时(对于速度)会使您所调用的假设是正确的(而不是正确的),然后崩溃或更糟。运行时可以假定您没有重复删除(即使重复删除会造成一些不好的情况,例如,计算机崩溃)。


每个人都已经告诉过你不应该这样做,这会导致不明确的行为。这是众所周知的,所以让我们在一个较低的层次上详细阐述这一点,让我们看看实际发生了什么。

标准的普遍答案是任何事情都有可能发生,这并不完全正确。例如,计算机不会因为这样做而试图杀死你(除非你正在为机器人编程人工智能):)

之所以没有任何通用答案,是因为这是未定义的,它可能因编译器而异,甚至在同一编译器的不同版本之间也有所不同。

但在大多数情况下,这就是"大致"发生的情况:

delete由两个主要操作组成:

  • 如果定义了析构函数,
  • 它以某种方式释放分配给对象的内存

因此,如果析构函数包含访问已删除类的任何数据的任何代码,那么它可能会出错,或者(很可能)您将读取一些无意义的数据。如果这些删除的数据是指针,那么它很可能是segfault,因为您将尝试访问包含其他内容或不属于您的内存。

如果您的构造函数不接触任何数据或不存在(为了简单起见,这里不考虑虚拟析构函数),那么它可能不是大多数编译器实现崩溃的原因。然而,调用析构函数并不是这里将要发生的唯一操作。

内存必须是空闲的。它是如何完成的取决于编译器中的实现,但它也可以执行一些类似于free的函数,给它对象的指针和大小。在已删除的内存上调用free可能会崩溃,因为该内存可能不再属于您。如果它确实属于您,它可能不会立即崩溃,但它可能会覆盖已经为程序的某个不同对象分配的内存。

这意味着您的一个或多个内存结构刚刚损坏,您的程序可能早晚崩溃,或者它的行为可能异常怪异。原因在你的调试程序中并不明显,你可能会花几个星期的时间弄清楚到底发生了什么。

所以,正如其他人所说,这通常是个坏主意,但我想你已经知道了。不过别担心,如果你删除一个对象两次,无辜的小猫很可能不会死。

下面是错误的示例代码,但也可以很好地工作(它在Linux上与gcc一起工作正常):

1
2
3
4
5
6
7
8
9
10
class a {};

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

如果在两次删除之间不创建该类的中间实例,则会在同一内存上调用2次释放,如预期的那样:

1
*** Error in `./a.out': double free or corruption (fasttop): 0x000000000111a010 ***

要直接回答您的问题:

最糟糕的情况是:

理论上,你的程序会导致一些致命的事情。在某些极端情况下,它甚至可能随机尝试擦除硬盘。机会取决于你的程序实际是什么(内核驱动程序?用户空间计划?).

实际上,它很可能只是与SegFault崩溃。但更糟的事情可能会发生。

编译器会抛出错误吗?

它不应该。


不,删除同一指针两次是不安全的。它是根据C++标准的未定义行为。

从C++常见问题:访问这个链接

删除同一指针两次安全吗?不!(假设你没有从中间的new得到指针。)

例如,以下是一场灾难:

1
2
3
4
5
6
7
8
class Foo { /*...*/ };
void yourCode()
{
  Foo* p = new Foo();
  delete p;
  delete p;  // DISASTER!
  // ...
}

第二条删除P行可能会对您造成一些非常坏的影响。它可能会破坏你的堆,使你的程序崩溃,对堆中已经存在的物体进行任意和奇异的改变,这取决于月球的相位。不幸的是,这些症状可能会随机出现和消失。根据墨菲定律,你将在最糟糕的时刻受到最沉重的打击(当客户在寻找时,当一笔高价值的交易试图过账时,等等)。注意:一些运行时系统将保护您免受某些非常简单的双重删除情况的影响。根据具体情况,如果您碰巧在其中一个系统上运行,并且没有人将您的代码部署到另一个处理方式不同的系统上,并且如果您正在删除没有析构函数的内容,并且如果您在两个删除之间没有做任何重要的操作,并且没有人更改您的在两次删除之间以及线程调度程序(您可能无法控制)之间执行重要操作的代码。不会在两个删除和if、if和if之间交换线程。所以回到墨菲:因为它可能会出错,它会,在最糟糕的时刻也会出错。非崩溃并不能证明没有错误;它只是不能证明存在错误。相信我:双重删除是坏的,坏的,坏的。只说不。


未定义时:

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

这是一个很好的定义:

1
2
3
4
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!

我想我应该把它贴出来,因为没人提到它。