删除指针后将指针NULL化是一种好习惯吗?

时间:2009-12-18 22:48:06

标签: c++ pointers null dynamic-allocation

我首先要说的是,使用智能指针,你永远不必担心这一点。

以下代码有什么问题?

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

an answer and comments引发了另一个问题。来自Neil Butterworth的一条评论产生了一些赞成票:

  

在删除后将指针设置为NULL不是C ++中的通用优良做法。有时候做一件好事,有时候没有意义,可以隐藏错误。

有很多情况下它无济于事。但根据我的经验,它不会伤害。有人开导我。

18 个答案:

答案 0 :(得分:78)

将指针设置为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,那么如果你正在进行双重删除,你将遇到麻烦。反对在删除后将指针设置为0的参数是这样做只是掩盖双重删除错误并使它们未处理。

最好不要有双删除错误,显然,但根据所有权语义和对象生命周期,这在实践中很难实现。我更喜欢UB上的蒙面双删除错误。

最后,关于管理对象分配的旁注,我建议你看一下std::unique_ptr的严格/单一所有权,std::shared_ptr用于共享所有权,或者另一个智能指针实现,这取决于你的需求。

答案 1 :(得分:53)

在删除指向的指针之后将指针设置为NULL肯定不会受到伤害,但它通常对一个更基本的问题有点助手:为什么你首先使用指针?我可以看到两个典型的原因:

  • 你只是想在堆上分配一些东西。在这种情况下,将它包装在RAII对象中会更加安全和清洁。当您不再需要该对象时,结束RAII对象的范围。这就是std::vector的工作方式,它解决了意外将指针留给释放的内存的问题。没有指针。
  • 或许你想要一些复杂的共享所有权语义。从new返回的指针可能与调用delete的指针不同。在此期间,多个对象可能同时使用了该对象。在这种情况下,最好使用共享指针或类似的东西。

我的经验法则是,如果你在用户代码中留下指针,你就是在做错了。指针不应该首先指向垃圾。为什么没有一个对象负责确保其有效性?为什么它的范围在指向对象时不会结束?

答案 2 :(得分:42)

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

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

答案 3 :(得分:28)

在删除指向的对象后,我总是设置指向NULL(现在为nullptr)的指针。

  1. 它可以帮助捕获许多对释放内存的引用(假设你的平台在空指针的deref上出错)。

  2. 例如,如果你有指针的副本,它将不会捕获所有对freed内存的引用。但有些人比没有人好。

  3. 它会屏蔽双重删除,但我发现它们远比访问已释放的内存少得多。

  4. 在许多情况下,编译器会优化它。因此,不必要的说法并不能说服我。

  5. 如果您已经在使用RAII,那么您的代码中的delete开头并不多,因此额外分配导致混乱的论点并不能说服我。

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

  7. 如果这仍然困扰你,请使用智能指针或参考。

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

    我参与了一项大型(900万份报表)商业产品(主要是C)。有一次,我们使用宏魔法在释放内存时使指针无效。这立即暴露了很多潜伏的错误,并迅速修复。据我所知,我们从未有过双重免费的错误。

    更新:Microsoft认为这是一种安全的良好做法,并建议在其SDL政策中采用这种做法。显然,如果使用/ SDL选项进行编译,MSVC ++ 11将stomp the deleted pointer自动(在许多情况下)。

答案 4 :(得分:12)

首先,关于这个和密切相关的主题存在很多现有问题,例如Why doesn't delete set the pointer to NULL?

在您的代码中,问题是(使用p)。例如,如果某个地方有这样的代码:

Foo * p2 = p;

然后将p设置为NULL可以完成很少,因为您仍然需要担心指针p2。

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

答案 5 :(得分:7)

如果delete后面有更多代码,请执行。在构造函数中或在方法或函数末尾删除指针时,编号

这个比喻的目的是在运行期间提醒程序员该对象已被删除。

更好的做法是使用自动删除目标对象的智能指针(共享或范围)。

答案 6 :(得分:3)

正如其他人所说,delete ptr; ptr = 0;不会导致恶魔飞出你的鼻子。但是,它确实鼓励使用ptr作为各种标志。代码变得充满delete并将指针设置为NULL。下一步是通过代码分散if (arg == NULL) return;以防止意外使用NULL指针。一旦针对NULL的检查成为检查对象或程序状态的主要方法,就会出现问题。

我确信在某个地方使用指针作为标志有一种代码味道,但我还没找到。

答案 7 :(得分:2)

在具有适当错误检查的结构良好的程序中,没有理由将其赋值为null。在这种情况下,0是一个普遍认可的无效价值。努力工作并尽快失败。

许多反对分配0的论点都暗示可以隐藏错误或使控制流复杂化。从根本上说,这可能是一个上游错误(不是你的错(抱怨不好的双关语))或代表程序员的另一个错误 - 甚至可能表明程序流程变得过于复杂。

如果程序员想要引入一个可能为null的指针作为特殊值并写下所有必要的躲避,那就是他们故意引入的并发症。检疫越好,越早发现滥用情况,就越不能传播到其他计划中。

可以使用C ++功能设计结构良好的程序以避免这些情况。您可以使用引用,或者您可以只说“传递/使用null或无效参数是一个错误” - 这种方法同样适用于容器,例如智能指针。增加一致和正确的行为可以阻止这些错误的发生。

从那里开始,你只有一个非常有限的范围和上下文,其中可能存在(或允许)空指针。

同样可以应用于非const的指针。跟随指针的值是微不足道的,因为它的范围很小,并且检查和定义了不正确的使用。如果您的工具集和工程师在快速阅读后无法遵循该程序,或者存在不适当的错误检查或不一致/宽松的程序流程,那么您还有其他更大的问题。

最后,当您想要引入错误(涂鸦),检测对释放的内存的访问以及捕获其他相关的UB时,您的编译器和环境可能会有一些防范。您也可以在程序中引入类似的诊断,通常不会影响现有程序。

答案 8 :(得分: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*>();

但如果你真的希望人们知道指针“变坏了”,我会与那些说最好的事情就是让它超出范围的人达成100%的协议。然后,您正在使用编译器来防止在运行时出现错误解除引用的可能性。

这是所有C ++洗澡水中的婴儿,不应该扔掉它。 :)

答案 9 :(得分:2)

我会稍微改变你的问题:

  

你会使用未初始化的吗?   指针?你知道,一个你没有   设置为NULL或分配内存   指向?

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

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

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

答案 10 :(得分:2)

如果您没有其他约束强制您在删除它之后设置或不将指针设置为NULL(Neil Butterworth提到了一个这样的约束),那么我个人的偏好就是保留它。

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

它还做了比必要工作更多的工作以及阻碍事后调试。你不需要它后触摸内存越少,就越容易找出崩溃的原因。很多时候,我依赖的事实是,内存处于与诊断和修复错误的特定错误时类似的状态。

答案 11 :(得分:1)

“有时候这是好事,有时候没有意义,可以隐藏错误”

我可以看到两个问题: 那个简单的代码:

delete myObj;
myobj = 0

成为多线程环境中的for-liner:

lock(myObjMutex); 
delete myObj;
myobj = 0
unlock(myObjMutex);

Don Neufeld的“最佳实践”并不总是适用。例如。在一个汽车项目中,即使在析构函数中,我们也必须将指针设置为0。我可以想象在安全关键软件中这样的规则并不少见。遵循它们比试图说服更容易(也更明智) 代码中每个指针使用的团队/代码检查器,使该指针归零的行是多余的。

另一个危险是在使用代码的异常中依赖这种技术:

try{  
   delete myObj; //exception in destructor
   myObj=0
}
catch
{
   //myObj=0; <- possibly resource-leak
}

if (myObj)
  // use myObj <--undefined behaviour

在这样的代码中,要么产生资源泄漏并推迟问题,要么进程崩溃。

所以,这两个问题在我脑海中自发地发生(Herb Sutter肯定会说得更多)为我提出了“如何避免使用智能指针,并使用普通指针安全地完成工作”的所有问题都已过时

答案 12 :(得分:1)

它可以做的唯一“伤害”是在你的程序中引入低效率(一种不必要的存储操作) - 但是在大多数情况下,这种开销与分配和释放内存块的成本相比是微不足道的。

如果你不这样做,你一天有一些讨厌的指针derefernce错误。

我总是使用宏来删除:

#define SAFEDELETE(ptr) { delete(ptr); ptr = NULL; }

(类似于数组,free(),释放句柄)

您还可以编写“self delete”方法来引用调用代码的指针,这样它们就会强制调用代码的指针为NULL。例如,要删除许多对象的子树:

static void TreeItem::DeleteSubtree(TreeItem *&rootObject)
{
    if (rootObject == NULL)
        return;

    rootObject->UnlinkFromParent();

    for (int i = 0; i < numChildren)
       DeleteSubtree(rootObject->child[i]);

    delete rootObject;
    rootObject = NULL;
}

修改

是的,这些技术确实违反了关于使用宏的一些规则(是的,现在你可能会用模板获得相同的结果) - 但是通过使用多年我永远不会访问死亡内存 - 调试您可能面临的问题的最恶劣,最困难和最耗时的事情之一。在多年的实践中,他们已经有效地消除了我介绍过的每个团队中的一类错误。

还有很多方法可以实现上面的内容 - 我只是想说明如果他们删除了一个对象,强制人们将NULL指针为NULL,而不是为他们提供释放不为NULL的内存的方法来电者的指针。

当然,上面的例子只是迈向自动指针的一步。我没有建议,因为OP专门询问不使用自动指针的情况。

答案 13 :(得分:1)

让我展开你已经提到的问题。

以下是您用问题点形式提出的问题:


在删除后将指针设置为NULL不是C ++中的通用优良做法。有时候:

  • 做一件好事
  • 以及没有意义的时候,可以隐藏错误。

然而,当时,没有时间!您将通过显式归零来引入更多错误,您将不会泄漏内存,您将不会导致未定义的行为发生。

所以,如果有疑问,请将其归零。

话虽如此,如果你觉得你必须显式地忽略一些指针,那么对我来说这听起来好像你没有足够的分割方法,应该看一下名为“Extract method”的重构方法来拆分将方法分成不同的部分。

答案 14 :(得分:0)

如果要在再次使用之前重新分配指针(取消引用它,将其传递给函数等),使指针为NULL只是一个额外的操作。但是,如果您不确定它是否会在重新使用之前重新分配,则将其设置为NULL是一个好主意。

正如许多人所说,使用智能指针当然要容易得多。

编辑:正如Thomas Matthews在this earlier answer中所说的,如果在析构函数中删除指针,则不需要为它分配NULL,因为它不会被再次使用,因为该对象正在被销毁已经

答案 15 :(得分:0)

我可以想象在删除它之后将指针设置为NULL在极少数情况下是有用的,在这种情况下存在在单个函数(或对象)中重用它的合法场景。否则它没有意义 - 指针需要指向有意义的东西,只要它存在 - 句号。

答案 16 :(得分:0)

如果代码不属于应用程序中性能最关键的部分,请保持简单并使用shared_ptr:

shared_ptr<Foo> p(new Foo);
//No more need to call delete

它执行引用计数并且是线程安全的。您可以在tr1(std :: tr1命名空间,#include&lt; memory&gt;)中找到它,或者如果您的编译器没有提供它,请从boost获取它。

答案 17 :(得分:0)

总有Dangling Pointers担心。