繁体   English   中英

C++ 使用智能指针的链表

[英]C++ Linked list using smart pointers

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

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

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

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

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

赫伯·萨特的话来说:

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

Herb Sutter 和 Bjarne Stroustrup 的 C++ Core Guidelines 中也表达了这些内容:

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

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


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

设置智能指针增强列表基本上有两种选择:

  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是否仍然有效。

我会看看 std::list 的接口,它是链表的 C++ 实现。 似乎您正在接近您的链接列表类的模板错误。 理想情况下,您的链表不应该关心所有权语义(即它是否使用原始 ptr、智能指针或堆栈分配变量进行实例化)。 下面是 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! 使用此方法,您已经可以看到问题的评论中提到的潜在问题。 如果节点有 shared_ptr next 它将导致调用该共享节点的析构函数,该析构函数将调用下一个共享节点析构函数等等(由于递归可能导致堆栈溢出)。 所以这就是为什么我不太关心这种方法。

结构看起来像

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 });

不要在类似数据结构的图形中使用智能指针,因为它可能会导致堆栈溢出,由于析构函数或 inc 的递归调用导致许多性能问题,由于 dfs 和 bfs 算法的工作原理,decr 引用计数不是最优的

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM