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之类的工具,在运行时可以检测到它)。至于行为,它可以是任何东西。一些安全的库可能会进行检查,并且处理得很好——但是其他运行时(对于速度)会使您所调用的假设是正确的(而不是正确的),然后崩溃或更糟。运行时可以假定您没有重复删除(即使重复删除会造成一些不好的情况,例如,计算机崩溃)。
每个人都已经告诉过你不应该这样做,这会导致不明确的行为。这是众所周知的,所以让我们在一个较低的层次上详细阐述这一点,让我们看看实际发生了什么。
标准的普遍答案是任何事情都有可能发生,这并不完全正确。例如,计算机不会因为这样做而试图杀死你(除非你正在为机器人编程人工智能):)
之所以没有任何通用答案,是因为这是未定义的,它可能因编译器而异,甚至在同一编译器的不同版本之间也有所不同。
但在大多数情况下,这就是"大致"发生的情况:
- 如果定义了析构函数,
- 它以某种方式释放分配给对象的内存
因此,如果析构函数包含访问已删除类的任何数据的任何代码,那么它可能会出错,或者(很可能)您将读取一些无意义的数据。如果这些删除的数据是指针,那么它很可能是segfault,因为您将尝试访问包含其他内容或不属于您的内存。
如果您的构造函数不接触任何数据或不存在(为了简单起见,这里不考虑虚拟析构函数),那么它可能不是大多数编译器实现崩溃的原因。然而,调用析构函数并不是这里将要发生的唯一操作。
内存必须是空闲的。它是如何完成的取决于编译器中的实现,但它也可以执行一些类似于
这意味着您的一个或多个内存结构刚刚损坏,您的程序可能早晚崩溃,或者它的行为可能异常怪异。原因在你的调试程序中并不明显,你可能会花几个星期的时间弄清楚到底发生了什么。
所以,正如其他人所说,这通常是个坏主意,但我想你已经知道了。不过别担心,如果你删除一个对象两次,无辜的小猫很可能不会死。
下面是错误的示例代码,但也可以很好地工作(它在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! |
我想我应该把它贴出来,因为没人提到它。