“delete p; p = NULL(nullptr);”是一种反模式吗?

15
在SO上搜索时,我偶然发现这个问题,并且对于得票最高答案的一个评论(针对该答案的第五条评论)建议delete p; p = NULL;是一种反模式。我必须承认,我经常使用它,并有时/大多数情况下与检查if (NULL != p)配对使用。该人本人似乎建议它(请参见destroy()函数示例),所以我真的很困惑为什么它可能被认为是一种可怕的反模式。我出于以下原因使用它:
  • 当我释放资源时,我还想使其无效以防止进一步使用,而NULL是表示指针无效的正确工具
  • 我不想留下悬空指针
  • 我想避免双重/多重释放错误-删除空指针就像nop一样,但删除悬空指针就像“自杀”一样

请注意,我不是在问关于"this"指针的问题,并且假设我们不生活在完美的C++世界中,需要维护旧代码,因此请不要建议任何智能指针:)。


1
哦,是的。任何一个像样的调试分配器都会告诉你当你释放内存两次时。你可以通过将指针设置为null来避免这种情况。你不想让自己的腿被炸掉吧。所以要么什么也不做,要么将其设置为始终生成处理器故障的值。 - Hans Passant
我认为它并不是通常意义上的“模式”,因为它不是一种设计方法;它相当局部。但如果这是阻止您两次删除某些内容的唯一障碍,那么这可能是一个不好的迹象。 - Owen
刚刚发现这个与同一主题相关的问题 (https://dev59.com/MXI-5IYBdhLWcg3wSGQB)。 - celavek
他并不是一般性地推荐这样做,他是在说如果你要经常这样做,那就把它变成一个函数:“如果你认为清零指针很重要…” - Bill
@Bill 我并没有说推荐,而是建议:);在链接的上下文中,他确实建议这样做。 - celavek
还有一个 https://dev59.com/XHA75IYBdhLWcg3w3NLf ... 看来我没有用正确的方式进行搜索。 - celavek
7个回答

15

是的,我不建议这样做。

原因是将指针设置为null只在非常有限的情况下有帮助。如果您处于析构函数中,在析构函数执行后,指针本身就不存在了,这意味着它是否为null并不重要。

如果指针被复制,将p设置为null将不会设置其余指针,那么您可能会遇到相同的问题,而且会出现额外的问题,即您期望找到已删除的指针为null,但对于为什么指针变成非零却没有对象存在却无法理解....

