【问题标题】:How is the recursive call to destructor made in linked list?如何在链表中递归调用析构函数?
【发布时间】:2016-10-24 18:40:09
【问题描述】:

我正在浏览 this 博客文章并遇到了这段代码。

#include <iostream>
#include <string>

using namespace std;

struct LNode {
   int data;
   LNode* next;

   LNode(int n){data = n; next = nullptr;}

   void add_to_end(int n) {
      if (next)
         next->add_to_end(n);
      else
         next = new LNode(n);
   }
   ~LNode() { cout << " I am from LNode Destructor " << endl; delete next; }
};

int main()
{   
   LNode root(1);
   root.add_to_end(2);
   root.add_to_end(3);
   root.add_to_end(4);

}

这段代码的输出是

 I am from LNode Destructor 
 I am from LNode Destructor 
 I am from LNode Destructor 
 I am from LNode Destructor 

出于某种原因,我一直认为我必须使用某种 while 或 for 循环遍历链表,然后继续删除我使用 new 动态分配的所有节点。但是在上面的代码中,析构函数是如何被调用四次的,它是如何自动遍历链表的,从而产生这种多米诺骨牌效应(完全自行删除分配的内存)。

【问题讨论】:

  • 注意:使用nullptr而不是NULL

标签: c++


【解决方案1】:

这段代码:

~LNode() { delete next; } // destructor

如果将next 设置为0NULLnullptr,则不会发生任何事情。但是,如果将其设置为以下LNode 的地址,则会导致以下LNode's 析构函数 运行。它的 destructor 将执行相同的操作,导致 LNode 之后的 its 被销毁,依此类推,直到遇到等于 nullptrnext 变量。

【讨论】:

  • 所以基本上next最初是指向root或1(假设next不为null并且有节点链)(1->2->3),所以root被删除并变成( 2->3) .. 我仍然很好奇 2 的析构函数现在是如何被调用的,因为我没有将指针指向 2
  • @pokche 你有一个对象列表,所有对象都有一个指向下一个对象的next 指针。当您在第一个对象上调用 delete 时,它的析构函数在 its 下一个对象上调用 delete,该对象在 its 上调用 delete 等。直到没有下一个对象对象,因为指针是nullptr
【解决方案2】:

它基本上是一个链条。

从根节点开始(这是失去范围的节点,因此会自动调用它的解构器),当调用delete next; 时,您正在调用next 元素的解构器,依此类推。当nextNULL0nullptr时,进程将停止。

你也可以给节点添加索引,自己看销毁顺序。

