C++ delete - 它删除了我的对象,但我仍然可以访问数据?

43

我已经编写了一个简单的俄罗斯方块游戏,其中每个块都是一个类singleblock的实例。

class SingleBlock
{
    public:
    SingleBlock(int, int);
    ~SingleBlock();

    int x;
    int y;
    SingleBlock *next;
};

class MultiBlock
{
    public:
    MultiBlock(int, int);

    SingleBlock *c, *d, *e, *f;
};

SingleBlock::SingleBlock(int a, int b)
{
    x = a;
    y = b;
}

SingleBlock::~SingleBlock()
{
    x = 222;
}

MultiBlock::MultiBlock(int a, int b)
{
    c = new SingleBlock (a,b);
    d = c->next = new SingleBlock (a+10,b);
    e = d->next = new SingleBlock (a+20,b);
    f = e->next = new SingleBlock (a+30,b);
}

我有一个函数,它扫描完整的一行,并遍历块的链表删除相关的块并重新分配 ->next 指针。

SingleBlock *deleteBlock;
SingleBlock *tempBlock;

tempBlock = deleteBlock->next;
delete deleteBlock;

游戏运行正常,方块被正确删除,所有功能都按照预期工作。但是检查后,我仍然可以访问已删除数据的随机位。

如果我在它们被删除之后 printf 每个已删除的单个块的 "x" 值,其中一些返回随机垃圾(确认已删除),而另一些返回 222。这告诉我尽管调用了析构函数,数据实际上并没有从堆中删除。许多相同的试验表明,总是有一些特定的块未被正确删除。

结果:

Existing Blocks:
Block: 00E927A8
Block: 00E94290
Block: 00E942B0
Block: 00E942D0
Block: 00E942F0
Block: 00E94500
Block: 00E94520
Block: 00E94540
Block: 00E94560
Block: 00E945B0
Block: 00E945D0
Block: 00E945F0
Block: 00E94610
Block: 00E94660
Block: 00E94680
Block: 00E946A0

Deleting Blocks:
Deleting ... 00E942B0, X = 15288000
Deleting ... 00E942D0, X = 15286960
Deleting ... 00E94520, X = 15286992
Deleting ... 00E94540, X = 15270296
Deleting ... 00E94560, X = 222
Deleting ... 00E945D0, X = 15270296
Deleting ... 00E945F0, X = 222
Deleting ... 00E94610, X = 222
Deleting ... 00E94660, X = 15270296
Deleting ... 00E94680, X = 222

能够从坟墓中访问数据是可以预期的吗?

如果我的表达有些冗长,那么我很抱歉。


2
最安全的策略是在不再使用一个项时将其删除,并且永远不再引用它。当多个指针引用同一内存中的对象时,智能指针可以提供帮助。 - Thomas Matthews
如果您可以访问这些块,那么您可以重新删除它们。这是不好的。请不要这样做。 - David Thornley
37
有时我认为“忘记”比“删除”更适合作为关键字;你实际上并不是在告诉编译器要删除任何东西,而是要停止关注它(并让其他人随意处理i),有点像把一本书归还图书馆而不是烧掉它。 - Dan Tao
1
这段代码的结构,Multiblock类并不负责处理它自己的成员。虽然这是合法的C++(可以编译,并且不依赖于未定义的行为 - 忽略你在这里谈论的删除后访问),但它实际上是一个C风格的程序。尝试使MultiBlock处理其自己的成员,包括删除操作。如果不太困难,避免将裸指针暴露给类外部。这种封装通常可以避免后续出现的所有错误/内存泄漏问题。 - Merlyn Morgan-Graham
我同意Thomas Matthews的观点。如果可以的话,请使用智能指针(boost库的shared_pointer是一个相当不错的通用型智能指针)。如果您不想依赖该库,请尝试使用std::list或std::vector代替手动编写链表/堆分配可扩展数组实现。 - Merlyn Morgan-Graham
这是一个最佳实践的案例。 - lsalamon
13个回答

96

能够从坟墓之外访问数据是可以预期的吗?

这在技术上被称为未定义行为。如果它给你一罐啤酒也不要感到惊讶。


18
另外,补充一下这个事实的必然推论...如果某人在内存中存储了敏感数据,删除前完全覆盖是一个良好的做法(以防其他代码段访问它)。 - Romain
在 dtor 调用之前应该处理那个。 - dirkgently
4
是的,我认为析构函数是正确的位置。你不希望过早地执行它,也不能太晚了。 - David Thornley
@Romain:只需要确保它没有被优化掉,因为它不是可观察行为。(使用一个保证不会被修剪的API函数,而不是memset。) - Deduplicator

44
能否从坟墓里访问数据是一个预期的问题吗? 在大多数情况下,是的。调用delete并不会清零内存。 请注意,这种行为没有被定义。使用某些编译器,内存可能会被清零。当您调用delete时,内存被标记为可用,因此下次有人执行new时,内存可能会被使用。 如果您仔细想一想,就会发现这很合乎逻辑——当您告诉编译器您对内存不再感兴趣(使用delete),计算机为什么要花时间将其清零呢?

