【问题标题】:Stack overflow with unique_ptr linked list [closed]unique_ptr 链表的堆栈溢出[关闭]
【发布时间】:2016-06-02 19:08:19
【问题描述】:

我已经转换了以下链表结构

struct node {
  node* next;
  int v;
};

进入 c++11 版本 - 不使用指针。

struct node {
  unique_ptr<node> next;
  int v;
};

添加、删除元素和遍历工作正常,但是当我插入大约 100 万个元素时,调用头节点的析构函数时会出现堆栈溢出。

我不确定自己做错了什么。

{
  node n;

  ... add 10mill elements

} <-- crash here

【问题讨论】:

  • ... add 10mill elements 在堆栈中??请提供minimal reproducible example
  • @HappyKoding 想一想这将如何运作。要删除第一个节点,首先需要删除第二个节点,所以调用一个函数。但要做到这一点,您首先需要删除第三个节点。所以你调用一个函数。您将拥有数百万级别的函数调用。您需要一种特定的方式来解开该链表,如果您不编写代码来执行此操作,它将不会自行编写。
  • @zenith 它不能那样做。它不知道该对象是链表的一部分,因此需要一种特殊的算法来使销毁变得合理。它只是为对象运行默认析构函数,它将为指向的对象运行默认析构函数,依此类推。对于这个特定问题,这是一个疯狂的算法。
  • @zenith 从性能的角度来看,这不仅是疯狂的,而且从“我可以合理地期望它在实际计算机上工作”的角度来看也是疯狂的。而且,事实上,它没有用。

标签: c++ c++11 linked-list stack-overflow unique-ptr


【解决方案1】:

正如其他答案中所解释的,由于递归隐式析构函数,您出现了段错误。无需借助原始指针、无需信任编译器或编写自定义分配器即可解决此问题:

~node() {
    for (std::unique_ptr<node> current = std::move(next);
         current;
         current = std::move(current->next));
}

在这里,您迭代地遍历指针链。这将一次一个地解开一个指针并将所有权std::move(current-&gt;next) 更改为当前。同时,current 拥有的先前未链接的指针将在被移动分配覆盖时被释放。

您可能会发现显式变体更直接:

current.reset(current->next.release()));

实际上等同于:

current = std::move(current->next));

我更喜欢move 版本,因为它不会给你留下一个原始指针。但在这种情况下,它没有任何区别。

【讨论】:

    【解决方案2】:

    你在这里没有做错任何事。

    当您创建包含 1000 万个元素的列表时,为每个节点分配 make_unique 一切都很好(当然数据不在堆栈上,可能除了第一个节点!)。

    问题是当您摆脱列表的头部时:unique_ptr 将负责删除它拥有的下一个节点,其中还包含一个 unique_ptr 将负责删除下一个节点……等等……

    所以最终这 1000 万个元素会被递归删除,每个递归调用都会占用堆栈上的一些空间。

    【讨论】:

    • 有没有办法解决这个问题或者应该回到原始指针?
    • 是的,有一种方法:当您创建 unique_ptr 时,您可以提供一个将被调用的自定义删除器,而不是默认删除器。因此,您可以提供一个使删除迭代而不是递归的方法。
    • 使用-O2 编译可能会优化这种递归。
    • @DavidSchwartz 你当然是对的:用一个自定义删除器替换默认删除器,该删除器遍历列表以重置指针需要一点 unique_ptr 的好处,并且看起来非常接近您在没有 unique_ptr 的情况下编写的代码。剩下的主要优势是明确节点的所有权。我认为 OP 的问题是出于教育/实验目的,因为在现实世界中我们都会使用 &lt;list&gt;
    • atZereges:我用 gcc 和 clang 尝试了 O2。 clang 现在没有崩溃,但 gcc 仍然崩溃。 gcc 是 4.9 clang 是 3.5
    【解决方案3】:

    默认情况下std::unique_ptr 调用结构体std::default_delete 的运算符函数,它只执行运算符delete

    因此结构std::default_delete 的每个运算符函数递归地调用结构node 的数据成员next

    结果导致堆栈溢出。

    如果你使用普通指针而不是 std::unique_ptr 类型的指针,但通过以下方式向结构节点添加析构函数,你会得到相同的结果

    struct node {
      node* next;
      int v;
      ~node() { delete next; } 
    };
    

    甚至喜欢

    struct node {
      node* next;
      int v;
      ~node() { if ( next ) delete next; } 
    };
    

    对于具有大量节点的列表,因为析构函数将被递归调用

    【讨论】:

    • 是的,你的权利,希望不必处理细节并依赖 RAII 概念
    • if (next) delete next 是多余的,因为删除空指针没有任何效果。
    • @zenith 我的意思是,即使他会这样写,也不会改变任何事情。
    【解决方案4】:

    因为当你销毁头节点元素时,它会调用析构函数oа unique_ptr,它会销毁调用析构函数的第二个元素,而第三个元素调用...等1mil次。

    因此,您有 1 百万个嵌套函数调用(析构函数)。每个函数调用至少在堆栈中占用内存以存储返回地址(以及参数和局部变量,如果需要)。自然,堆栈不能提供这么大的内存量。您应该重新设计代码来解决它。例如,重写 Node 类的析构函数,使其找到最后一个列表元素,然后在循环中从末尾开始销毁它以及所有其他节点,而不是递归。

    【讨论】:

      猜你喜欢
      • 2012-06-02
      • 1970-01-01
      • 1970-01-01
      • 2011-07-30
      • 1970-01-01
      • 2014-07-20
      • 2016-05-06
      • 2019-05-18
      • 2010-10-27
      相关资源
      最近更新 更多