【问题标题】:Doubly-linked list infinite loop? [closed]双向链表无限循环? [关闭]
【发布时间】:2011-11-24 12:37:32
【问题描述】:

如果我要创建一个节点类,如下所示,并且如果它用于双向链表,它会在解构双向链表时创建一个无限循环吗?或者它会很好地终止?

class Node
{
    Node(  );

    ~Node(  )
    {
       delete mNext; //deallocs next node
    }

    Contact mContact;
    Node* mPrevious;
    Node* mNext;
}; 

编辑:如果我将代码修改为这样,它会工作吗?

~Node(  )
{
   mPrevious = NULL;
   if (mNext->mPrevious != NULL)
   {
      delete mNext; //deallocs next node
   }
}

编辑 2:或者这样效果最好?

~Node(  )
{
   if (mPrevious != NULL)
   {
      mPrevious = NULL;
      delete mNext; //deallocs next node
   }
}

【问题讨论】:

  • 我不知道你为什么认为它会形成一个无限循环,你应该在删除它之前检查 mNext 是否不为 NULL。
  • 由于没有设置 mNext,会发生崩溃。如果您确实在列表中添加了两次您不应该的内容,您也可以删除两次。也不要使用链接列表
  • 当 STL 有大量列表结构可供选择时,我不确定为什么要编写自己的双向链表。 (除非这是家庭作业或其他形式的学习练习,也就是说……)
  • @Nican:实际上,如果 mNext 为 NULL,您不应该检查。如果它为空,删除将忽略它。我最讨厌的事情之一是在删除之前检查代码是否为空
  • 我只是想澄清一下——我们是在谈论一个循环双向链表,其中“第一个”和“最后一个”元素相互连接,还是只是一个以和开头的普通元素以NULL结尾? @acidzombie24 我不同意,我认为了解简单数据结构如何工作的基础知识很重要,尤其是链表 - 这稍后会让您直观了解其他数据结构以及不同操作的复杂性等。

标签: c++ memory-management doubly-linked-list


【解决方案1】:

如果考虑到mNext 指针,节点正在形成一个循环,那么任何节点的破坏确实会可能形成一个无限递归循环,它会通过炸毁堆栈来终止程序.

可能发生的事情是

  1. 发出第一个“外部”delete node;
  2. 当进入节点析构函数时,还没有做任何事情,因为代码析构函数是销毁过程中执行的“第一”件事(销毁过程本身相当复杂,包括析构函数代码、类更改、成员销毁、基础销毁此顺序:请参阅this answer 以获得更详细的说明)。
  3. 第一个析构指令填充执行delete mNext;,从而在循环中的下一个节点上触发相同的进程。
  4. 因为节点正在形成一个循环,这条链将再次“从后面”到达node,从而使第一次调用成为永远不会结束的递归。
  5. 每次调用都会为激活记录分配堆栈空间,因此一段时间后所有允许用于堆栈的内存将被耗尽,操作系统将终止该进程。删除调用不是“尾调用”,因为在析构函数代码完成后,内存必须被释放,所以这个递归不能轻易被优化掉......而delete mNext;是析构函数的最后一条语句,仍然有一些操作必须是在delete 运算符完成后执行。

但是请注意,根据我的经验,除非您使用特殊的编译器选项,否则不会检查堆栈溢出,因此程序终止将非常“异常”。另请注意,在 Windows 下有一些可怕的代码,如果它们发生在程序终止时,在某些情况下会隐藏 segfault 错误,因此在退出事件循环后完成此操作时,Windows 程序很可能会优雅地终止。

考虑到堆栈溢出通常不被认为确实任何行为都是可能的,包括明显的“无限循环”(请注意,这个无限循环可能不是递归析构函数之一,而是运行时系统内部的某个地方变得疯狂,因为堆栈溢出)。

为什么我使用可能这个词?原因是 C++ 标准规定对象的多次破坏是未定义的行为。如果您将此添加到 C++ 中无法在不完成销毁的情况下退出析构函数的事实中,您将了解编译器理论上允许将对象标记为“正在销毁”并使守护程序飞出您的nosrils 如果你两次输入同一个对象的析构函数。但是检查这个错误不是强制性的,编译器编写者通常很懒(这不是对程序员的侮辱),因此不太可能出现这种检查(除非启用了一些特殊的额外调试选项)。

