删除指针后将其置为NULL是一种好的做法吗?

185

首先,我要说的是,使用智能指针,你就不必再担心这个问题了。

以下代码有什么问题?

Foo * p = new Foo;
// (use p)
delete p;
p = NULL;

这是由另一个问题的 答案和评论 引发的。其中来自Neil Butterworth的一条评论获得了几个赞:

在 C++ 中,在 delete 后将指针设置为 NULL 不是普遍的良好实践。有时这样做是好的,有时是无意义的,有可能会隐藏错误。

在许多情况下这样做并没有帮助。但在我的经验中,它不会产生负面影响。请提醒我如果我理解错了。


7
@Andre:从技术上讲,这是未定义的行为。可能会发生的情况是您访问与之前相同的内存,但它现在可能被其他东西使用。如果您两次删除内存,则很可能会以难以找到的方式破坏程序执行。但是,安全地delete一个空指针是可以的,这也是将指针清零的好处之一。 - David Thornley
6
@André Pena,这是未定义的。通常它甚至不可重复。将指针设置为NULL可以使调试时更容易发现错误,也可能使其更可重复。 - Mark Ransom
18
飞行的恶魔是一种特色,而非缺陷。 - jball
4
这个问题不是重复的,因为另一个问题是关于C语言,而这个问题是关于C++。很多答案都依赖于诸如智能指针之类的东西,而这些在C++中是不可用的。 - Adrian McCarthy
4
Stroustrup谈到了这个问题:为什么delete不会将其操作数清零?。他说道:“C++明确允许delete的实现将lvalue操作数清零,我曾希望实现者能够这样做,但是这个想法似乎并没有得到实现者们的欢迎。如果您认为清零指针很重要,请考虑使用一个destroy函数:template<class T> inline void destroy(T*& p) { delete p; p = 0; }”。 - Dennis
显示剩余9条评论
18个回答

120

将指针设置为0(在标准C++中是“null”,来自C的NULL定义略有不同),可以避免双重删除导致的崩溃。

考虑以下情况:

Foo* foo = 0; // Sets the pointer to 0 (C++ NULL)
delete foo; // Won't do anything

鉴于:

Foo* foo = new Foo();
delete foo; // Deletes the object
delete foo; // Undefined behavior 

换句话说,如果您不将已删除的指针设置为0,则在执行双重删除时会遇到麻烦。反对在delete后将指针设置为0的一个论点是这样做只是掩盖了双重删除错误并使其无法处理。
显然最好不要有双重删除错误,但根据所有权语义和对象生命周期,实际上可能很难实现。我更喜欢掩盖的双重删除错误而不是UB。
最后,关于管理对象分配的附注,我建议您查看std::unique_ptr以进行严格/单一所有权,std::shared_ptr以进行共享所有权,或者根据您的需求选择另一个智能指针实现。

14
你的应用程序不会总是在双重删除时崩溃。取决于两次删除之间发生的情况,任何事情都可能发生。最有可能的是,你会破坏堆栈,并在以后的完全无关的代码中崩溃。虽然段错误通常比静默忽略错误更好,但在这种情况下,段错误不能保证出现,其实用性也值得怀疑。 - Adam Rosenfield
31
这里的问题在于你进行了双重删除。将指针设置为NULL只是隐藏了这个事实,没有改正或使它更安全。想象一下,一年后维护者回来看到foo已经被删除。他现在相信可以重用这个指针,不幸的是,他可能会错过第二次删除(甚至可能不在同一个函数中),然后指针的重用就会被第二次删除破坏。第二次删除后的任何访问现在都是一个严重的问题。 - Martin York
13
将指针设置为NULL确实可以掩盖双重删除错误。(有些人可能认为这是一种解决方案 - 的确是,但并不是一个很好的解决方案,因为它没有解决问题的根本原因。)但如果不将其设置为NULL,则会掩盖更常见(远远!)的问题,即在数据被删除后访问它。 - Adrian McCarthy
据我所知,std::auto_ptr已经在即将发布的C++标准中被弃用。 - rafak
我不会说被弃用了,那样听起来就像这个想法已经消失了。相反,它正在被 unique_ptr 取代,unique_ptr 带有移动语义,实现了 auto_ptr 试图做的事情。 - GManNickG
1
好的观点,“被替换为”是正确的说法;-) - rafak

64

在你删除指针所指对象后将其设置为 NULL,当然不会有什么问题,但这通常是掩盖更根本性问题的一种临时解决方案:你为什么要使用指针?我可以看到两个典型的原因:

  • 你只是想在堆上分配一些内存。在这种情况下,使用 RAII 对象来封装它会更安全、更清晰。当你不再需要该对象时,结束 RAII 对象的作用域。这就是 std::vector 的工作方式,它解决了意外留下指向已释放内存的指针的问题。这里没有指针。
  • 或者你可能想要一些复杂的共享所有权语义。从 new 返回的指针可能与调用 delete 的指针不同。多个对象可能同时使用对象。在这种情况下,使用 shared pointer 或类似的东西会更好。

