我只使用带有模板的链表的原始指针。例如,成员数据Node<T>* head;
以及当我插入节点时,其中一行将是head = new Node<T>(data);
。
但是,现在我需要使用智能指针,我不知道如何更改它以使用智能指针。会员数据是否会更改为shared_ptr<Node<T>> head;
而另一行会更改为
head = shared_ptr<Node<T>>( new <Node<T>>(data) );
?
答案 0 :(得分:8)
您不需要为链表使用智能指针,因为该语句没有意义。您不使用智能指针来处理低级数据结构。您可以将智能指针用于高级程序逻辑。
就低级数据结构而言,您使用C ++标准库中的标准容器类,如std::list
[*] ,它可以解决所有内存管理问题。无论如何,问题都没有在内部使用任何智能指针。
如果真的需要您自己的高度专业化/优化的自定义容器类,因为整个C ++标准库不适合您的要求,并且您需要{strong>替换来{{ 1}},std::list
,std::vector
以及其他优化,测试,记录和安全的容器 - 我非常怀疑! - 然后你必须手动管理内存,因为这样一个专门的类几乎肯定需要内存池,写时复制甚至垃圾收集等技术,所有这些都与典型的智能指针冲突相当简单的删除逻辑。
用Herb Sutter的话说:
永远不要使用拥有原始指针并删除,除非在极少数情况下 实现自己的低级数据结构(甚至保持 很好地封装在类边界内。)
这些方面的内容也在Herb Sutter's and Bjarne Stroustrup's C++ Core Guidelines中表达:
通过转换所有拥有来解决这个问题(大规模) 指向unique_ptrs和shared_ptrs的指针,部分原因是我们需要/使用 拥有“原始指针”以及实现中的简单指针 我们的基本资源处理。例如,常见的矢量 实现有一个拥有指针和两个非拥有指针。
使用原始指针在C ++中编写链接列表类可以是一个有用的学术练习。使用智能指针在C ++中编写链表类是一个毫无意义的学术练习。在生产代码中使用这两个自制的东西几乎是自动错误的。
[*] 或仅std::unordered_map
,因为缓存局部性几乎总是更好的选择。
答案 1 :(得分:4)
基本上有两种方法可以设置智能指针增强列表:
使用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
是一个观察指针。然而,复制等事情需要手工定义和实施。
使用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 *
通常就足够了( - 注意:观察指针意味着你永远不会在指针上执行与new
,delete
等内存相关的事情。
如果您想要更安全,也可以使用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
,以这种方式查看{{1}}是否仍然有效。
答案 2 :(得分:0)
我会看一下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.
理解所有权,内存分配/释放和共享指针的一个很好的练习是做一个实现自己的智能指针的教程。然后你就会明白如何使用智能指针,你将会有一个这样的时刻,你会发现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,它将导致对该共享节点的析构函数的调用,该析构函数将调用下一个共享节点析构函数,以此类推(由于递归可能导致堆栈溢出)。所以这就是我不太关心这种方法的原因。
答案 3 :(得分:0)
结构看起来像
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 });