删除指针后使指针为空是一种好的做法吗?

我先说 使用智能指针,你就永远不用担心这个了。

下面的代码有什么问题?

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

这是由 回答和评论提出的另一个问题引起的。 Neil Butterworth的一条评论引起了一些赞扬:

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

在很多情况下,这样做是没有用的。但是以我的经验来看,这样做是没有坏处的。有人可以给我一些启发。

107985 次浏览

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

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

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

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

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

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

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

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

首先,目前在这方面存在很多问题,而且这些问题都与 为什么不删除设置指针为空?紧密相关。

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

Foo * p2 = p;

然后将 p 设置为 NULL 几乎没有什么作用,因为您仍然需要担心指针 p2。

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

让我详细说明一下你已经提出的问题。

以下是你在问题中的要点:


在删除后将指针设置为 NULL 并不是 C + + 中的普遍良好做法:

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

但是,有 没有时间当这是 很糟糕!你会通过显式取消 没有引入更多的 bug,你不会 漏水内存,你不会 导致不明确的行为发生。

所以,如果有疑问,就取消它。

话虽如此,如果你觉得你必须显式地为某个指针赋值,那么对我来说,这听起来就像你没有把一个方法拆分得足够,应该看看称为“提取方法”的重构方法,把这个方法拆分成不同的部分。

总是有 悬挂指针需要担心。

将指针设置为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的一个参数是,这样做只会掩盖双重删除 bug,并使它们无法处理。

显然,最好不要有双重删除错误,但是取决于所有权语义和对象生命周期,这在实践中可能很难实现。比起 UB 我更喜欢屏蔽双删除。

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

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

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

编辑: 正如 Thomas Matthews 在 这个早期的答案中所说,如果在析构函数中删除了一个指针,那么就没有必要将 NULL 赋值给它,因为它不会再被使用,因为对象已经被销毁了。

我稍微改变一下你的问题:

是否使用未初始化的 指示器? 你知道,一个你没有 设置为 NULL 或分配它的内存 指向?

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

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

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

是的。

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

如果你不这样做,你 威尔有一些讨厌的指针解引用错误的一天。

我总是用宏来删除:

#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;
}

编辑

是的,这些技术确实违反了使用宏的一些规则(是的,现在你可能可以通过模板获得同样的结果)——但是通过使用多年的我 永远不会访问死内存——一个你可以面对的最讨厌、最困难和最耗时的调试问题之一。实际上,多年来,他们已经有效地从我介绍给他们的每个团队中消除了一整类错误。

还有很多方法可以实现上述功能——我只是想说明一个想法: 如果人们删除了一个对象,就强迫他们使用一个指针为空,而不是提供一种方法让他们释放内存,不使调用者的指针为空。

当然,上面的示例只是向自动指针迈进了一步。我没有这样建议,因为 OP 特别询问了不使用自动指针的情况。

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

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

如果在删除指针之后,没有其他约束强迫您设置或不设置指针为 NULL (Neil Butterworth提到了一个这样的约束) ,那么我个人倾向于不设置它。

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

它还会做很多不必要的工作,并且阻碍事后调试。在你不需要记忆的时候,你越少触摸它,就越容易弄清楚为什么有些东西会崩溃。很多时候,我依赖于这样一个事实,即内存处于类似于某个特定 bug 发生时的状态,以便诊断和修复该 bug。

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

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

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

  3. 它会屏蔽双重删除,但是我发现这种情况远不如访问已经释放的内存那么常见。

  4. 在许多情况下,编译器会优化它。所以这种认为没有必要的论点并没有说服我。

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

  6. 在调试时,查看 null 值比查看陈旧的指针更方便。

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

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

我曾经做过一个大型(900万份报表)的商业产品(主要是 C 语言)。有一次,我们使用宏魔术在释放内存时使指针失效。这立即暴露了许多潜伏的错误,并迅速得到修复。就我记忆所及,我们从来没有过双自由窃听器。

更新: 微软相信这是一个很好的安全实践,并在他们的 SDL 策略中推荐这种实践。显然,如果使用/SDL 选项编译,MSVC + + 11将自动(在许多情况下) 践踏已删除的指针

在 delete 之后显式为 null 强烈地向读者暗示指针代表某种概念上的 可以选择。如果我看到这样做,我就会开始担心在源代码的任何地方使用的指针都应该首先针对 NULL 进行测试。

如果这是您的实际意思,那么最好在源代码中使用类似于 可选的代码将其显式化

optional<Foo*> p (new Foo);
// (use p.get(), but must test p for truth first!...)
delete p.get();
p = optional<Foo*>();

但是如果你真的想让人们知道指针已经“变质”了,我将100% 同意那些说最好的事情是让它超出范围的人。然后使用编译器来防止在运行时出现错误解引用的可能性。

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

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

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

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

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

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

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

如果程序员想要引入一个可能为空的指针作为一个特殊值,并编写所有必要的规避,这是他们有意引入的一个复杂性。隔离做得越好,就能越早发现滥用的情况,它们就越不能扩散到其他程序中。

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

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

这同样适用于不是 const的指针。跟随指针的值是微不足道的,因为它的作用域非常小,并且会检查和定义不当的使用。如果您的工具集和工程师不能按照快速阅读后的程序执行,或者存在不适当的错误检查或不一致/宽松的程序流,那么您还有其他更大的问题。

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

“有时候,这是一件好事,有时候,这是毫无意义的,可以隐藏错误”

我看到两个问题: 这个简单的代码:

delete myObj;
myobj = 0

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

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

唐 · 纽菲尔德的“最佳实践”并不总是适用。例如,在一个汽车项目中,即使在析构函数中,我们也必须将指针设置为0。我可以想象,在安全至关重要的软件中,这样的规则并不少见。跟随他们比试图说服他们更容易(也更明智) 在代码中使用的每个指针的团队/代码检查程序,使用一行使该指针为空是多余的。

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

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


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

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

因此,这两个问题自然而然地在我脑海中闪现(Herb Sutter 肯定会告诉我更多) ,使得所有类似“如何避免使用智能指针,如何使用普通指针安全地完成工作”这样的问题都变得过时了。