【问题标题】:What is the proper way to delete a singly-linked-list in destructor?在析构函数中删除单链表的正确方法是什么?
【发布时间】:2020-04-17 17:26:41
【问题描述】:

您好,我对编写适当的析构函数有点含糊:

class SLLst 
{
public:
    SLLst() = default;
    SLLst(const SLLst&);
    SLLst& operator=(SLLst);
    ~SLLst();

    void insert(int);
    void remove(int);

private:
    SLLst* next = nullptr;
    int data = 0;
    friend void swap(SLLst&, SLLst&);
    friend std::ostream& print(std::ostream&, const SLLst&);
};

SLLst::SLLst(const SLLst& rhs) : 
    next(rhs.next ? new SLLst() : nullptr),
    data(rhs.data)
{
    cout << "cpy-ctor" << endl;
}

SLLst& SLLst::operator=(SLLst rhs)
{
    cout << "operator=(SLLst)" << endl;
    using std::swap;
    swap(*this, rhs);
    return *this;
}

void swap(SLLst& lhs, SLLst& rhs)
{
    cout << "operator=(SLLst)" << endl;
    using std::swap;
    swap(lhs.next, rhs.next);
    swap(lhs.data, rhs.data);
}

SLLst::~SLLst()
{
    cout << "dtor" << endl;
    delete next;// is this enough?

    // or should I use this code?
    //SLLst* cur = next;
    //SLLst* n = nullptr;

    //while (cur != NULL) {
    //  n = cur->next;
    //  cur->next = nullptr;
    //  delete cur;
    //  cur = n;
    //}

}


void SLLst::insert(int x)
{
    SLLst* tmp = new SLLst();
    tmp->data = x;
    if (!next)
    {
        next = tmp;
        return;
    }

    tmp->next = next;
    next = tmp;
}

std::ostream& print(std::ostream& out, const SLLst& lst)
{
    auto tmp = lst.next;
    while (tmp)
    {
        out << tmp->data << ", ";
        tmp = tmp->next;
    }
    return out;
}

如您所见,如果我只是在析构函数中使用delete next;,那么我会调用它与列表中的节点一样多,但是为什么许多实现使用循环来释放节点,就像析构函数中的注释代码一样?

  • 因为如果我只在next 上调用delete,那么析构函数将被递归调用,因此我认为我不需要循环来释放析构函数中的节点?对吗?

  • 什么时候应该使用循环来释放析构函数中的节点?谢谢!

*如果我运行我的代码,我会得到:

81、77、57、23、16、7、5,

done
dtor
dtor
dtor
dtor
dtor
dtor
dtor
dtor
  • 如您所见,dtor 被调用了 8 次;这是否意味着它已正确释放所有节点?

【问题讨论】:

  • @JamesAdkison:但我认为析构函数会被递归调用。
  • 我没有查看您的代码,但是在使用循环的实现中,这些类型没有析构函数,对吧?我猜这就是你所做的和那些例子的区别。
  • 是的,它应该递归删除所有节点。
  • 应谨慎使用递归删除。对于长列表,它可能会导致堆栈溢出。
  • @ChrisMM 这不是答案部分。

标签: c++ linked-list destructor


【解决方案1】:

如您所见,我是否只使用下一个删除;在析构函数中,我将其调用与列表中的节点一样多

是的。

因为如果我只在 next 上调用 delete,那么析构函数将被递归调用,因此我认为我不需要循环来释放析构函数中的节点?对吗?

是的。

什么时候应该使用循环来释放析构函数中的节点?

当你需要时。

但为什么许多实现使用循环来释放节点,如析构函数中的注释代码?

因为许多实现都是“类 C”的,并且不使用析构函数。所以他们需要。

您正在充分利用 C++ 的对象管理功能来“为您执行循环”。耶!

(虽然,老实说,我仍然会循环执行,因为你的方式可能会很重。)

现在更进一步,切换到std::list(或std::forward_list)。 ?

【讨论】:

  • std::forward_list, std::list 是双向链接的
  • @JohnkaS 并不是说​​它在功能上是等效的,但是可以
  • 非常感谢!真是令人惊叹和彻底的解释!
  • 是的,我最喜欢的一点是“是的”:P
  • @Maestro 做得对,递归和迭代之间几乎没有性能差异。递归开销通常是消耗的自动存储量。足够长的列表将通过存储this 和查找返回路径所需的簿记信息来耗尽堆栈(或任何支持自动存储)。或者,也许编译器会看到你在做什么并为你循环工作。如果你允许编译器可以非常聪明。 See the Asif Rule
【解决方案2】:

立即开始,std::unique_ptr 会为您解决这个问题,但如果您想自己做,那么它看起来像这样,请记住这是一个最小的示例。

RAII 是 C++ 中一个非常重要的原则,它的基本意思是,在需要时分配,但在使用完毕时销毁。

因此,如果一个节点被指向,并且您删除了指向它的节点,那么该节点也应该销毁它所指向的东西,因为它拥有它的所有权。

class List {
    Node* first_node;

    ~List() {
        delete first_node;
    }

};

class Node {
    ~Node() {
        delete next; // will then in turn destroy the one it points to untill one is nullptr, deleting nullptr is well defined in C++ nowadays
    }
    Node* next;

};

std::unique_ptr 示例

class List {
    std::unique_ptr<Node> first_node;

    // default dtor
};

class Node {
    std::unique_ptr<Node> next;
    // default dtor

};

【讨论】:

  • 没有要求替代方法;这不是问题
  • @Maestro,这是一个很好的属性。如果你想复制一个列表,你并不想复制指针。
  • @Evg:我知道使用智能指针更安全、更快、更不容易出错,但出于某种教育原因,我们应该多练习。
  • 如果你想复制一个链表那么你不应该复制指针,你应该复制内容,所以只需实现一个自定义的复制 ctor 遍历列表并创建一个新的,我实际上相信 Node 类不应该被复制,但 List 本身应该,所以应该为你处理它
猜你喜欢
  • 1970-01-01
  • 2012-09-12
  • 1970-01-01
  • 2013-10-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-03-19
  • 1970-01-01
相关资源
最近更新 更多