165

我会首先说,使用智能指针,你永远不必担心这个。

以下代码有什么问题?

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

这是由对另一个问题的回答和评论引发的。Neil Butterworth的一条评论引发了一些支持:

在删除之后将指针设置为 NULL 并不是 C++ 中普遍的好习惯。有时它是一件好事,有时它是无意义的并且可以隐藏错误。

在很多情况下它都无济于事。但根据我的经验,它不会受到伤害。有人给我解惑。

4

18 回答 18

103

将指针设置为 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共享所有权或其他智能指针实现,具体取决于您的需要。

于 2009-12-18T23:01:39.510 回答
60

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

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

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

于 2009-12-18T22:55:08.423 回答
45

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

{
    Foo* pFoo = new Foo;
    // use pFoo
    delete pFoo;
}
于 2009-12-18T22:49:38.210 回答
33

在删除它指向的对象后,我总是设置一个指向NULL(now nullptr) 的指针。

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

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

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

  4. 在许多情况下,编译器会对其进行优化。所以没有必要的论点并不能说服我。

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

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

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

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

我从事大型(900 万条语句)商业产品(主要是 C 语言)。有一次,当内存被释放时,我们使用宏魔法来清空指针。这立即暴露了许多潜伏的错误,这些错误很快得到了修复。据我所知,我们从未遇到过双重释放错误。

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

于 2009-12-19T00:24:35.367 回答
12

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

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

Foo * p2 = p;

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

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

于 2009-12-18T22:56:17.503 回答
7

如果后面有更多代码delete,是的。当指针在构造函数中或在方法或函数的末尾被删除时,否。

这个比喻的重点是在运行时提醒程序员对象已经被删除。

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

于 2009-12-18T22:52:33.887 回答
3

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

我确信在某处使用指针作为标志存在代码气味,但我还没有找到。

于 2009-12-18T23:21:24.150 回答
2

我会稍微改变你的问题:

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

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

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

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

于 2009-12-18T23:04:50.033 回答
2

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

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

它还做了比必要的工作更多的工作,并阻碍了事后调试。在不需要内存后,您越少接触内存,就越容易找出崩溃的原因。很多时候,我依靠内存处于与特定错误发生时相似的状态来诊断和修复所述错误的事实。

于 2009-12-18T23:26:47.383 回答
2

在 delete 之后显式置空强烈地向读者表明指针表示的东西在概念上是可选的。如果我看到这样做了,我会开始担心在源代码中的任何地方都使用指针,应该首先针对 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++ 洗澡水中的婴儿,不应该把它扔掉。:)

于 2009-12-19T01:17:08.673 回答
2

在具有适当错误检查的结构良好的程序中,没有理由将其分配为空。0在这种情况下,它作为一个普遍公认的无效值独立存在。努力失败,很快就会失败。

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

如果程序员想要引入一个可能为 null 的指针作为特殊值的使用,并围绕它编写所有必要的回避,这是他们故意引入的复杂性。隔离越好,您越早发现滥用案例,它们传播到其他程序的可能性就越小。

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

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

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

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

于 2012-02-28T07:43:22.367 回答
1

让我扩展您已经提出的问题。

这是您以要点形式提出的问题:


在删除之后将指针设置为 NULL 并不是 C++ 中普遍的好习惯。有时:

  • 这是一件好事
  • 以及它毫无意义并且可以隐藏错误的时候。

但是,没有时候这很糟糕!你不会通过显式地清空它来引入更多的错误,你不会泄漏内存,你不会导致未定义的行为发生。

因此,如果有疑问,只需将其设为空即可。

话虽如此,如果你觉得你必须明确地为空一些指针,那么对我来说这听起来你没有足够的拆分方法,并且应该查看称为“提取方法”的重构方法将方法拆分为分开的部分。

于 2009-12-18T22:57:19.033 回答
1

是的。

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

如果你不这样做,有一天你会遇到一些讨厌的指针取消引用错误。

我总是使用宏进行删除:

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

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

您还可以编写“自删除”方法来引用调用代码的指针,因此它们强制调用代码的指针为 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 的内存.

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

于 2009-12-18T23:11:25.010 回答
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 肯定会告诉我更多),这使我认为“如何避免使用智能指针并使用普通指针安全地完成工作”之类的所有问题都已过时。

于 2014-01-27T11:38:52.940 回答
0

总有悬空指针需要担心。

于 2009-12-18T22:57:42.793 回答
0

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

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

编辑:正如 Thomas Matthews 在这个较早的答案中所说,如果在析构函数中删除了指针,则无需为其分配 NULL,因为它不会再次使用,因为对象已经被销毁。

于 2009-12-18T23:02:50.067 回答
0

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

于 2009-12-19T17:14:33.860 回答
0

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

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

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

于 2009-12-19T19:10:43.907 回答