双重删除中会发生什么

What happens in a double delete?

本文关键字:什么 删除      更新时间:2023-10-16
Obj *op = new Obj;
Obj *op2 = op;
delete op;
delete op2; // What happens here?

当您不小心重复删除时可能发生的最坏情况是什么?有关系吗?编译器会抛出错误吗?

它会导致未定义的行为。 任何事情都可能发生。 在实践中,运行时崩溃可能是我所期望的。

未定义的行为。 该标准没有任何保证。 可能您的操作系统会做出一些保证,例如"您不会损坏另一个进程",但这对您的程序没有多大帮助。

您的程序可能会崩溃。 您的数据可能已损坏。 直接存入您的下一份薪水可能会从您的帐户中扣除 500 万美元。

这是未定义的行为,因此实际结果将根据编译器和运行时环境而有所不同。

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

在后台,任何内存管理器都必须维护有关其分配的每个数据块的一些元数据,以允许它从 malloc/new 返回的指针中查找元数据。通常,这采用在分配块之前具有固定偏移量的结构形式。这种结构可以包含一个"幻数"——一个不太可能纯粹偶然发生的常数。如果内存管理器在预期位置看到幻数,它知道提供给 free/delete 的指针很可能有效。如果它没有看到幻数,或者如果它看到一个表示"此指针最近被释放"的其他数字,它可以静默地忽略免费请求,也可以打印有用的消息并中止。根据规范,两者都是合法的,并且任何一种方法都有赞成/反对的论据。

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

让我们试试吧。将您的代码转换为完整的程序.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;
}

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

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 运行时实际上检测到它是双重删除,并且在崩溃之前相当有帮助。

虽然这是未定义的:

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!

以为我应该发布它,因为没有其他人提到它。

编译器可能会发出警告或其他内容,尤其是在明显的情况下(如您的示例(,但它不可能始终检测到。(您可以使用像valgrind这样的东西,它在运行时可以检测到它(。至于行为,它可以是任何东西。一些安全的库可能会检查并处理它 - 但其他运行时(为了速度(会假设你调用是正确的(事实并非如此(,然后崩溃或更糟。允许运行时假设您没有双重删除(即使双重删除会做一些不好的事情,例如使您的计算机崩溃(

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

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

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

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

delete由 2 个主要操作组成:

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

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

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

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

这意味着您的一个或多个内存结构刚刚损坏,您的程序迟早可能会崩溃,或者它的行为可能非常奇怪。原因在调试器中并不明显,您可能需要花费数周时间弄清楚刚刚发生了什么。

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

这是错误的示例代码,但也可以正常工作(它在 Linux 上的 GCC 上工作正常(:

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

如果我在删除之间没有创建该类的中间实例,则在同一内存上释放的 2 次调用将按预期发生:

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

要直接回答您的问题:

可能发生的最坏情况是什么

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

在实践中,它很可能只是因段错误而崩溃。但更糟糕的事情可能会发生。

编译器会抛出错误吗

不应该。

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

从C++常见问题解答:访问此链接

删除同一指针两次是否安全?
不!(假设您没有从中间的新指针返回该指针。

例如,下面是灾难:

class Foo { /*...*/ };
void yourCode()
{
  Foo* p = new Foo();
  delete p;
  delete p;  // DISASTER!
  // ...
}

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

在 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;
}