然而,不能保证newmalloc不会在旧对象之上分配一些新对象。另一个灾难可能是系统垃圾回收器。此外,如果您的程序从系统范围内的内存池中获得内存,则其他程序可能会覆盖幽灵数据。 - Thomas Matthews
实际上,不是这样的。成功访问已删除的内存不是预期行为,而是未定义的行为。另一个分配也可以轻松地覆盖您刚释放的内存。 - Curt Nichols
6
@Thomas Matthews 我并不是在说试图访问它是一个好主意。@Curt Nichols 这只是玩弄言辞。根据使用的编译器,你可以期望在调用delete时内存不会立即清零。尽管如此,你显然不能确定它。 - Martin

18

删除并不会真正删除任何东西,它只是将内存标记为“可供重用”。除非有其他的分配调用来预留和填充该空间,否则它将保留旧数据。然而,依赖这种方式是绝对不可取的,如果你删除了某些东西,请忘记它。

在库中经常遇到的做法之一是Delete函数:

template< class T > void Delete( T*& pointer )
{
    delete pointer;
    pointer = NULL;
}

这可以防止我们意外访问无效的内存。

请注意,调用delete NULL;是完全可以的。


即使您不使用宏,释放指针后立即将其设置为NULL也是一个好习惯。这是一个很好的习惯,可以避免这些误解。 - Mark Ransom
2
@Kornel,我个人认为任何使用这种宏的C++库都应该引起高度怀疑。至少,它应该是一个内联模板函数。 - anon
13
在C++中,将指针设置为NULL以跟随delete并不是一种普遍的良好实践。有时这样做是好事,有时则是无意义的,并可能掩盖错误。 - anon
4
我讨厌这个做法。它很混乱,而且不怎么样。 - GManNickG
6
“这可以防止我们意外地访问无效内存。” 这是不正确的,它说明为什么使用这个技巧应该与编写糟糕的代码相关。 char *ptr = new char; char *ptr2 = ptr; Delete(ptr); *ptr2 = 0; 我无意中访问了无效内存。将一个引用置空,以保护所引用的对象,这种想法很混乱。此外,请不要忘记,您需要为指向数组的指针编写一个单独的版本的此函数。 - Steve Jessop
显示剩余5条评论

10

这就是C++所谓的未定义行为 - 你可能能够访问数据,也可能不能。无论如何,这都是错误的做法。


6
堆内存就像一堆黑板。想象一下你是一名老师,当你在上课时,这些黑板就属于你,你可以随心所欲地在上面涂画,覆盖内容。
当课程结束,你准备离开教室时,并没有规定你必须擦拭黑板 - 你只需将黑板交给下一位老师,他们通常能够看到你写的内容。

1
如果编译器能够确定代码不可避免地要访问(甚至查看)其未拥有的黑板的一部分,这种确定将使编译器摆脱时间和因果律的束缚;一些编译器以过去十年被认为是荒谬的方式利用了这一点(其中许多仍然是荒谬的,依我之见)。我可以理解说,如果两个代码片段彼此不依赖,编译器可以以任何方式交错它们的处理,即使这会导致 UB“提前”发生,但一旦 UB变得不可避免,所有规则都将飞出窗外。 - supercat

3
系统在通过delete()释放内存时并不会清除内存内容。因此,内存内容仍然可以访问,直到该内存被重新分配并覆盖为止。

在对象被删除后访问它仍然是不允许的,无论内存中有什么内容都一样。 - walnut
“仍然可以访问”只是指另一侧的活雷区仍然可以访问 - 也就是说,你可能会逃脱,但如果你尝试,你也很有可能被炸死,所以最好不要冒险。 - Jeremy Friesner

3

删除对象后,不确定其占用的内存内容会发生什么。这意味着该内存可用于重新使用,但实现不必覆盖原始数据并且不必立即重用该内存。

在对象消失后不应访问内存,但某些数据仍可能保留在那里也不应感到惊讶。


1

delete释放内存,但不会修改或清零。尽管如此,您仍不应访问已释放的内存。


没有明确说明内存是否会被清零。例如,为了调试或安全目的,实现可能会在删除后覆盖内存。 - walnut

1

有时候是可以预料到的。当 new 为数据保留空间时,delete 只是使使用 new 创建的指针失效,从而允许在先前保留的位置写入数据;它不一定会删除数据。但是,您不应该依赖于这种行为,因为这些位置上的数据随时可能发生变化,可能导致程序表现异常。这就是为什么在使用指针上的 delete(或使用 new[] 分配的数组上的 delete[])之后,您应该将其赋值为 NULL,以便不能篡改无效的指针,并且假设在再次使用该指针之前不会使用 newnew[] 来分配内存。


3
C++语言标准中没有任何规定防止delete擦除已被删除的内存或用奇怪的值填充,这是由实现定义的。 - Thomas Matthews

0

它还不会立即将内存清零/更改...但是在某个时候,地毯会被从你的脚下拉开。

不,它确实是不可预测的:它取决于内存分配/释放的速度。


它可能会立即清零内存。语言标准中没有任何阻止它的内容,出于调试或安全原因这样做可能是有意义的。无论如何,在delete调用之后访问对象都是未定义行为。 - walnut

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