删除指针后将其设为NULL是一种好习惯吗?


149

我首先要说的是,使用智能指针,您将永远不必为此担心。

以下代码有什么问题?

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

这是由另一个问题的答案和评论引起的。尼尔·巴特沃思Neil Butterworth)的一则评论产生了一些反对意见:

在C ++中,删除后将指针设置为NULL并不是普遍的好习惯。有时候,这是一件好事,有时却是毫无意义的,并且会掩盖错误。

在很多情况下它无济于事。但是以我的经验,这不会伤害。有人启发我。


6
@Andre:从技术上讲,它是未定义的。可能发生的情况是您访问的内存与以前相同,但是现在可能被其他东西使用。如果您两次删除内存,则很可能会以一种难以发现的方式使程序执行更糟。delete不过,使用空指针是安全的,这是将指针归零可能很好的原因之一。
David Thornley,2009年

5
@AndréPena,它是未定义的。通常,它甚至不可重复。将指针设置为NULL可以使错误在调试时更明显,并且可能使其更可重复。
Mark Ransom

3
@André:没人知道。这是未定义的行为。它可能因访问冲突而崩溃,或者可能覆盖应用程序其余部分使用的内存。语言标准不保证会发生什么,因此一旦发生应用程序,您将无法信任。它可能发射了核导弹或格式化了您的硬盘。它可能会破坏应用程序的内存,或者可能使恶魔从您的鼻子中飞出。所有赌注都关闭了。
jalf

16
飞行的恶魔是功能,而不是错误。
jball

3
这个问题不是重复的,因为另一个问题与C有关,而这个问题与C ++有关。许多答案取决于智能指针之类的东西,而C ++则无法提供。
Adrian McCarthy

Answers:


84

将指针设置为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共享所有权或其他智能指针实现。


13
您的应用程序不会总是在两次删除时崩溃。根据两次删除之间发生的情况,可能会发生任何事情。最有可能的是,您将破坏堆,并且稍后将在完全不相关的代码段中崩溃。通常,segfault比沉默地忽略错误要好,但在这种情况下不能保证segfault的有效性。
亚当·罗森菲尔德2009年

28
这里的问题是您有两次删除的事实。将指针设为NULL只是隐藏了以下事实:它无法更正它或使其更安全。Imagaine一年后回来了,发现foo被删除了。现在,他相信自己可以重新使用指针,但不幸的是,他可能会错过第二次删除操作(它甚至可能不在同一函数中),现在第二次删除操作破坏了指针的重复使用。现在,第二次删除后的任何访问都是一个主要问题。
马丁·约克

11
的确,将指针设置为NULL可以掩盖双重删除错误。(有些人可能认为此掩码实际上是一种解决方案,它是一种解决方案,但不是很好的解决方案,因为它不能解决问题的根源。)但不要将其设置为NULL可以掩盖更远的事实(FAR!)删除数据后访问数据的常见问题。
阿德里安·麦卡锡

AFAIK,std :: auto_ptr在即将到来的C ++标准中被弃用
rafak

我不会说不赞成使用,这听起来好像这个想法已经消失了。而是将其替换为unique_ptr,它auto_ptr使用移动语义完成了尝试做的事情。
GManNickG

55

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

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

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


15
因此,您在争辩说,一开始就不应有原始指针,而涉及该指针的任何东西都不应被“良好实践”一词所蒙蔽?很公平。
Mark Ransom

7
好吧,或多或少。我不会说任何涉及原始指针的东西都不能算是一种好习惯。只是这是例外而不是规则。通常,指针的存在指示着更深层次的问题。
jalf

3
但是要回答眼前的问题,不,我不知道将指针设置为null怎么会导致错误。
jalf

7
我不同意-在某些情况下可以很好地使用指针。例如,堆栈上有2个变量,您想选择其中之一。或者您想将可选变量传递给函数。我要说的是,永远不要将原始指针与结合使用new
rlbond

4
当指针超出范围时,我看不到任何东西或任何人可能需要处理它。
jalf

43