我的经验法则是,如果在用户代码中留下指针,那么你做错了。指针本来就不应该指向垃圾。为什么没有一个对象负责确保其有效性?为什么它的作用域没有随着所指对象的结束而结束?


18
所以你主张一开始就不应该有一个裸指针,任何涉及该指针的事情都不应该被赋予“良好实践”的称号?好的。 - Mark Ransom
7
差不多吧,我不会说涉及原始指针的所有内容都不能称为良好的实践。只是这种情况是例外而不是规则。通常,指针的存在表明在更深层次上有一些问题。 - jalf
3
回答这个问题,不,我认为将指针设为null不会导致任何错误。 - jalf
9
我不同意——有时使用指针是很好的选择。例如,当栈上有两个变量且你想要选择其中一个时,或者当你想要向函数传递一个可选的变量时。但我认为,在使用new时,你永远不应该使用原始指针。 - rlbond
4
当一个指针超出作用域后,我不认为任何东西或任何人需要处理它。 - jalf
显示剩余9条评论

47

我有一个更好的最佳实践:在可能的情况下,结束变量的作用域!

{
    Foo* pFoo = new Foo;
    // use pFoo
    delete pFoo;
}

18
RAII是你的朋友。将其封装在类中,它会变得更简单。或者干脆不要自己处理内存,使用STL! - Brian
27
确实,那是最好的选择。但这并没有回答问题。 - Mark Ransom
4
这似乎只是使用函数作用域的副产品,并没有真正解决问题。当您使用指针时,通常会将它们的副本传递到多个层级中,然后您的方法在解决问题方面实际上毫无意义。虽然我同意良好的设计可以帮助您隔离错误,但我认为您的方法不是实现这一目标的主要手段。 - San Jacinto
5
我的示例意图是故意保持最简化。举个例子,如果对象是通过工厂创建的,那么它就不能放在栈上。或者它可能不是在作用域的开头创建的,而是位于某些结构中。我要说明的是,这种方法将在编译时发现指针的任何误用,而将其置空将在运行时发现任何误用。 - Don Neufeld
3
这是糟糕的代码。如果在使用 foo 时抛出异常,它将永远不会被释放。 - y2k
显示剩余6条评论

37
我总是在删除指向的对象之后将指针设置为NULL(现在是nullptr)。
  1. 这可以帮助捕获许多对已释放内存的引用(假设您的平台在对空指针进行解引用时会出错)。

  2. 如果例如您有指针副本,则不会捕获所有对已释放内存的引用。但是,一些总比没有好。

  3. 它将掩盖双重删除,但是我发现这些情况比访问已释放内存的情况要少得多。

  4. 在许多情况下,编译器将对其进行优化。因此,认为它是不必要的这个论点并不能说服我。

  5. 如果您已经使用RAII,则代码中几乎没有delete,因此认为额外的赋值会导致混乱的论点也无法说服我。

  6. 调试时,看到空值而不是旧指针通常很方便。

  7. 如果仍然感到困扰,请改用智能指针或引用。

当资源被释放时(通常仅在编写用于封装资源的RAII包装器的析构函数中),我还将其他类型的资源句柄设置为无资源值。

我曾在一款大型(900万条语句)商业产品(主要使用C语言)上工作。有一次,我们使用宏来将指针置为空指针,当内存被释放时。这立即暴露了许多潜在的错误,并得到了及时修复。据我所记,我们从未遇到过双重释放的错误。

更新: 微软认为这是一种良好的安全实践,并在其SDL政策中推荐使用此方法。 显然,如果您使用/SDL选项进行编译,则MSVC++11将自动删除指针(在许多情况下)。

12

首先,有很多关于此问题和紧密相关主题的现有问题,例如为什么delete不会将指针设置为NULL?

在你的代码中,问题出在(use p)发生了什么。例如,如果你在某个地方有这样的代码:

Foo * p2 = p;

将p设置为NULL实际上没有太大作用,因为您仍然需要关心指针p2。

这并不是说将指针设置为NULL总是毫无意义的。例如,如果p是指向资源的成员变量,该资源的生命周期与包含p的类不完全相同,则将p设置为NULL可能是一种指示资源存在或不存在的有用方式。


