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

C++ delete - It deletes my objects but I can still access the data?

提问人:Ash 提问时间:12/19/2009 最后编辑:TrebledJAsh 更新时间:6/1/2020 访问量:18984

问:

我编写了一个简单的、有效的俄罗斯方块游戏,每个方块都作为类 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;

游戏运行正常,方块被正确删除,一切都按预期运行。但是,在检查时,我仍然可以访问已删除数据的随机位。

如果我在删除后打印每个已删除的单块“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

能够从坟墓之外访问数据是否值得期待?

对不起,如果这有点啰嗦。

C 指针 undefined-behavior delete-operator c++-faq

评论

2赞 Thomas Matthews 12/19/2009
最安全的策略是在不再使用某个项目时将其删除,并且不再引用它。当多个指针引用内存中的同一对象时,智能指针会有所帮助。
0赞 David Thornley 12/19/2009
如果可以访问这些块,则可以重新删除它们。这很糟糕。别这样。
37赞 Dan Tao 12/19/2009
有时我认为一个比本来更好的关键字;你实际上并没有告诉编译器删除任何东西,而是停止关心它(并让其他人对我做任何他们想做的事情),有点像将一本书归还给图书馆而不是烧毁它。deleteforget
1赞 Merlyn Morgan-Graham 12/19/2009
此代码的结构方式,Multiblock 类不负责处理它自己的成员。虽然这是合法的 C++(它编译,并且不依赖于未定义的行为 - 忽略您在这里谈论的删除后的访问),但它实际上是一个 C 风格的程序。尝试让 MultiBlock 处理自己的成员,包括删除操作。如果不是太难,请避免在类之外公开原始指针。这种封装通常可以使您免于一大堆错误/内存泄漏。
0赞 Merlyn Morgan-Graham 12/19/2009
我同意托马斯·马修斯的观点。如果可以的话,使用智能指针(boost 库shared_pointer是一个非常好的通用指针)。如果您不想依赖库,请尝试使用 std::list 或 std::vector,而不是手动编写链表/堆分配的可扩展数组实现。

答:

0赞 jldupont 12/19/2009 #1

它还不会将内存归零/更改......但在某些时候,地毯会从你的脚下被拉出来。

不,这当然是不可预测的:这取决于内存分配/释放的速度。

评论

0赞 walnut 4/11/2020
它可能会立即将内存归零。语言标准中没有任何内容阻止它,出于调试或安全原因,它可能有意义。在任何情况下,在调用后访问对象都是 UB。delete
44赞 Martin 12/19/2009 #2

能够从坟墓之外访问数据是否值得期待?

在大多数情况下,是的。调用 delete 不会将内存归零。

请注意,未定义行为。使用某些编译器时,内存可能会归零。调用 delete 时,会发生内存被标记为可用,因此下次有人执行操作时,可能会使用内存。

如果你仔细想想,这是合乎逻辑的——当你告诉编译器你不再对内存感兴趣(使用 delete)时,计算机为什么要花时间将其归零。

评论

0赞 Thomas Matthews 12/19/2009
但是,不能保证或不会在旧对象之上分配一些新对象。另一个灾难可能是系统垃圾回收器。此外,如果程序从系统范围的内存池中被授予内存,则其他程序可能会覆盖幻影数据。newmalloc
0赞 Curt Nichols 12/19/2009
其实不然。成功访问已删除的内存不是预期行为,而是未定义的行为。另一个分配可以很容易地覆盖您刚刚释放的内存。
6赞 Martin 12/19/2009
@Thomas Matthews,我并不是说尝试访问它是个好主意。@Curt Nichols:这是在玩文字游戏。根据您使用的编译器,您可以预期在调用 delete 时内存不会立即归零。不过,您显然无法确定。
1赞 Ramónster 12/19/2009 #3

delete 会解除分配内存,但不会对其进行修改或将其清零。不过,您不应该访问已释放的内存。

评论

0赞 walnut 4/11/2020
未指定内存是否归零。例如,出于调试或安全目的,实现可能会在删除后覆盖内存。
96赞 dirkgently 12/19/2009 #4