【讨论】:

    【解决方案3】:

    delete 和析构函数调用是两个不同的东西。但是每次删除(非 NULL)指向 LNode 的指针时,都会调用它的析构函数。

    详情请看这个问题:Does delete call the destructor?

    root 实例的析构函数在main() 末尾超出范围时被调用:

    int main()
    {   
       LNode root(1);
       root.add_to_end(2);
       root.add_to_end(3);
       root.add_to_end(4);
    /* Destructor of root is called here. Imagine this is present here: */
       root.~LNode(); // calling destructor on object root
    }
    

    ~LNode 析构函数调用delete next;,后者又调用该对象的析构函数,而该对象的析构函数又...当delete next; 被调用NULL 指针时,整个递归停止,它什么都不做(绝对不调用任何析构函数)。

    调用最后一个析构函数时的调用堆栈将如下所示:

    root.~LNode()
    delete root.next
    root.next.~LNode()
    delete root.next->next
    root.next->next->~LNode()
    delete root.next->next->next
    root.next->next->next->~LNode()
    

    然后在NULL 上实际调用delete next; 并停止递归。

    【讨论】:

      【解决方案4】:

      一个小小的说明:delete next; 调用会导致在节点上从第一个到最后一个调用析构函数 - 但实际的存储回收是从最后一个到第一个发生的 .即,这是序列:

       1) destructor invoked on node 1.
       2)   delete called with pointer to node 2.
       3)   destructor invoked on node 2.
       4)     delete called with pointer to node 3.
       5)     destructor invoked on node 3.
       6)       delete called with pointer to node 4.
       7)       destructor invoked on node 4.
       8)         delete called with nullptr.
       9)       destructor call at (7) reclaims node 4 and returns.
      10)     destructor call at (5) reclaims node 3 and returns.
      11)   destructor call at (3) reclaims node 2 and returns.
      12) destructor call at (1) reclaims node 1 and returns.
      

      【讨论】:

        【解决方案5】:

        析构函数是如何被调用四次的? 自动遍历链表

        这是一种常见的递归形式。


        链表是一种递归数据结构。

        我认为您可能有兴趣在(自制和简单的)单链表中查看更多递归示例。


        这是 ctor(简化版)。参数 a_max 指定要创建多少节点元素。

        LMBM::Node::Node(uint8_t a_max) :
           m_nodeId            (++M_nodeCount),
           m_next              (0), // linked list
           //... other node initializers items
        {
           if(a_max > 1)
           {
              uint8_t nmax = static_cast<uint8_t>(a_max - 1);
              m_next = new Node(nmax); // recurse create another Node
              dtbAssert(m_next)(a_max); // confirm 
           }
           else // (1 >= a_max)
           {
              dtbAssert(1 == a_max)(a_max); // at least one node
              // all requested nodes created
           }
        
           if (1 == m_nodeId) //i.e. the 1st node
           {
              M_firstNode   = this;   // capture list anchor to static
              M_MAX_THREADS = a_max;  // capture list size to static
        
              // ...  a few more actions
           }
        
        } // LMBM::Node::Node(uint8_t a_max)
        

        以上内容是从运行代码中提取的,但我现在认为此代码不适合发布,因为当 new 失败时(出于任何原因)代码会断言。虽然我的 dtbAssert 提供调试器支持,但它不适合客户端使用。

        是的,这里没有循环。

        也许当你习惯它时,简单易用是递归的典型特征。

        ctor 使用简单的 new 扩展列表(通常被许多喜欢智能指针的同行不鼓励)。

        每个节点都被分配一个唯一的 m_node_id 以帮助调试。

        创建此列表的调用很简单:

        LMBM::Node nodes(LMBM::Node::DEFAULT_MAX_NODES);
        

        main 中的代码(带有限制检查)可以为更大或更小的列表传入用户值。


        dtor 与您的博客发现非常相似(尽管顺序相反)

        LMBM::Node::~Node(void)
        {
           if(m_next) // delete objects and list
              delete m_next; // recurse down the list
        
           // ... clean up actions, if any 
        
           m_nodeId = 0;
        
        } // Node::~Node(void)
        

        这个 dtor 首先旋转到列表的末尾,然后在展开堆栈时进行清理和删除活动。


        此 init() 方法再次使用递归初始化一些资源(信号量等),并首先完成最后一个节点的 init()。

        void LMBM::Node::init(void)
        {
           if(m_next)
              m_next->init(); // tail first
        
           // 1. ALLOCATE resource, such as a semaphore
           m_semIn = new Sem_t; // create 1 semaphore per thread
           dtbAssert(m_semIn)(m_nodeId);
        
           //std::cout << "m_semIn " << m_nodeId << " = " << (void*)m_semIn << "\n";
        
           // 2. INITIALIZE default Sem_t ctor is ok
        
        } // void LMBM::Node::init(void)
        

        (这里也没有循环。)

        仅供参考 - LMBM::Sem_t 有 4 行 C++ 代码,并通过 Linux API 包装单个 Posix 进程信号量,设置为本地模式(未命名,未共享)。

        事实证明,这个 Posix 信号量确实适用于 std::threads 以及 posix 线程 - 这是我在这里测试的东西之一。


        这里是 startApp()。除了所使用的递归之外,我不会展示更多内容......在我截取这段代码的示例中,一个线程执行每个节点的活动。

        void LMBM::Node::startApp(void)   // activate threads
        {
           if(m_next)
              m_next->startApp(); // recurse to end of linked list
        
           // 4. start my thread
           m_thread = new std::thread (LMBM::Node::threadEntry, this);
           dtbAssert(m_thread);
        
           // ... confirm thread running state
        
        } // void LMBM::Node::startApp(void)   // activate threads
        

        所有线程都以一种或另一种方式合作。


        此时,我有N个节点;每个节点都有一个线程;而线程做什么可以由m_node_id决定;并且调试器 cout 可以使用具有 1..10 性质的线程 id 而不是系统 id 来扩充。

        【讨论】:

          猜你喜欢
          • 2017-06-12
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-10-21
          • 2016-03-13
          • 1970-01-01
          • 2012-08-29
          相关资源
          最近更新 更多