C++中delete后的指针处理

14

阅读了许多有关此事的帖子后,我想澄清下一个重点:


A* a = new A();
A* b = a;

delete a;

A* c = a; //illegal - I know it (in c++ 11)
A* d = b; //I suppose it's legal, is it true?

所以问题是关于使用已删除指针的副本

我读过,在c++ 11中,读取a的值会导致未定义的行为 - 但是读取b的值呢?

试图读取指针的值(注意:这与取消引用不同)会导致实现定义的行为,自C++14以来,这可能包括生成运行时故障。(在C++11中,这是未定义的行为) 删除后指针本身会发生什么?


解除指向无效内存的指针始终是未定义行为 - 但是您的代码没有解除任何这些指针。 - UnholySheep
你可以将'a'或'b'分配给其他指针,但在任何情况下它都将指向已释放的相同内存位置,因此如果您取消引用任何指针,则很可能会崩溃。实际上,这是未定义的行为。 - AlexG
1
为什么 d=b 是合法的,而 b 持有与 a 完全相同的值却是非法的? - Sergey Kalinichenko
1
A* c = a; 是合法的 - 只是有点愚蠢。 - Richard Critten
1
@dasblinkenlight 很多人认为 delete 修改指针本身,因此产生了这些想法。 - Slava
显示剩余3条评论
3个回答

35

两者:

A* c = a;
A* d = b;
在C++11中,ab都是“无效指针值”(因为它们指向已释放的存储空间),在C++14中则是实现定义。这是因为“使用无效指针值”在C++版本中无论是未定义还是实现定义。(“使用”包括“复制值”)。
相关章节([basic.stc.dynamic.deallocation]/4)在C++11中如下所述(强调添加):
如果标准库中的解分配函数所给出的参数是不是空指针值(4.10),则解分配函数应该解分配由指针引用的存储器,从而使所有指向解分配的存储器的部分的指针都变得无效。使用无效指针值的影响(包括将其传递到解分配函数中)是未定义的。
带有非规范性注释的块引用:
在一些实现中,它会导致系统生成的运行时
在C++14中,相同的章节如下所述:
如果标准库中的解分配函数所给出的参数是不是空指针值(4.10),则解分配函数应该解分配由指针引用的存储器,从而使所有指向解分配的存储器的部分的指针都变得无效。通过无效指针值进行间接引用和将无效指针值传递给解分配函数具有未定义的行为。任何其他使用无效指针值的行为都是实现定义的。
带有非规范性注释的块引用:
一些实现可能定义复制无效指针值会导致系统生成的运行时故障

有趣的是,即使在C++14之后(当前草案),仍然存在这样的说明:“注意:使用无效指针值(包括将其传递给解除分配函数)的影响是未定义的”。我不确定“未定义的影响”是否意味着未定义或实现定义的行为。尽管这只是一条注释,因此不具有规范性。 - eerorika
@Programmer1234 我把你链接的那个标记为重复,因为这个解释很清楚,所有相关的东西都被正确引用了。 - πάντα ῥεῖ
1
@user2079303:新的草案似乎将细节移动到[basic.stc]/4;但效果仍然是实现定义的:“当存储区域的持续时间结束时,表示该区域任何部分地址的所有指针的值都变为无效指针值。通过无效指针值进行间接引用并将无效指针值传递给解除分配函数具有未定义行为。对无效指针的任何其他使用具有实现定义的行为。” - Mankarse
[basic.stc.dynamic.safety]/4中的注释缺乏这种细微差别,可能是为了简洁起见。 - Mankarse

1

这两行代码在C++中没有任何区别(即合法性):

A* c = a; //illegal - I know it (in c++ 11)
A* d = b; //I suppose it's legal, is it true?

你犯了一个常见的错误,认为当你在 a 上调用 delete 时,它会与 b 有所不同。你应该记住,当你在指针上调用 delete 时,你是按值传递参数的,因此在 delete 后,a 指向的内存不再可用,但这个调用并不会使得 a 在你的例子中与 b 有任何不同。