能够从坟墓之外访问数据是否值得期待?

这在技术上称为未定义行为。如果它为您提供一罐啤酒,也不要感到惊讶。

评论

18赞 Romain 12/19/2009
另外,最好加上这个事实的推论......如果内存中存储了“敏感”数据,则应考虑在删除之前完全覆盖它是一种很好的做法(以防止其他代码段访问它)。
0赞 dirkgently 12/19/2009
这应该在 dtor 调用之前处理。
4赞 David Thornley 12/19/2009
@dirkgently:是的,我认为析构函数是正确的地方。你不想做得太早,也不能做得太晚。
0赞 Deduplicator 9/26/2014
@Romain:人们只需要真正确保它没有被优化出来,因为它不是可观察的行为。(使用保证不被修剪的 API 函数,而不是memset
10赞 anon 12/19/2009 #5

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

3赞 William Bell 12/19/2009 #6

当您通过 释放内存时,系统不会清除内存。因此,在分配内存以供重用和覆盖之前,仍然可以访问这些内容。delete()

评论

0赞 walnut 4/11/2020
但是,在删除对象后,不允许访问该对象。内存包含哪些内容并不重要。
0赞 Jeremy Friesner 10/19/2021
“仍然可以进入”,只是在活动雷区的另一侧仍然可以进入的意义上 - 也就是说,你可能会侥幸逃脱,但如果你尝试,你也很可能会被炸毁,所以你最好不要冒险。
3赞 CB Bailey 12/19/2009 #7

删除对象后,没有定义它占用的内存内容会发生什么。这确实意味着该内存可以自由重用,但实现不必覆盖最初存在的数据,也不必立即重用内存。

你不应该在对象消失后访问内存,但不应该推测某些数据仍然在那里。

1赞 Dustin 12/19/2009 #8

是的,有时可以预料到。虽然为数据保留了空间,但只是使使用 创建的指针失效,允许在先前保留的位置写入数据;它不一定会删除数据。但是,您不应依赖该行为,因为这些位置的数据可能随时更改,这可能会导致程序行为异常。这就是为什么在指针上使用(或分配了 的数组)后,应为其分配 NULL,以便无法篡改无效指针,假设您不会使用或再次使用该指针之前分配内存。newdeletenewdeletedelete[]new[]newnew[]

评论

3赞 Thomas Matthews 12/19/2009
C++ 语言标准中没有任何内容可以阻止擦除已删除的内存或填充奇怪的值。它是实现定义的。delete
18赞 Kornel Kisielewicz 12/19/2009 #9

删除不会删除任何内容 - 它只是将内存标记为“可自由重用”。在一些其他分配调用保留并填充该空间之前,它将拥有旧数据。然而,依赖它是一个很大的禁忌,基本上如果你删除了一些东西,就忘记它。

在这方面,库中经常遇到的一种做法是 Delete 函数:

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

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

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

评论

0赞 Mark Ransom 12/19/2009
即使不使用宏,最好在释放 NULL 后立即设置指向 NULL 的指针。养成这个习惯是个好习惯,可以防止这些误解。
2赞 12/19/2009
@Kornel恕我直言,任何使用此类宏的C++库都将非常可疑。在非常简单的情况下,它应该是一个内联模板函数。
13赞 12/19/2009
@Mark 在删除后设置指向 NULL 的指针并不是 C++ 中的通用良好做法。有时这是一件好事,有时它毫无意义,可以隐藏错误。
4赞 GManNickG 12/19/2009
我讨厌这种做法。这很杂乱,嗯。
6赞 Steve Jessop 12/19/2009
“这可以防止我们意外访问无效内存”。这是不正确的,它说明了为什么使用这个技巧应该与编写糟糕的代码相关联。.我不小心访问了无效的内存。将引用无效的想法只是混乱的想法,认为这可以保护所引用对象。另外,不要忘记,对于指向数组的指针,您需要此函数的单独版本。char *ptr = new char; char *ptr2 = ptr; Delete(ptr); *ptr2 = 0;
-1赞 Ashish 12/19/2009 #10

它将导致未定义的行为并删除释放内存,它不会用零重新初始化它。

如果你想让它归零,那就做:

SingleBlock::~SingleBlock()

{    x = y = 0 ; }

评论

0赞 walnut 4/11/2020
这不是清除内存的安全方法。编译器可能会优化存储。当析构函数被调用时,你仍然不能再访问该对象。
6赞 Fred 12/19/2009 #11

堆内存就像一堆黑板。想象一下,你是一名老师。当你在课堂上教书时,黑板是属于你的,你可以用它做任何你想做的事情。您可以在上面涂鸦并根据需要覆盖内容。

當課程結束,你將要離開教室時,沒有政策要求你擦掉黑板 - 你只需將黑板交給下一位老師,他通常可以看到你寫下的文章。

评论

1赞 supercat 4/20/2015
如果编译器可以确定代码不可避免地会访问(甚至查看)它不拥有的黑板的一部分,那么这种确定将使编译器摆脱时间和因果关系的规律;一些编译者以十年前被认为是荒谬的方式利用了这一点(恕我直言,其中许多仍然是荒谬的)。我可以理解,如果两段代码不相互依赖,编译器可能会以任何方式交错处理它们,即使这会导致 UB “提前”命中,但一旦 UB 变得不可避免,所有规则都会飞出窗外。
0赞 Toby Speight 8/31/2016 #12

尽管运行时可能不会报告此错误,但使用适当的错误检查运行时(如 Valgrind)会在释放内存后提醒您内存的使用情况。

我建议,如果您使用 / 和原始指针(而不是类似指针)编写代码,请在 Valgrind 下进行单元测试,至少有机会发现此类错误。newdeletestd::make_shared()

-3赞 Ste_95 3/6/2017 #13

好吧,我也一直在思考这个问题,我试图运行一些测试来更好地了解引擎盖下发生的事情。标准答案是,在调用 delete 之后,您不应该期望访问该内存点有任何好处。 然而,这在我看来还不够。调用 delete(ptr) 时到底发生了什么?这是我发现的。我在 Ubuntu 16.04 上使用 g++,所以这可能在结果中起作用。

在使用 delete 运算符时,我首先期望的是释放的内存将交还给系统以供其他进程使用。让我说,在我尝试过的任何情况下,这都不会发生

使用 delete 释放的内存似乎仍然分配给它首先使用 new 分配给它的程序。我已经试过了,调用 delete 后内存使用量没有减少。我有一个软件,它通过调用分配了大约 30MB 的列表,然后在随后的删除调用中释放它们。发生的事情是,在程序运行时查看系统监视器,即使在删除调用后长时间睡眠,我的程序的内存消耗也是相同的。没有减少!这意味着删除不会将内存释放到系统。

事实上,看起来程序分配的内存是他的永远!但是,关键是,如果解除分配,内存可以由同一程序再次使用,而不必再分配任何内存。我试图分配 15MB,释放它们,然后再分配 15MB 的数据,但程序从未使用 30MB。系统监视器总是显示它大约 15MB。关于之前的测试,我所做的只是改变事情发生的顺序:一半分配,一半解除分配,另一半分配。

因此,显然程序使用的内存可以增加,但永远不会减少。我认为,在危急情况下,例如当没有更多可用内存时,也许真的会为其他进程释放内存。毕竟,当其他进程要求它时,让程序永远保留自己的内存有什么意义呢?因此,我再次分配了 30MB,在解除分配它们的同时,我尽可能多地运行物理内存。我希望看到我的软件将其内存分发给 memtester。但你猜,它没有发生!memtester

我编了一个简短的截屏视频,展示了这个东西的实际效果:

Delete example memory

老实说,有一种情况发生了一些事情。当我在程序的释放过程中尝试使用超过可用物理内存的 memtester 时,我的程序使用的内存下降到大约 3MB。然而,memtester进程被自动杀死了,发生的事情更令人惊讶!每次删除调用时,我的程序的内存使用量都会增加!就好像 Ubuntu 在 memtester 事件后恢复了所有内存一样。

取自 http://www.thecrowned.org/c-delete-operator-really-frees-memory