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

Mar*_*som 139 c++ null pointers dynamic-allocation

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

以下代码有什么问题?

Foo * p = new Foo;
// (use p)
delete p;
p = NULL;
Run Code Online (Sandbox Code Playgroud)

这是由另一个问题的回答和评论引发的.Neil Butterworth的一条评论产生了一些赞成:

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

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

Håv*_*d S 80

将指针设置为0(在标准C++中为"null",从C定义的NULL定义有些不同)避免了双删除时的崩溃.

考虑以下:

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

鉴于:

Foo* foo = new Foo();
delete foo; // Deletes the object
delete foo; // Undefined behavior 
Run Code Online (Sandbox Code Playgroud)

换句话说,如果你没有将删除的指针设置为0,如果你正在进行双重删除,你将遇到麻烦.反对在删除后将指针设置为0的参数是这样做只是掩盖双删除错误并使它们未处理.

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

最后,关于管理对象分配的旁注,我建议您根据需要查看std::unique_ptr严格/单一所有权,std::shared_ptr共享所有权或其他智能指针实现.

  • 这里的问题是你有一个双删除的事实.使指针为NULL只是隐藏了它不会纠正它或使其更安全的事实.Imagaine是一名mainainer在一年后回来并看到foo被删除了.他现在相信他可以重用指针,不幸的是他可能会错过第二次删除(它甚至可能不在同一个函数中),现在指针的重复使用现在被第二次删除所破坏.第二次删除后的任何访问现在都是一个主要问题. (26认同)
  • 您的应用程序不会总是在双重删除时崩溃.根据两个删除之间发生的情况,任何事情都可能发生.最有可能的是,你会破坏你的堆,并且稍后你会在一段完全不相关的代码中崩溃.虽然段错误通常比默默地忽略错误更好,但在这种情况下不能保证段错误,并且它的实用性有问题. (12认同)
  • 将指针设置为"NULL"确实可以掩盖双删除错误.(有些人可能认为这个掩码实际上是一个解决方案 - 它是,但不是一个非常好的,因为它没有找到问题的根源.)但是不设置为NULL掩盖远(FAR!)更多删除后访问数据的常见问题. (10认同)
  • 但是话又说回来,不推荐使用*是*正确的术语。:p (2认同)

jal*_*alf 54

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

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

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

  • 所以你提出的论点是,首先不应该有一个原始指针,任何涉及指针的东西都不应该被称为"良好实践"这个词吗?很公平. (15认同)
  • 好吧,或多或少.我不会说*没有任何*涉及原始指针可以被称为一个好习惯.只是它是例外而不是规则.通常,指针的存在表明在更深层次上存在错误. (7认同)
  • 我不同意 - 有时指针很好用.例如,堆栈上有2个变量,您想要选择其中一个变量.或者您想将可选变量传递给函数.我会说,你永远不应该将原始指针与`new`结合使用. (7认同)
  • 当指针超出范围时,我看不出*任何*或任何人可能需要处理它. (4认同)
  • 但要回答当前的问题,不,我没有看到设置指向null的指针如何*导致*错误. (3认同)
  • “你自己的包装”是什么意思?为什么不应该使用 RAII 作为该包装器?它是一个创可贴,因为它修复了仅因为您没有修复*底层*问题(变量仍在范围内)而发生的错误 (3认同)
  • @Adrian:是的,但是实现不会将指针暴露在它所在的范围之外.如果指针是您正在编写的RAII包装器的私有成员,那么它将在包装器执行时超出范围,因此没有任何设置为NULL. (2认同)

Don*_*eld 42

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

{
    Foo* pFoo = new Foo;
    // use pFoo
    delete pFoo;
}
Run Code Online (Sandbox Code Playgroud)

  • 是的,这是最好的选择.但是不回答这个问题. (24认同)
  • 是的,RAII是你的朋友.将它包装在一个类中,它变得更加简单.或者不要使用STL自己处理内存! (17认同)
  • 我的例子是故意的.例如,对象可能是由工厂创建的,而不是新的,在这种情况下,它不能进入​​堆栈.或者它可能不是在范围的开头创建的,而是位于某个结构中.我所说明的是这种方法会在**编译时**发现任何误用指针,而NULL会在**运行时**发现任何误用. (4认同)
  • 这似乎只是使用函数范围期间的副产品,并没有真正解决问题.当你使用指针时,你通常会将它们的副本多层传递给它们,然后你的方法在解决问题方面毫无意义.虽然我同意好的设计可以帮助你隔离错误,但我不认为你的方法是达到这个目的的主要手段. (3认同)
  • 想想看,如果你能做到这一点,为什么你不会忘记堆并将所有内存从堆栈中拉出来? (2认同)
  • 这是糟糕的代码.如果在使用`foo`时抛出异常,它将永远不会被释放. (2认同)

Adr*_*thy 30

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

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

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

  3. 它将掩盖双重删除,但我发现这些远远不如访问已释放的内存.

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

  5. 如果你已经在使用RAII,那么delete你的代码中没有多少s开头,所以额外的赋值导致混乱的论点并没有说服我.

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

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

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

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

更新: Microsoft认为这是安全性的良好实践,并建议在其SDL策略中实践.显然,如果使用/ SDL选项进行编译,MSVC++ 11将自动(在许多情况下)重写已删除的指针.


小智 12

首先,关于这个和密切相关的主题存在很多现有问题,例如为什么不删除将指针设置为NULL?.

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

Foo * p2 = p;
Run Code Online (Sandbox Code Playgroud)

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

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

  • 弗朗西,有区别.你洗碗是因为你再次使用它们.删除后不需要指针.这应该是你做的最后一件事.*更好的*练习是完全避免这种情况. (3认同)
  • @Franci很多人似乎不同意你的意见.如果您在删除原件后尝试使用该副本,是否确实存在副本. (2认同)

Tho*_*ews 7

如果之后有更多代码delete,是的.在构造函数中或在方法或函数末尾删除指针时,编号.

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

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