我有一个更好的最佳实践:尽可能结束变量的作用域!

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

17
是的,RAII是您的朋友。将其包装在一个类中,它将变得更加简单。或完全不使用STL处理内存!
布赖恩

24
是的,的确是最佳选择。虽然不回答问题。
Mark Ransom

3
这似乎只是使用函数作用域期间的副产品,并没有真正解决问题。当使用指针时,通常是将它们的副本传递到更深的几层,然后您的方法对于解决该问题实际上是毫无意义的。尽管我同意好的设计可以帮助您隔离错误,但我认为您的方法不是达到此目的的主要手段。
圣哈辛托

2
想一想,如果您能做到这一点,为什么不忘了堆并把所有内存从堆栈中拉出来呢?
圣哈辛托2009年

4
我的示例是故意最小的。例如,不是新对象,而是对象是由factory创建的,在这种情况下,它不能进入​​堆栈。也许它不是在作用域的开头创建,而是位于某些结构中。我要说明的是,这种方法将在编译时发现任何滥用该指针的方法,而将其为NULL将在运行时发现任何滥用的指针。
唐·诺伊费尔德

31

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

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

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

  3. 它将屏蔽两次删除,但是我发现它们远不及访问已释放的内存。

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

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

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

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

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

我开发了一个大型(900万条语句)商业产品(主要是C语言)。一方面,当释放内存时,我们使用宏魔术来使指针无效。这立即暴露出许多潜伏的错误,这些错误已得到及时修复。据我所记得,我们从未遇到过双重释放错误。

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


12

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

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

Foo * p2 = p;

然后将p设置为NULL几乎无法完成,因为您仍然需要担心指针p2。

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


1
我同意有时它无济于事,但您似乎暗示它可能会带来积极的危害。那是您的意图,还是我读错了?
Mark Ransom

1
指针是否存在副本与指针变量是否应设置为NULL无关。出于相同的原因,将其设置为NULL是一个好习惯,当您在晚餐后洗碗是一个好习惯-虽然这并不是防范代码可能包含的所有错误的保障,但它确实可以提高代码的健康度。
弗朗西·佩诺夫2009年

2
@Franci很多人似乎不同意您的看法。如果删除原始文件后尝试使用该副本,是否存在副本当然也很重要。

3
弗朗西,有区别。您洗碗是因为再次使用它们。删除指针后,不需要它。这应该是您要做的最后一件事。更好的做法是完全避免这种情况。
GManNickG

1
您可以重用一个变量,但现在不再是防御性编程了;这就是您设计解决眼前问题的方法。OP正在讨论这种防御风格是否是我们应该争取的东西,而不是我们将指针设置为null。理想情况下,对您的问题,是的!删除指针后,请勿使用!
GManNickG 2009年

7

如果在后面还有更多代码,则可以delete。在构造函数中或方法或函数末尾删除指针时,否。

这个寓言的目的是提醒程序员在运行时该对象已被删除。

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


每个人(包括原始提问者)都同意,智能指针是必经之路。代码不断发展。首次将其删除后,删除后可能没有更多代码,但是随着时间的推移,代码可能会发生变化。分配作业会在发生这种情况时有所帮助(并且在此期间几乎不花任何费用)。
阿德里安·麦卡锡(

3

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

我确定在某处使用指针作为标志有一种代码味道,但是我还没有找到。


9
使用指针作为标志没有错。如果使用的指针NULL不是有效值,则可能应该使用引用。
阿德里安·麦卡锡

2

我会稍微改变您的问题:

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

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

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

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


(A)“听起来像在争论您不应该修复错误。”不将指针设置为NULL并不是错误。(B)“但是将其设置为NULL实际上会导致完全相同的错误。”否。设置为NULL会隐藏double delete。(C)摘要:设置为NULL会隐藏重复删除,但会公开陈旧的引用。不将其设置为NULL可以隐藏陈旧的引用,但可以公开两次删除。双方都同意,真正的问题是修复陈旧引用和重复删除。
Mooing Duck 2013年

2

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

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

它也做比必要的更多的工作,并且阻碍事后调试。不需要内存后,您触摸的内存越少,找出原因的原因就越容易。很多时候,我一直依靠内存处于与发生特定错误时相似的状态来诊断和修复该错误。


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 ++浴池中的婴儿,不应该扔掉它。:)


