Correct way of erasing a linked list
假设,我有一个单链表,它的基本组成部分是,
1
2 3 4 5 6 |
struct Node {
Data d; Node *pNext; // methods ~Node(); }; |
链表的头部存储为,
1
|
Node *m_Head; // member of some class
|
当我完成列表时,我将通过删除每个节点来清理它,
1
2 3 4 5 6 7 8 9 10 |
void Erase()
{ Node *pIter, *pTemp = m_Head; while((pIter = pTemp) != 0) { pTemp = pIter–>pNext; delete pIter; pIter = pTemp; } } |
我想,如果我能简化一下。所以我想出了一个主意,我可以用一条指令清理整个链表!
1
|
delete m_Head;
|
和析构函数看起来像:
1
|
Node::~Node() { delete this–>pNext; }
|
我担心的是,它会导致递归(隐含地由于delete)吗?如果是,那么对于更大的链表来说绝对是一个问题。编译器能以任何方式帮助优化吗?
[注意:不使用任何库工具,如 std::list 或其他。]
- 注意,如果你选择了后面的【使用删除】,在删除前【修改列表时,不要销毁】必须设置node->pNext = NULL;,否则会丢失这个子列表的尾部。
我想你要问的问题是,列表中的每个 Node 是否都拥有它的 pNext Node?如果不是,那么它就没有必要在其析构函数中删除其 pNext 节点。
在大多数链表实现中,所有节点都归链表所有,一个节点并不拥有链表中它之后的所有节点。将节点保持为哑(POD 结构)并让所有逻辑驻留在列表中更有意义。
您的节点具有析构函数但没有复制构造函数或复制赋值运算符,这绝对是一种设计”气味”。我认为当您编写实现插入、拼接和擦除单个元素功能的代码时,这种方法会导致更多复杂性,因为在任何情况下您都必须手动管理 pNext 指针,以避免无意破坏列表的整个尾部。
- 在 cons 列表中,一个节点拥有它之后的那些似乎很自然,因为定义是 List a = Empty | Cons a List。我同意它需要小心操作。
当然:仅出于学习目的或确定自己的 List 确实更适合您的用例时才这样做
这取决于。您的编译器可能会检测到尾递归并发出在概念上等同于使用循环的代码。
如果不是,那么是的,它将递归。通常,如果堆栈压力很小(如您的情况),商品盒上应该可以进行数千次递归。但是,不能保证,事实上,对于非常大的列表,这可能是一个问题。
另外,我认为递归确实不完全适合兄弟节点的概念。一个节点层次结构,就像四叉树一样,需要递归,但是当列表概念与兄弟节点有关时,我在递归(形成调用层次结构)方面的思考不是很好。
您也可以将手动循环视为对递归的一种易于实现的优化,它可以保证您的代码更加健壮。
顺便说一句,您也可以将删除的节点删除到持有者类中:
1
2 3 4 5 6 7 8 9 10 11 12 13 14 |
class List {
public: ~List() { for–each–node delete–node } private: |
这基本上是标准库的列表通常是如何实现的。它使整个实现更容易实现并且在概念上更正确(节点在逻辑上不拥有它们的兄弟)
- AFAIK,Data 成员(和 Node 的基类)的破坏发生在 Node 析构函数调用的末尾,这使得它根本不是尾递归的(除非所有这些都是空操作并优化了)。
大多数编译器在默认设置中进行尾调用消除。一些更聪明的可以将非尾调用转换为尾调用。
所以,只要你开启了一些优化,这个方法就可以了。
原始指针?手动调用 delete ?
为什么不呢,简单地说:
1
2 3 4 |
struct Node {
Data d; std::unique_ptr<Node> next; }; |
那你根本不用担心内存管理,它是自动的!
- 实际上,这会产生递归,但这会阻止stackoverflow吗?这是问题的主要关注点。
- @stefaanv:开放主题。这取决于编译器和标准库的实现,我希望是这样,但这是猜测。我不知道标准是否Promise最小递归深度,如果它不关心这一点,我不会感到惊讶。
来源:https://www.codenong.com/7239657/