在结尾处添加了一个链接 - 这就是我认为 A* c = a; 是非法的原因。 - Programmer1234
虽然这两行代码在合法性上没有区别,但是有些人可能会读到你的回答并认为两者都是合法的(实际上两者都可能导致运行时错误)。delete不是一个函数,因此谈论“按值传递参数”是没有意义的。 - M.M
@M.M语义上调用delete与调用函数没有任何区别。但是,SO变得非常不友好——不会因为答案不正确而被投反对票,而是因为“可能有人会读你的答案并认为”。 - Slava
调用delete与调用函数是不同的。 - M.M
2
是的,它从有效变为无效了,在C++14中您的断言可能会失败或导致硬件故障(或在C++11中恶魔飞出您的鼻子)。 - M.M
显示剩余16条评论

0

delete之后,您不应该使用指针。我的下面的示例访问a是基于实现定义的行为。(感谢M.M和Mankarse指出这一点)

我认为这里重要的不是变量a(或bcd),而是值(=已释放块的内存地址),在某些实现中,当在某些“指针上下文”中使用时,可能会触发运行时错误。

这个值可能是一个rvalue/表达式,不一定是存储在变量中的值——所以我不相信a的值会改变(我使用宽松的“指针上下文”来区分与在非指针相关表达式中使用相同的值,即相同的一组位,这不会导致运行时错误)。

------------我的原始帖子如下。---------------

好吧,你的实验已经接近完成了。只需添加一些cout,就像这样:

class A {};
A* a = new A();
A* b = a;
std::cout << a << std::endl;   // <--- added here
delete a;
std::cout << a << std::endl;   // <--- added here. Note 'a' can still be used! 
A* c = a; 
A* d = b; 

调用delete a对变量a不会产生任何影响。这只是一个库调用。管理动态内存分配的库会保留已分配内存块的列表,并使用变量a传递的值将其中一个先前分配的块标记为已释放。

虽然Mankarse从C++文档中引用的内容是正确的,即:"使所有指向已释放存储区域的指针无效" - 请注意,变量a的值仍然保持不变(您没有通过引用而是通过值传递它!)。

因此,总结并尝试回答您的问题:

a变量在delete后仍存在于作用域中。变量a仍然包含相同的值,即为class A对象分配的内存块的起始地址(现在已释放)。 这个a的值在技术上可以被使用-例如可以像上面的例子一样打印它,但是很难找到比打印/记录过去更合理的用途… 您不应该尝试解引用此值(您也保留在变量bcd中的值),因为此值不再是有效的内存指针。

您永远不应该依赖于对象在已释放的存储中(虽然C++不要求在使用后清除已释放的存储),因为您没有任何保证和安全的检查方式。


@M.M delete a 无法更改 ab 的值,这在技术上是不可能的,并且会违反太多的规定。 - Slava
@Slava,从技术上讲是可行的,并且标准允许这样做(请参见Mankarse的答案以获取标准参考)。其中一个用途是,在调试构建中,编译器可以将ab设置为已知的垃圾值,以帮助检测后续对它们的无效使用。一些编译器在调试模式下也会对未初始化的变量执行此操作。 - M.M
@M.M 我在标准中没有看到允许更改指针值的内容,也不知道虚拟机如何实现包括传递参数、从函数返回值、将它们存储在全局变量中等操作。 - Slava
请非常仔细地阅读Mankarse答案中的标准引用,特别是“使所有指针无效”的文本。指针在之前具有有效值,在之后具有无效值。因此,该值已更改。您真的认为有效值与无效值相同吗?您如何理解C++11中的下一句话“使用无效指针值的影响是未定义的”? - M.M
我提到调试器是为了说明语言规则的合理性。在上面的代码中,b没有传递给任何函数,所以我不确定你为什么要谈论按值传递 b - M.M
显示剩余6条评论

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