如何在链表中对析构函数进行递归调用?

时间:2016-06-22 17:17:32

标签: c++

我正在浏览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动态分配的所有节点。但是在上面的代码中,析构函数是如何被调用四次的,它是如何自动遍历链表创建这种多米诺骨牌效应的(完全删除已分配的内存)。

5 个答案:

答案 0 :(得分:5)

此代码:

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

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

答案 1 :(得分:3)

它基本上是一个链条。

从根节点(丢失范围的人,因此,自动调用其解构函数)开始,在调用delete next;时,您正在调用next元素的解构函数,等等。当nextNULL0nullptr时,流程将停止。

您还可以向节点添加索引,并自行查看销毁顺序。

答案 2 :(得分:2)

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时,整个递归停止了什么都不做(绝对不会叫任何析构函数)。

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

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()

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

答案 3 :(得分:1)

  

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

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

链表是递归数据结构。

我认为你可能有兴趣在一个(自制的和简单的)单链表中看到一些递归的例子。

这是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)

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

是的,这里没有循环。

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

ctor用简单的新扩展列表(通常不喜欢智能指针的许多同伴)。

为每个节点分配一个唯一的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)

(这里也没有循环。)

FYI - LMBM :: Sem_t有4行C ++代码,通过Linux API包装一个Posix Process Semaphore,设置为本地模式(未命名,非共享)。

事实证明,这个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进行扩充。

答案 4 :(得分:1)

一个微小的澄清:删除下一个; 调用导致从头到尾在节点上调用析构函数 - 但实际的存储回收从上到下发生 。即,这是序列:

 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.