代码之家  ›  专栏  ›  技术社区  ›  Mark Ransom

删除指针后将其置空是一种好的做法吗?

  •  131
  • Mark Ransom  · 技术社区  · 15 年前

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

    下面的代码有什么问题?

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

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

    在C++中设置指针为NULL,并不是通用的好方法。有时这是件好事,有时它毫无意义,可以隐藏错误。

    在很多情况下,这是没有帮助的。但以我的经验,它不会伤害你。有人启发我。

    18 回复  |  直到 8 年前
        1
  •  76
  •   Håvard S    9 年前

    将指针设置为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的一个参数是,这样做只是屏蔽了两个delete错误,并使它们未被处理。

    显然,最好不要有双重删除错误,但是依赖于所有权语义和对象生命周期,这在实践中很难实现。比起ub,我更喜欢蒙面的双重删除错误。

    最后,关于管理对象分配,我建议您看看 std::unique_ptr 对于严格/单一所有权, std::shared_ptr 用于共享所有权,或其他智能指针实现,具体取决于您的需要。

        2
  •  53
  •   Stack Overflow is garbage    15 年前

    删除指针后将指针设置为空肯定不会造成伤害,但这通常是一个更基本的问题上的创可贴:为什么首先要使用指针?我可以看到两个典型的原因:

    • 你只需要在堆上分配一些东西。在这种情况下,把它包在一个raii对象会更安全和清洁。当你不再需要这个对象时,结束raii对象的作用域。就是这样 std::vector 有效,并且它解决了意外地将指针留在释放的内存中的问题。没有指针。
    • 或者你想要一些复杂的共享所有权语义。从返回的指针 new 可能和那个 delete 被召唤。多个对象可能同时使用了该对象。在这种情况下,共享指针或类似的东西会更好。

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

        3
  •  42
  •   Don Neufeld    15 年前

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

    {
        Foo* pFoo = new Foo;
        // use pFoo
        delete pFoo;
    }
    
        4
  •  27
  •   Adrian McCarthy    8 年前

    我总是把指针指向 NULL (现在) nullptr )删除它指向的对象后。

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

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

    3. 它将屏蔽一个双重删除,但我发现这些访问比访问已经释放的内存要少得多。

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

    5. 如果你已经在使用raii,那么 delete 在你的代码中以s开头,所以额外的赋值导致混乱的论点不能说服我。

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

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

    我还将其他类型的资源句柄设置为当资源空闲时的no resource值(通常仅在为封装资源而编写的raii包装的析构函数中)。

    我做了一个大的(900万份声明)商业产品(主要是C语言)。有一次,当内存释放时,我们使用宏魔术将指针清空。这立即暴露了许多潜伏的虫子,并迅速修复。据我所知,我们从未有过双重自由的错误。

    更新: 微软认为这是一个很好的安全实践,并在sdl策略中推荐了这种实践。显然msvc++11会 stomp the deleted pointer 如果使用/sdl选项编译,则自动(在许多情况下)。

        5
  •  12
  •   Community Anvaka    7 年前

    首先,在这方面和相关话题上存在很多问题,例如 Why doesn't delete set the pointer to NULL? .

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

    Foo * p2 = p;
    

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

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

        6
  •  7
  •   Thomas Matthews    15 年前

    如果在 delete 是的。当指针在构造函数中或在方法或函数的末尾被删除时,否。

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

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

        7
  •  3
  •   D.Shawley    15 年前

    正如其他人所说, delete ptr; ptr = 0; 不会让恶魔从你鼻子里飞出来。但是,它确实鼓励使用 ptr 就像一面旗帜。代码变得乱七八糟 delete 并将指针设置为 NULL . 下一步是分散 if (arg == NULL) return; 通过您的代码来防止意外使用 无效的 指针。一旦检查到 无效的 成为检查对象或程序状态的主要方法。

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

        8
  •  2
  •   Franci Penov    15 年前

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

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

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

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

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

        9
  •  2
  •   Community Anvaka    7 年前

    如果没有其他强制您在删除指针后设置或不将其设置为空的约束(其中一个约束由 Neil Butterworth ),那么我个人的偏好是留下它。

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

    它也做了比必要更多的工作,并且妨碍了事后调试。当你不需要记忆的时候,你接触的记忆越少,就越容易弄清楚为什么会有东西崩溃。很多时候,我都依赖于这样一个事实,即内存的状态与某个特定的错误发生时的状态相似,以诊断和修复所说的错误。

        10
  •  2
  •   HostileFork says dont trust SE    15 年前

    在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*>();
    

    但如果你真的想让人们知道指针“坏了”,我会百分之百同意那些说最好的办法是让它超出范围的人。然后使用编译器来防止在运行时出现错误的解引用。

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

        11
  •  2
  •   justin    12 年前

    在具有适当错误检查的结构良好的程序中,没有理由 指定为空。 0 在这种情况下作为一个普遍认可的无效值。努力失败,很快就会失败。

    反对分配的许多论据 建议 能够 隐藏错误或使控制流复杂化。从根本上说,这要么是一个上游错误(不是你的错误(对不起坏双关语))要么是代表程序员的另一个错误——甚至可能是程序流变得太复杂的迹象。

    如果程序员想要引入一个指针的使用,这个指针可能是空的,作为一个特殊的值,并写下所有必要的回避,这是他们故意引入的一个复杂问题。隔离越好,你越早发现误用案例,它们传播到其他程序中的能力就越小。

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

    从那里,您只有一个非常有限的作用域和上下文,其中可能存在(或允许)空指针。

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

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

        12
  •  1
  •   Lasse V. Karlsen    15 年前

    让我把你已经提出的问题扩大一下。

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


    在C++中设置指针为NULL,并不是通用的好方法。有时:

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

    但是,有 没有时间 当这是 坏的 !你会 通过显式地取消它引入更多的错误,您将不会 泄漏 记忆,你不会 导致未定义的行为 发生。

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

    话虽如此,如果你觉得你必须显式地空一些指针,那么在我看来,这听起来好像你还没有足够地拆分一个方法,应该看看名为“提取方法”的重构方法,将该方法拆分成不同的部分。

        13
  •  1
  •   Jason Williams    15 年前

    对。

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

    如果你不这么做,你 总有一天会有一些讨厌的指针错误。

    我总是使用宏来删除:

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

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

    您还可以编写“self delete”方法来引用调用代码的指针,以便将调用代码的指针强制为空。例如,要删除许多对象的子树:

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

    编辑

    是的,这些技术确实违反了一些使用宏的规则(是的,这些天你可能会用模板得到同样的结果),但是通过多年的使用 从来没有 访问死区内存—调试可能遇到的问题中最糟糕、最困难、最耗时的问题之一。在多年的实践中,他们有效地消除了我介绍给他们的每个团队中的一类错误。

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

    当然,上面的例子只是迈向自动指针的一步。我没有建议,因为操作人员特别询问不使用自动指针的情况。

        14
  •  1
  •   Valentin Heinitz    10 年前

    有时这是件好事,有时它毫无意义,可以掩盖错误

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

    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
    

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

    所以,这两个问题自然而然地出现在我的脑海中(赫伯·萨特肯定会告诉我更多),让我觉得“如何避免使用智能指针,如何安全地使用普通指针”这样的问题已经过时了。

        15
  •  0
  •   GrayWizardx    15 年前

    总是有 Dangling Pointers 担心。

        16
  •  0
  •   Community Anvaka    7 年前

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

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

    编辑:正如托马斯·马修斯在 this earlier answer ,如果在析构函数中删除了指针,则无需为其分配空值,因为该对象已被销毁,因此将不再使用该指针。

        17
  •  0
  •   Nemanja Trifunovic    15 年前

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

        18
  •  0
  •   beta4    15 年前

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

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

    它执行引用计数,并且是线程安全的。您可以在Tr1(STD::Tr1命名空间,γ包含和lt;内存& gt)中找到它,或者如果编译器不提供它,从Boost获取它。