此外,它可能会隐藏其他错误,例如,如果您的应用程序尝试多次删除指针,则将指针设置为null的效果是第二次删除将转换为无操作,并且虽然应用程序不会崩溃,但逻辑中的错误仍然存在(考虑稍后的编辑在未失败的delete之前访问指针...怎么可能失败?


关于你的第二点,这就是为什么你应该在代码中始终具有明确定义的所有权语义以避免这种情况。强制程序崩溃并不能替代知道谁拥有指针。关于你的第三点,当你进行防御性编程时,可能会有两个地方如果指针仍然存在,则可能删除指针,这是完全有效的。 - Chris Walton
@arke:我同意,我的意思是相反的:将指针设置为null以避免程序崩溃并不能替代明确定义的所有权语义 - David Rodríguez - dribeas
我完全同意你提出的第一和第三点,但是对于第二点,如果我通过其中一个指针删除内存,然后尝试通过副本访问它,无论是否将原始指针设置为NULL,这都是完全错误的事情。 - celavek
你第一个点的情况在多线程环境下安全吗? - celavek
1
@celavek:当析构函数被调用时,不应再有其他线程尝试使用它。对象的生命周期始于其构造函数结束并在其析构函数开始时结束,在其生命周期之外访问对象是未定义行为。因此,是的,David在第一点上的建议是正确的:如果您在此时在另一个线程中使用对象,则已经调用了未定义的行为,并且将指针设置为NULL不会产生任何效果。 - Matthieu M.
虽然第一点的背景有些局限,但我并不完全同意这种轻视的情绪。将这样的指针设置为null可以帮助捕获错误,如果析构函数调用其他可能错误地尝试访问该成员的成员函数。 (当然,所有这些都可以通过使用智能指针来避免...) - jamesdlin

7

我建议这样做。

  • 显然,在指针为NULL时是有效的。当然,这意味着如果在其他地方使用它,则必须进行检查。
  • 即使指针在技术上可能不为NULL,在实际场景中,当客户向您发送崩溃转储时,它也会有所帮助。如果它为NULL并且不应该是(并且没有使用assert()进行测试),那么很容易看出这是问题-您将在类似于mov eax,[edx + 4]之类的东西上崩溃,您将看到edx为0,并且您知道问题所在。另一方面,如果您不将指针设置为NULL,但已删除,则可能发生各种事情。它可能工作,它可能立即崩溃,它可能以后崩溃,它可能显示奇怪的东西-此时发生的任何事情都是软非确定性的。
  • 防御性编程是王道。这包括无端地将指针设置为NULL,即使您认为您不必这样做,并在一些地方执行额外的NULL检查,即使您在技术上知道这不应该发生。
  • 即使您有指针两次删除的逻辑错误,安全处理而不是崩溃更好。这可能意味着您进行额外的检查,您可能需要记录该操作,甚至可能优雅地结束程序,但这不仅仅是崩溃。客户不喜欢那样。

个人而言,我使用一个内联函数,将指针作为引用并将其设置为NULL。


关于第二点:如果您要遵循这条路线,那么您应该建议不要设置为null,而是设置为reinterpret_cast<type*>(1),在大多数架构中都是无效指针(或者任何其他奇数)。将其设置为0意味着您将无法确定指针是从未分配还是已释放。在崩溃/隐藏错误方面,我相信进行广泛的测试,如果您进行测试,那么在客户首次获得软件之前就会发生崩溃。 - David Rodríguez - dribeas

4
不,这不是一种反模式。
将指针设置为 NULL 是一个非常好的方式,可以表明指针不再指向任何有效的东西。实际上,这正是 NULL 值的意图。
如果要避免反模式,则应该避免没有简单明确的规则/约定来管理程序内存。这些规则并不重要,只要它们能够避免泄漏和崩溃,并且您能够始终如一地遵循它们即可。

2
你绝对没有理解整个反模式的概念。如果程序正确地管理内存,为什么要在将指针设置为null时浪费CPU周期?就让它保持原样吧。如果你的编译器有一些生成诊断版本的方式,为什么要让它更难呢? - Hans Passant
如果您的程序有其他简单的方法来知道指针是否有效(例如,父对象仅在构造函数中分配子对象,并仅在析构函数中删除它),那么当然不需要将指针设置为NULL。但是,如果子对象可能在任何特定点上分配或未分配(例如,它是一个按需构造的子对象),则您需要一个单独的pointer_is_currently_valid布尔值,或者当指针没有指向有效对象时,可以将其设置为NULL。 - Jeremy Friesner
这不是我们正在讨论的内容。 - Hans Passant

4
这取决于指针的使用方式。如果所有能够看到指针的代码都应该“知道”它不再有效——特别是在析构函数中,因为指针即将超出作用域——那么就没有必要将其设置为null。
另一方面,指针可能表示一个有时存在、有时不存在的对象,并且您有像if(p) {p->doStuff();}这样的代码来处理它存在时的对象。在这种情况下,删除对象后显然应将其设置为null。
后一种情况中重要的区别在于,指针变量的生命周期比它(有时)指向的对象的生命周期长得多,而它的null-ness承载着一些重要的含义,必须传达给程序的其他部分。

3

我认为反模式不是 delete p; p = NULL;,而是 assert(this != NULL)

我使用你所说的反模式有两个原因 - 首先是为了增强糟糕代码崩溃时无法隐藏的可能性,并使核心问题在调试中更加明显。其次,我不会仅仅因为可能会捕获一些东西就在我的代码中添加 assert


断言(this=null)也有点重复的代码。如果this==null,你将立即崩溃,那么为什么还要调用它呢? - Edward
@Edward,你不会总是立即崩溃——你需要访问成员变量或调用虚函数。但一旦你这样做,通常很明显你为什么会崩溃。 - Mark Ransom

3
尽管我认为@Mark Ransom的想法差不多正确,但我认为比起仅仅使用assert(this!=NULL),还存在更加根本的问题。
我认为更重要的是,你经常使用裸指针和直接使用new,这就足以说明问题。虽然这并不一定是代码味道/反模式/等等中的一种,但它往往指向的是类似于C语言的不必要的代码,并且没有充分利用标准库中容器等功能。
即使标准容器不满足您的需求,您仍然可以将指针包装成足够小、足够简单的程序包中,这些技术就不再相关了——您已将对该指针的访问限制在极小的代码范围内,因此只需浏览一下即可确保它仅在有效时使用。正如Hoare所说,有两种做事的方式:一种是让代码非常简单,没有明显的缺陷;另一种是让代码非常复杂,没有明显的缺陷。在我看来,这种技术只有在您已经处理后一种情况时才会显得相关。
最终,我认为这样做的愿望基本上是在承认失败——而不是试图确保代码的正确性,你做的相当于在沼泽旁边设置了一个电子灭蚊器。它可以在小范围内减少错误的出现率,但如果还有更多的错误可以繁殖,那么对总体人口的影响太小以至于无法测量。

我认为更有意义的是,你经常使用原始指针和new(直接)以至于需要关注这个问题。事实上,我没有选择;我在一个非常庞大的代码库的上下文中工作,该代码库不使用智能指针或任何其他正确管理动态分配内存的技术;尝试做正确的事情需要额外的开发时间,而我没有这个时间。我认为你在这里提出的论点是完全正确的,对于“新代码”,我总是尽力采用适当的技术,并努力学习如何做到正确。 - celavek
有时候我不得不承认失败,希望在下一场战斗中能够获胜(耶,听起来像是出自柯埃霍的书)。 - celavek

0
原因是将额外的设置为null只会在非常有限的情况下有所帮助。如果您处于析构函数中,在析构函数执行后,指针本身将不存在,这意味着它是否为null并不重要。
必须更正该语句,因为在C++中它是错误的。
对象被销毁时,其它函数可能会被调用(因为某种原因销毁过程需要它们)。虽然通常被视为丑陋的做法,但并不总有好的解决方案。
因此,清除指针可能是避免问题的唯一良好解决方案(即这些其他函数被调用时可以测试对象是否有效以便使用)。
然而,在C ++中一个好的想法是使用智能对象(也许就是你所说的)。更或少来说,这是一个持有对象引用的类,在析构函数被触发时确保该对象被释放,并且不会添加多个对象到一个对象中进行同时销毁(尽管结果相同,但这样更加清洁).

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