总结一下:它可以永远循环吗?是的。能崩溃吗?当然。它可以停止告诉我一个物体被摧毁了两次吗?当然。它可以很好地终止程序(即不设置任何错误代码)吗?是的,那也是。

任何事情都可能发生。墨菲说它会发生任何会对你造成最大伤害的事情......例如,当你开发它时,程序每次都会很好地终止......并且它会在你的演示日期间严重崩溃在一千名潜在客户面前。

别那样做 :-)

【讨论】:

    【解决方案2】:

    它无法知道何时停止,因此它可能会无限运行。
    您可能应该编写一个List 类,它有一个指向(或实际的)Node 的指针。 Node 的 d'tor 应该只处理自己的字段,在这种情况下是 mContactList 的 d'tor 应该遍历列表中的所有节点(记住何时停止),并删除每个节点(只删除一次)。

    【讨论】:

    • 实际上它不会无限运行。如果 mNext 无效或为 null 时停止,则会崩溃。
    • @acidzombie24 但是首先假设该列表是合法的,则应在释放任何空间之前调用下一个节点的 d'tor。而且由于那个 d'tor 调用了下一个,等等,你会得到一个无限的运行。
    • hmmm dtor 订单的好点子。 +1:D
    • @Eran,在某些时候 mNext 将为空,然后删除链将展开。我仍然不明白为什么它会是无限的。如果列表足够长,您可能会遇到堆栈问题,但不会遇到无穷大问题。
    • @SteveRowe 我的错,我认为它是一个循环双向链表,其中“最后一个”和“第一个”元素是相互连接的。
    【解决方案3】:

    假设你将 mNext 初始化为 null,它不会无限运行。当遇到空指针时,Delete 什么也不做。因此它会在你期望的时候结束。

    我不确定您使用“如果以前”选项做了什么。那些行不通。这将是一个有效的节点,因此有一个前一个节点,或者它不是一个有效的节点,检查前一个将有未定义的结果。坚持简单的答案:

    class Node 
    { 
    Node(  mNext = NULL; ); 
    
    ~Node(  ) 
    { 
       delete mNext; //deallocs next node 
    } 
    
    Contact mContact; 
    Node* mPrevious; 
    Node* mNext; 
    };  
    

    澄清:此解决方案有效,但前提是满足两个条件: 1) 列表中没有节点出现两次。 2)列表不是循环的。 如果你能保证这些条件,这是你最简单的答案。如果不能,则需要做一些更复杂的事情。

    【讨论】:

      【解决方案4】:

      我个人认为Node 的析构函数与其他节点有任何关系,这有点奇怪。

      如果设计取决于我,我会创建一个 List 类,其中包含指向 Node 对象(firstlast)的指针。 List 类的析构函数将负责遍历列表中的所有节点并销毁它们。

      【讨论】:

        【解决方案5】:

        这其实很简单。 假设 1)它是一个双向链接列表,而不是一个循环列表 2)链接列表中没有循环:这是一个双链接列表 3) 实现类只有一个 Node 实例,可能称为 HeadNode 或 LinkList ;) 这是显式销毁的节点


        示例:LinkList 是 1->2->3->4->5->6->NULL HeadNode 的析构函数调用(参考第三个假设)将导致递归调用,如下所示: 删除(1)->删除(2)->删除(3)->删除(4)->删除(5)->删除(6)->NULL 所以请检查 if (mNext != NULL) delete mNext 它有效:)


        但是:如果你想具体删除一个节点:假设我们想在上面的例子中只删除 4 个,所有的节点都将被删除直到 NULL,所以在删除之前请确保你将 Mnext 设置为 NULL。


        最佳实践是使用 STL 库或以其他方式使用自动指针类来解决问题的一部分

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2016-07-18
          • 1970-01-01
          • 1970-01-01
          • 2013-01-05
          • 1970-01-01
          • 2021-07-05
          • 1970-01-01
          相关资源
          最近更新 更多