【问题标题】:C++ Linked list using smart pointers使用智能指针的 C++ 链表
【发布时间】:2016-08-08 23:25:02
【问题描述】:

我只对带有模板的链表使用原始指针。例如,成员数据Node<T>* head;,当我插入一个节点时,其中一行将是head = new Node<T>(data);

但是,现在我需要使用智能指针,但我不确定如何将其更改为使用智能指针。成员数据是否会更改为shared_ptr<Node<T>> head;,而另一行将更改为
head = shared_ptr<Node<T>>( new <Node<T>>(data) );

【问题讨论】:

  • 不知道为什么你想要一个共享的ptr而不是一个唯一的ptr,但是是的。此外,假设您使用的是 c++14,您应该使用 std::make_unique/make_shared。不过,在添加/删除不会意外删除列表尾部的元素时,您需要非常小心。
  • 对链表使用智能指针时要小心,因为当你删除链表中包含另一个节点的任何节点时,析构函数最终会被递归调用;如果列表的已删除部分链接了足够多的元素,这可能会导致堆栈溢出。
  • @xaxxon unique ptr 在这里更好用吗?如果我更改它,我是否只需将所有shared_ptr 部分更改为unique_ptr
  • @xaxxon 我能够将所有内容更改为使用共享 ptr,但调试器显示了很多内存泄漏。这就是为什么唯一 ptr 是更好的选择吗?
  • @Mark 我们不知道,因为我们不是读心者,也看不到您的代码。当正确使用时,智能指针有助于防止内存泄漏;不介绍他们。

标签: c++ templates pointers linked-list


【解决方案1】:

您“不需要”对链表使用智能指针,因为该语句没有意义。您确实将智能指针用于低级数据结构。您将智能指针用于高级程序逻辑。

就低级数据结构而言,您使用 C++ 标准库中的标准容器类,例如 std::list [*],它可以解决所有内存管理问题, 内部不使用任何智能指针。

如果您真的需要您自己的高度专业化/优化的自定义容器类,因为整个 C++ 标准库不适合您的要求,您需要 替换 @987654324 @、std::vectorstd::unordered_map 和其他经过优化、测试、记录和安全的容器——我非常怀疑! –,那么无论如何您都必须手动管理内存,因为这样一个专门的类的重点几乎肯定是需要内存池、写时复制甚至垃圾收集等技术,所有这些都与典型的智能指针相冲突比较简单的删除逻辑。

Herb Sutter的话来说:

永远不要使用拥有原始指针和删除,除非在极少数情况下 实现你自己的低级数据结构(即使这样保持 很好地封装在类边界内)。

Herb Sutter's and Bjarne Stroustrup's C++ Core Guidelines 也表达了类似的内容:

这个问题不能通过改变所有的所有权来解决(大规模) 指向 unique_ptrs 和 shared_ptrs 的指针,部分原因是 我们需要/使用 在实现中拥有“原始指针”以及简单指针 我们的基本资源句柄。例如,常见的向量 实现有一个拥有指针和两个非拥有指针。

使用原始指针在 C++ 中编写链表类可能是一个有用的学术练习。在 C++ 中使用智能指针编写链表类是一项毫无意义的学术练习。在生产代码中使用这两种自制的东西几乎都是错误的。


[*]或者只是std::vector,因为由于缓存局部性,无论如何这几乎总是更好的选择。

【讨论】:

  • 我认为这是最好的答案......这是我试图在我的答案中得到的。我只是无法以非迂回的方式到达它。为了将原始指针保留在类边界中,我相信 OP 需要删除 Node 析构函数中的底层数据并迭代地删除 LinkedList 析构函数中的节点......对吗?
  • @benzeno:类边界更多地是指类的用户不应该知道它的实现这一事实;即没有T* 出现在类的公共界面中。 delete 是否必要取决于容器的实现。我作为示例列出的一些高级技术可以在没有明确的deletes 的情况下工作。再说一次,给定std::list,一个简单的链表根本不需要高级技术或任何自定义类。
  • @benzeno:除此之外,是的,这样一个简单的自定义链表的析构函数可以做到这一点。
  • 我不同意“不要将智能指针用于低级数据结构”的说法(...... Herb Sutter 也没有说明这一点)。例如,考虑一棵二叉树:当您获得无穷无尽的资源时,您可以使用底层的原始指针以及您提到的所有复杂的东西来设置一个类似于标准库的实现。但实际上,您没有这些资源,因此没有理由不使用unique_ptr - 通常您几乎不会因此而损失任何性能并获得很多安全性。
  • 这不是问题的答案,因此是-1。你可以对这个问题有所有你想要的意见,但在这里你应该首先给出一个答案。如果可以的话,我还会再给你一个-1,因为你不需要勇气。
【解决方案2】:

基本上有两种方法可以设置智能指针增强列表:

  1. 使用std::unique_ptr

    template<typename T>
    struct Node
    {
         Node* _prev;
         std::unique_ptr<Node> _next;
         T data;
    };
    
    std::unique_ptr<Node<T> > root; //inside list
    

    那将是我的第一选择。唯一指针_next 负责没有内存泄漏,而_prev 是一个观察指针。然而,副本之类的东西需要手动定义和实现。

  2. 使用shared_ptr

    template<typename T>
    struct Node
    {
         std::weak_ptr<Node> _prev;   //or as well Node*
         std::shared_ptr<Node> _next;
         T data;
    };
    
    std::shared_ptr<Node<T> > root; //inside list
    

    这是更安全的选择,但性能不如唯一指针。此外,它可以通过设计复制。

这两种想法都是一个节点拥有完整的剩余列表。现在,当一个节点超出范围时,剩余列表不会成为内存泄漏的危险,因为节点会被迭代破坏(从最后一个开始)。

_prev 指针在两个选项中都只是一个观察指针:它的任务不是保持先前节点的活动,而只是提供访问它们的链接。 为此,Node * 通常就足够了(--注意:观察指针意味着您永远不会在指针上执行与内存相关的操作,例如 newdelete)。

如果您想要更安全,也可以使用std::weak_ptr。这可以防止类似的事情

std::shared_ptr<Node<T> > n;
{
    list<T> li;
    //fill the list
    n = li.root->next->next; //let's say that works for this example
}
n->_prev; //dangling pointer, the previous list does not exists anymore 

使用weak_ptr,您可以lock() 它并以此检查_prev 是否仍然有效。

【讨论】:

  • 自回答以来已经过了一段时间......但我很好奇你的 unique_ptr 版本是如何工作的,因为它将是你的首选。当前节点的 unique_ptr 拥有下一个节点的内存,因此,例如,当您按键删除当前节点时,您还将为下一个节点释放内存......您将遇到与 @ 相同的问题987654321@
  • @cevalek:公平点,我从来没有对这么大的树使用过上述方法。但是,与链接中列表的问题相反,对于树来说,情况似乎更糟。原因是你通常在树上迭代工作,当你通过迭代调用析构函数得到堆栈溢出时,你可能也不能递归调用任何其他函数。
  • @cevalek:一种可能的解决方法是从叶节点向上遍历——可以在Node 析构函数中实现它,但这会破坏unique-ptr 的优势。所以可能最好实现一个自定义的 unique-ptr - 一个假设树结构并从下向上删除的。 (但这只是一些快速的建议)。无论如何,这整件事都不是使用原始指针而不是智能指针的论据......只是提一下。
【解决方案3】:

我会看一下 std::list 的接口,它是链表的 C++ 实现。看来您正在接近链接列表类的模板错误。理想情况下,您的链表不应该关心所有权语义(即它是否使用原始 ptrs、智能指针或堆栈分配的变量进行实例化)。下面是一个带有 STL 容器的所有权语义的示例。但是,有来自更权威来源的更好的 STL 和所有权示例。

#include <iostream>
#include <list>
#include <memory>

using namespace std;

int main()
{

    // Unique ownership.
    unique_ptr<int> int_ptr = make_unique<int>(5);

    {
        // list of uniquely owned integers.
        list<unique_ptr<int>> list_unique_integers;

        // Transfer of ownership from my parent stack frame to the
        // unique_ptr list.
        list_unique_integers.push_back(move(int_ptr));

    } // list is destroyed and the integers it owns.

    // Accessing the integer here is not a good idea.
    // cout << *int_ptr << endl;
    // You can make a new one though.
    int_ptr.reset(new int(6));

    // Shared ownership.
    // Create a pointer we intend to share.
    shared_ptr<int> a_shared_int = make_shared<int>(5);

    {
        // A list that shares ownership of integers with anyone that has
        // copied the shared pointer.
        list<shared_ptr<int>> list_shared_integers;

        list_shared_integers.push_back(a_shared_int);

        // Editing and reading obviously works.
        const shared_ptr<int> a_ref_to_int = list_shared_integers.back();
        (*a_ref_to_int)++;
        cout << *a_ref_to_int << endl;

    } // list_shared_integers goes out of scope, but the integer is not as a
    // "reference" to it still exists.

    // a_shared_int is still accessible.
    (*a_shared_int)++;
    cout << (*a_shared_int) << endl;

} // now the integer is deallocated because the shared_ptr goes 
// out of scope.

了解所有权、内存分配/释放和共享指针的一个很好的练习是编写一个教程,您可以在其中实现自己的智能指针。然后,您将准确了解如何使用智能指针,并且您将有一个 xen 时刻,您将意识到 C++ 中的几乎所有东西都回到了 RAII(资源所有权)。

回到问题的关键。如果您想坚持使用 T 类型的节点,请不要将节点包装在智能指针中。 Node 析构函数必须删除底层的原始指针。原始指针可能指向本身指定为 T 的智能指针。当您的“LinkedList”的类析构函数被调用时,它会使用 Node::next 遍历所有节点,并在获得指向下一个节点的指针后调用 delete node;

您可以创建一个节点是智能指针的列表...但这是一个非常专业的链表,可能称为 SharedLinkedList 或 UniqueLinkedList 具有非常不同的语义用于对象创建、弹出等。举个例子,UniqueLinkedList 会移动向调用者弹出值时返回值中的一个节点。要针对这个问题进行元编程,需要对传递的不同类型的 T 使用部分特化。例如,类似:

template<class T>
struct LinkedList
{
    Node<T> *head;
};

// The very start of a LinkedList with shared ownership. In all your access
// methods, etc... you will be returning copies of the appropriate pointer, 
// therefore creating another reference to the underlying data.
template<class T>
struct LinkedList<std::shared_ptr<T>>
{
    shared_ptr<Node<T>> head;
};

现在您开始实现自己的 STL!使用这种方法,您已经可以看到 cmets 中提到的问题的可能性。如果节点有 shared_ptr next ,它将导致调用该共享节点的析构函数,该析构函数将调用下一个共享节点析构函数,依此类推(由于递归可能导致堆栈溢出)。所以这就是我不太关心这种方法的原因。

【讨论】:

    【解决方案4】:

    结构看起来像

    template<typename T> struct Node
    {
    T data;
    shared_ptr<Node<T>> next;
    };
    

    节点的创建看起来像

    shared_ptr<Node<int>> head(new Node<int>);
    

    auto head = make_shared<Node>(Node{ 1,nullptr });
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-02-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-08-12
      • 1970-01-01
      • 2014-11-22
      相关资源
      最近更新 更多