1
我同意有时候这并没有帮助,但你似乎在暗示它可能会有积极的危害作用。这是你的本意吗,还是我理解错了? - Mark Ransom
1
指针是否有副本与指针变量是否应设置为NULL无关。将其设置为NULL是一种良好的做法,就像在用餐后清洁餐具一样 - 虽然它不能保护代码免受所有错误的侵害,但它确实促进了良好的代码健康。 - Franci Penov
3
@Franci 看上去有很多人不同意你的看法。如果你在删除原始文件后尝试使用备份文件,是否存在副本显然是相关的。 - anon
4
Franci,有所区别。你清洗餐具是因为你会再次使用它们。你删除指针后就不需要它了。这应该是你做的最后一件事情。更好的做法是尽量避免这种情况。 - GManNickG
1
例如,如果p是指向资源的成员变量,其生命周期与包含p的类不完全相同,则将p设置为NULL可能是一种有用的指示资源存在或不存在的方式。这实际上展示了一个有趣的观点,并接受指针可以是有用的。 - João Portela
显示剩余5条评论

8
如果在delete之后还有更多的代码,那么是的。如果指针在构造函数中被删除或在方法或函数结束时被删除,则不是这样的情况。
这个寓言的目的是在运行时提醒程序员对象已经被删除了。
更好的做法是使用智能指针(共享或作用域),它们可以自动删除其目标对象。

每个人(包括原问题提出者)都同意智能指针是正确的选择。代码会不断演进。当你第一次编写删除代码时,可能不会有更多的代码,但随着时间的推移,情况很可能会发生改变。在赋值中加入智能指针可以帮助应对这种情况(而且在此期间几乎不会产生任何成本)。 - Adrian McCarthy

3
正如其他人所说,delete ptr; ptr = 0;并不会让恶魔从你的鼻子里飞出来。然而,它确实鼓励将ptr用作某种标志。代码变得杂乱无章,充斥着delete和将指针设置为NULL。下一步是在代码中散布if (arg == NULL) return;以防止意外使用NULL指针。问题出现在检查NULL是否成为了检查对象或程序状态的主要手段。
我相信,在某个地方使用指针作为标志存在代码异味,但我还没有找到。

9
使用指针作为标志并没有什么问题。如果你正在使用指针,并且NULL不是有效的值,那么你可能应该使用引用(reference)代替。 - Adrian McCarthy

2
明确在删除后将指针显式置空,强烈建议读者认为该指针表示的是概念上可选的内容。如果我看到这样做了,我会开始担心在源代码中无论哪里使用该指针都应首先针对NULL进行测试。
如果这正是您的意思,最好在源代码中明确使用类似boost::optional的东西来表达。
optional<Foo*> p (new Foo);
// (use p.get(), but must test p for truth first!...)
delete p.get();
p = optional<Foo*>();

但是,如果您真的想让人们知道指针已经“失效”,我完全同意那些认为最好的方法是使其超出作用域。然后,您可以使用编译器来防止运行时发生错误引用的可能性。
这是所有C++中重要的部分,不应该被丢弃。 :)

2
我会稍微改变你的问题:

你会使用未初始化的指针吗?也就是说,你没有将其设置为NULL或分配它所指向的内存?

有两种情况可以跳过将指针设置为NULL:

  • 指针变量立即超出作用域
  • 你已经重载了指针的语义,并且不仅将其值用作内存指针,还将其用作键或原始值。然而,这种方法会遇到其他问题。

同时,争论将指针设置为NULL可能会隐藏错误,对我来说听起来像是争论你不应该修复一个bug,因为修复可能会隐藏另一个bug。如果指针未设置为NULL,则可能显示的唯一错误是尝试使用指针的那些错误。但将其设置为NULL实际上会导致与释放内存后使用它时显示的完全相同的错误,不是吗?


1
(A) "听起来像是争论你不应该修复一个错误" 不将指针设置为NULL不是一个错误。(B) "但将其设置为NULL实际上会导致完全相同的错误" 不。将其设置为NULL会隐藏_double delete_。(C) 总结:将其设置为NULL会隐藏double delete,但会暴露过时引用。不将其设置为NULL可以隐藏过时引用,但会暴露double delete。双方都认为真正的问题是修复过时引用和double delete。 - Mooing Duck

2

如果你没有其他的限制强迫你在删除指针后将其设置为NULL或不设置(Neil Butterworth提到了其中一种限制),那么我的个人偏好是让它保持不变。

对我来说,问题不是“这是一个好主意吗?”,而是“通过这样做,我会阻止或允许哪些行为成功执行?”例如,如果这使其他代码能够看到指针不再可用,为什么其他代码甚至在释放指针后还尝试查看已释放的指针?通常,这是一个错误。

它也会做更多不必要的工作,同时也会阻碍事后调试。在不需要内存后,你触碰内存越少,就越容易找出为什么会崩溃。很多时候,我依赖于内存在特定错误发生时处于类似状态的事实来诊断和修复该错误。


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