2

在具有适当错误检查的结构良好的程序中,没有理由将其分配为null。0在这种情况下,它是公认的无效值。努力失败并很快失败。

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

如果程序员希望引入一个可能为null的指针作为特殊值并围绕它编写所有必要的躲避,这就是他们故意引入的一个复杂问题。隔离越好,发现滥用案件的机会就越早,它们传播到其他程序的能力就越低。

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

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

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

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


1

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

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


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

  • 这是一件好事
  • 有时它毫无意义并可能隐藏错误。

但是,没有时候这是不好的!您不会通过显式将其归零来引入更多的错误,不会泄漏内存,也不会导致未定义的行为发生。

因此,如果有疑问,请将其为空。

话虽如此,如果您觉得必须显式地使某些指针为空,那么对我来说,这听起来好像您没有足够地拆分一个方法,应该查看称为“ Extract method”的重构方法以将该方法拆分为分开的部分。


我不同意“在任何时候都不好”。考虑这个成语引入的裁切器数量。你有一个头被包含在每一个单元,删除了一些数据,以及所有那些删除的位置变得稍稍少直截了当。
GManNickG

有时候情况很糟。如果有人试图在不应该的时候取消引用您的Delete-now-null指针,则它可能不会崩溃,并且该错误被“隐藏”。如果他们取消引用已删除的指针(该指针中仍然具有一些随机值),则您可能会注意到,该错误将更易于查看。
卡森·迈尔斯

@Carson:我的经验恰好相反:取消引用nullptr几乎会导致应用程序崩溃,并可能被调试器捕获。取消引用悬空指针通常不会立即产生问题,但是通常只会导致错误的结果或其他错误。
MikeMB

@MikeMB我完全同意,在过去约6.5年中,我对此的看法发生了重大变化
Carson Myers

关于成为一名程序员,我们在6-7年前都是所有人:)我什至不确定我今天是否敢回答C / C ++问题:)
Lasse V. Karlsen

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专门询问了不使用自动指针的情况。


2
宏是一个坏主意,特别是当它们看起来像正常函数时。如果要执行此操作,请使用模板化函数。

2
哇...我从未见过像anObject->Delete(anObject)使anObject指针失效那样的东西。那太可怕了。您应该为此创建一个静态方法,以便TreeItem::Delete(anObject)至少被迫这样做。
D.Shawley

抱歉,将其键入为函数而不是使用正确的大写形式“ this is a macro”。已更正。还添加了一条评论,以更好地解释自己。
杰森·威廉姆斯

你是对的,我迅速被拒绝的例子就是垃圾!固定:-)。我只是想考虑一个简单的示例来说明这个想法:任何删除指针的代码都应确保将指针设置为NULL,即使其他人(调用者)拥有该指针也是如此。因此,始终将引用传递给指针,以便在删除时将其强制为NULL。
杰森·威廉姆斯

1

“有时候这是一件好事,有时却毫无意义并可能掩盖错误”

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

delete myObj;
myobj = 0

成为多线程环境中的主流:

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肯定会告诉更多),这使我过时了所有这样的问题:“如何避免使用智能指针并使用普通指针安全地完成工作”。


我看不到4衬套比3衬套复杂得多(无论如何都应该使用lock_guards),而且如果析构函数抛出异常,则仍然会遇到麻烦。
MikeMB

当我第一次看到这个答案时,我不明白为什么要在析构函数中将指针为空,但是现在我知道了-这是因为拥有指针的对象在删除后就被使用了!
Mark Ransom


0

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

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

编辑:正如托马斯·马修斯(Thomas Matthews)在此较早的回答所说,如果在析构函数中删除了一个指针,则无需为它分配NULL,因为由于该对象已经被破坏,将不再使用它。


0

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


0

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

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.