我曾经读过的一本C++书,说明当使用EDCOX1×0操作符删除指针时,它指向的位置的内存被"释放",并且可以被重写。它还声明指针将继续指向同一位置,直到重新分配或设置为NULL。
然而,在Visual Studio 2012中,情况似乎并非如此!
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #include <iostream>
using namespace std;
int main()
{
int* ptr = new int;
cout <<"ptr =" << ptr << endl;
delete ptr;
cout <<"ptr =" << ptr << endl;
system("pause");
return 0;
} |
当我编译和运行这个程序时,我得到如下输出:
1 2 3
| ptr = 0050BC10
ptr = 00008123
Press any key to continue.... |
显然,当调用delete时指针指向的地址会发生更改!
为什么会这样?这与Visual Studio有什么特别的关系吗?
如果删除可以改变它指向的地址,为什么不自动将指针设置为NULL而不是某个随机地址呢?
- 型删除一个指针,并不意味着它将被设置为空,你必须注意。
- 型我知道这一点,但我正在读的那本书特别指出,它仍然包含删除前指向的地址,但该地址的内容可能会被覆盖。
- 型@TJWRONA1992,是的,因为这是通常发生的事情。这本书只列出了最有可能的结果,而不是硬性规定。
- 型在调试器中查看指针时,指针的值是什么?这一点之所以重要,是因为您正在通过operator <<的挑战来运行指针。谁知道如果指针不再有效,另一端会出现什么。
- 型@ TJWRONA1992 1992年我一直在读的一本C++书,书的名字是…是吗?
- 型你所做的是不明确的行为,你可以看到我的答案。
- 型这本书是"山姆每天在一小时内自学C++",这本书是从2009开始的,所以有些信息可能过时了。此外,本书中的任何程序都不会指向任何单个编译器,因此不会提及任何特殊的Visual Studio。
- 型真的不是…这个问题是基于我在Visual Studio中看到的内容,而不是我在书中看到的内容…如果你读了我的答案,它就表明这本书是对的。如果禁用Visual Studio中重定向已删除指针的功能,则指针将保持指向其原始位置。
- 型@TJWRONA1992:正如我在回答中所说,你所做的是未定义的行为,我想你不应该期望任何一致的行为。
- 型@乔尔奇的行为是一致的。尝试使用已删除的指针将导致未定义的结果,但我没有使用它。我只是看看它指向哪里。我的示例提供了一致的结果,这些结果对Visual Studio实际使用指针所做的操作具有决定性。
- 型@TJWRONA1992:这可能令人惊讶,但所有使用无效指针值的行为都是未定义的,不仅是取消引用。"检查它指向的位置"正在以不允许的方式使用该值。
- 型@TJWRONA1992:你所做的已经在使用它了
- 型@不管我是否使用它,结果都是一致的。如果启用了该功能,Visual Studio将始终将指针重定向到0x8123;如果禁用了该功能,则始终保持指针指向其原始位置。结果并非难以预料。我不是火箭科学家,但我相当肯定,当某些东西总是相同的时候,根据定义,它被认为是一致的。
- 型@Benvoigt:您认为删除后读取它是ub吗,即使编译器在删除后自动将0x8123分配给指针?(见我的答案)
- 型@本沃伊特:多谢我的观点,也许是因为编译器为指针分配了0x8123,也许现在读取指针值就不再是未定义的行为了?(也许现在它不再有未初始化的状态了?)
- 型@Giorgi:如果您要问的是编译器是否替换了剩余的"无效指针值",通过编写0x00008123,答案是"没关系"。首先,期望编译器这样做是不可移植的,其次,写入的值也是一个"无效的指针值",读取是非法的。读取是非法的,不是因为它是一个"不确定的值"(在编译器写入它之后可能不是真的),而是因为它包含一个"无效的指针值"。
- 型@本沃伊特:这就是我的观点,我想也许是因为编译器为删除的指针分配了0x0008123,也许现在可以读取它了?(如果没有,我现在就不得不修改我的答案的最后一部分)。但是,如果读取任何无效的指针值是ub,不是因为它被"删除",而是因为指针有无效的值,那么即使编译器分配了0x0008123,读取它也是ub
- 型@Giorgi:编译器所做的并不算是语言规则下的赋值。只是"读作0x00008123"是未定义行为的法律结果。尽管严格来说,读取"无效指针值"是根据标准定义的实现行为(但脚注非常清楚,实现可以指定任何行为,包括我们通常与未定义行为关联的行为)。
- 型你为什么认为0x0008123不是空的?(我不这么认为,但我知道唯一保证的相关承诺是"0"->"空"。不能保证某些空的运行时表示为0x0或任何其他特定的位模式。)
- 型@EricTowers:标准很清楚,deallocation函数使指针中包含的值无效,指针在deallocation之前指向deallocation空间中的对象。因为它们是无效的指针值,所以您不能对它们进行任何可移植的操作,包括讨论它们是否为空。
- 型@EricTowers,尝试将指针变量设置为NULL:ptr = NULL,然后打印它的值。cout << ptr << endl;。您会发现,当一个指针显式地设置为NULL时,它将指向地址00000000…
- 型@TJWRONA1992:Visual Studio的实现定义的行为(在该版本中,使用那些补丁,使用那些编译标志)不是通用的。stackoverflow.com/questions/27714377
- 型@不,我可以肯定地计算一个指针,并将其值与其他值进行比较。我不能取消引用。没关系,我不想。
我注意到存储在ptr中的地址总是被00008123覆盖…
这看起来很奇怪,所以我做了一点挖掘,发现这个微软博客帖子包含了一个章节,讨论"删除C++对象时的自动指针消毒"。
...checks for NULL are a common code construct meaning that an existing check for NULL combined with using NULL as a sanitization value could fortuitously hide a genuine memory safety issue whose root cause really does needs addressing.
For this reason we have chosen 0x8123 as a sanitization value – from an operating system perspective this is in the same memory page as the zero address (NULL), but an access violation at 0x8123 will better stand out to the developer as needing more detailed attention.
它不仅解释了删除指针后Visual Studio如何处理指针,还回答了为什么他们选择不自动将其设置为NULL!
此"功能"作为"SDL检查"设置的一部分启用。启用/禁用它:项目->属性>配置属性> > C/C++ > > -> SDL检查
要确认这一点:
更改此设置并重新运行同一代码将产生以下输出:
1 2
| ptr = 007CBC10
ptr = 007CBC10 |
"feature"用引号括起来,因为在同一位置有两个指针的情况下,调用delete只会清除其中一个指针。另一个将左指无效位置。
Visual Studio可能会因未能在其设计中记录此缺陷而使您陷入困境。
- 这是一个很好的发现。我希望MS能够更好地记录这样的调试行为。例如,最好知道哪个编译器版本开始实现这一点,以及哪些选项启用/禁用该行为。
- 这个链接对我有用。以下是完整地址:blogs.microsoft.com/cybertrust/2012/04/24/&hellip;
- 好吧,现在您知道这是由/sdl compile选项启用的,只需将其添加到您的答案中即可。
- 好的,可以。谢谢@hanspasant
- "从操作系统的角度看,这和零地址在同一个内存页上"-huh?对于Windows和Linux,x86上的标准(忽略大页面)页面大小是否仍为4KB?虽然我隐约记得陈瑞蒙的博客上的前64kb地址空间,但实际上我还是得到了同样的结果。
- @VOO Windows保留了第一个(和最后一个)64kb的RAM作为捕获的死区。0x8123正好落在那里
- 我手头没有虚拟机,所以想知道…它是在所有配置中执行此操作,还是只在调试中执行此操作?
- 实际上,它不鼓励坏习惯,也不允许您跳过将指针设置为空——这就是他们使用0x8123而不是0的全部原因。指针仍然无效,但在试图取消引用它时会导致异常(良好),并且它不会通过空检查(也很好,因为不这样做是一个错误)。哪里有坏习惯?它实际上只是帮助您调试的东西。
- @乱安坏习惯的地方是当你有两个指向同一地点的指针。当您删除一个Visual Studio时,它会将该Visual Studio设置为0x8123,但会使另一个Visual Studio指向当前无效的地址。它应该做的是将它们都设置为0x8123,这样就可以保持一致。如果Visual Studio根本不这样做,并让您自己将其设置为NULL,那么对于正在发生的事情将毫无疑问,因为两个指针都将指向一个无效的地址,而事情将完全如您所料。
- 好吧,它不能同时设置两个(全部),所以这是第二个最佳选项。如果您不喜欢,只需关闭SDL检查-我发现它们非常有用,尤其是在调试其他人的代码时。
- 我同意,在调试其他人的代码时,这样做是有好处的。您可以确保将自己删除的所有指针都设置为NULL,但不知道其他人会做什么。
- 很好的为你弄清楚到底发生了什么,为什么不接受标准"C++标准把它定义为未定义的行为,任何事情都可能发生",无回答。
您可以看到/sdl编译选项的副作用。在VS2015项目的默认情况下,它启用了比/GS提供的安全检查更多的安全检查。使用Projject > Projt> C/C++>通用> SDL检查设置以更改它。
引用来自msdn文章:
-
Performs limited pointer sanitization. In expressions that do not involve dereferences and in types that have no user-defined destructor, pointer references are set to a non-valid address after a call to delete. This helps to prevent the reuse of stale pointer references.
请记住,在使用msvc时,将删除的指针设置为空是一种糟糕的做法。它会破坏您从调试堆和这个/sdl选项中获得的帮助,您将无法再在程序中检测到无效的空闲/删除调用。
- 证实。禁用此功能后,指针将不再重定向。感谢您提供修改它的实际设置!
- 汉斯,在两个指针指向同一位置的情况下,将删除的指针设置为空是否仍然被认为是一种坏做法?当您使用deleteone时,Visual Studio将使第二个指针指向其原始位置,而该位置现在无效。
- 我很不清楚通过将指针设置为空,您希望发生什么样的魔法。另一个指针没有,所以它不能解决任何问题,您仍然需要调试分配器来查找错误。
- 我的观点是,如果您总是依赖于Visual Studio来为您清理指针,那么第二个指针将不会被正确地清理;但是,如果您自己清理所有指针,则很可能会停止并考虑它,然后对这两个指针进行清理。没有文档说明Visual Studio将无法清理第二个指针,因此大多数用户只会假设它已清理,并在程序中留下悬空的引用。
- vs不清除指针。它腐蚀了他们。所以不管怎样,当你使用它们的时候,你的程序都会崩溃。调试分配器对堆内存做了很多相同的事情。空值的大问题是,它没有足够的损坏。否则,一个常见的策略,谷歌"0xDeadBeef"。
- 将指针设置为空仍然比让它指向以前的地址要好得多,而这个地址现在是无效的。尝试写入空指针不会损坏任何数据,可能会导致程序崩溃。尝试在该点重用指针可能不会使程序崩溃,它可能只会产生非常不可预测的结果!
It also states that the pointer will continue to point to the same location until it is reassigned or set to NULL.
这绝对是误导信息。
Clearly the address that the pointer is pointing to changes when delete is called!
Why is this happening? Does this have something to do with Visual Studio specifically?
这显然在语言规范中。调用delete后,ptr无效。在deleted之后使用ptr是导致未定义行为的原因。不要这样做。在调用delete之后,运行时环境可以自由地使用ptr做它想做的任何事情。
And if delete can change the address it is pointing to anyways, why wouldn't delete automatically set the pointer to NULL instead of some random address???
将指针的值更改为任何旧值都在语言规范中。如果把它改成空的话,我会说,那就太糟糕了。如果指针的值设置为空,程序的行为将更加正常。然而,这将隐藏问题。当使用不同的优化设置编译程序或将程序移植到不同的环境时,问题很可能会在最不合适的时刻出现。
- 我不相信它能回答OP的问题。
- 即使编辑后也不同意。将其设置为空并不能隐藏问题——事实上,它将在更多情况下公开问题,而不是不公开问题。正常实现不这样做是有原因的,原因是不同的。
- @Sergeya,大多数实现不是为了效率。但是,如果一个实现决定设置它,最好将它设置为不为空的值。它会比设置为空更快地揭示问题。它被设置为空,在指针上两次调用delete不会导致问题。那绝对不好。
- 不,不是效率——至少,这不是首要问题。
- @谢尔盖,我错过了你想的东西。如果你不介意的话,请在你的答案中加上这个。
- @Sergeya设置一个指向一个值的指针,该值不是NULL,但也肯定在进程的地址空间之外,这将暴露比两个选项更多的情况。如果释放后使用,使其悬空不一定会导致segfault;如果再次使用deleted,将其设置为NULL不会导致segfault。
1 2
| delete ptr;
cout <<"ptr =" << ptr << endl; |
一般来说,甚至读取(如上面所述,注意:这与取消引用不同)无效指针的值(指针变为无效,例如,当deleteit)是实现定义的行为时。这是在CWG 1438中引入的。这里也可以看到。
请注意,在读取无效指针的值之前是未定义的行为,所以上面的内容是未定义的行为,这意味着任何事情都可能发生。
- 与此相关的还有来自[basic.stc.dynamic.deallocation]的引用:"如果在标准库中给deallocation函数的参数是一个不是空指针值的指针,则deallocation函数应取消分配指针引用的存储,使所有指向已取消分配存储的任何部分的指针都无效"以及中的规则。[conv.lval](第4.1节)表示读取(lvalue->rvalue转换)任何无效的指针值都是实现定义的行为。
- 即使是ub,也可以由特定的供应商以特定的方式实现,这样它才是可靠的,至少对于编译器来说是这样。如果微软决定在CWG之前实现他们的指针清理功能,这不会使该功能变得越来越不可靠,特别是,如果该功能被打开,不管标准怎么说,"任何事情都可能发生",这是不真实的。
- @Kylestrand:我基本上给出了ub的定义(blog.regehr.org/archives/213)。
- 对大多数C++社区来说,"任何事情都可能发生"完全是字面上的。我觉得这很荒谬。我理解ub的定义,但我也理解编译器只是由真正的人实现的软件的一部分,如果那些人实现了编译器,使它以某种方式运行,那就是编译器的行为方式,不管标准怎么说。
我相信,您正在运行某种调试模式,而vs正试图将指针重新指向某个已知位置,以便进一步尝试取消对它的引用,从而跟踪并报告。尝试在发布模式下编译/运行同一程序。
为了提高效率和避免错误的安全性,通常不会在delete内更改指针。在大多数复杂的场景中,将删除指针设置为预定义的值是没有用的,因为被删除的指针可能只是指向此位置的几个指针中的一个。
事实上,我想得越多,我就越发现vs在做这件事的时候是错误的,就像往常一样。如果指针是常量呢?它还会改变吗?
- 我不知道!我将用一个常量指针来尝试此操作:)
- 是的,即使是固定的指针也会被重定向到这个神秘的8123!
- 就在今天早上,有人问为什么他们应该使用G++而不是V。这里是。
- 同样,这个特性也不一定是坏的。当然,如果你试图使用无效的指针,它会导致你的程序在你的脸上爆炸,但这总比让你的整个计算机崩溃,因为你试图重用一个左指旧位置的指针要好!
- @TJWRONA1992那个功能不好。它给你提供了一种无法保证的安全感。原因是,在任何复杂的程序中,最终都会有多个指针指向同一内存。那么,如果你"消毒"了其中一个会有什么好处呢?不好,对吧?
- 你是对的。。。如果两个指针指向同一位置,并且一个指针被删除,则只有一个指针被"净化"。另一个指针左指现在无效的内存…你真丢脸,微软
- @TJWRONA1992年,它在20年或更多年前被讨论过,大家一致认为我们不会"净化"指针。原因就在这里。微软再次改造了方向盘,并使之成为方形。
- @但另一方面,删除的指针将按segfault显示您试图删除一个删除的指针,它将不等于空。在另一种情况下,只有当页面也被释放(这是非常不可能的)时,它才会崩溃。更快地失败;更快地解决。
- @Ratchetfreak"速战速决,早日解决"是一个非常有价值的咒语,但"通过销毁关键的法医证据而速战速决"并不能开启如此有价值的咒语。在简单的情况下,这可能很方便,但在更复杂的情况下(我们往往需要最大的帮助),删除有价值的信息会减少我可用的解决问题的工具。
- @TJWRONA1992:我认为微软在这里做的是正确的。清除一个指针总比完全不清除要好。如果这导致调试出现问题,请在错误的删除调用之前放置一个断点。很可能如果没有这样的东西,你永远不会发现问题。如果您有更好的解决方案来定位这些bug,那么就使用它,为什么您关心微软的工作?
型
删除指针后,它指向的内存可能仍然有效。为了显示这个错误,指针值被设置为一个明显的值。这确实有助于调试过程。如果该值被设置为NULL,它可能永远不会在程序流中显示为潜在的bug。因此,当您稍后对NULL进行测试时,它可能会隐藏一个bug。
另一点是,一些运行时优化器可能会检查该值并更改其结果。
在早期,ms将值设置为0xcfffffff。