unique_ptr 链表的堆栈溢出
Stack overflow with unique_ptr linked list
我已经转换了以下链表结构
struct node {
node* next;
int v;
};
进入 c++11 版本 - 即不使用指针。
struct node {
unique_ptr<node> next;
int v;
};
添加、删除元素和遍历工作正常,但是当我插入大约 100 万个元素时,调用头节点的析构函数时出现堆栈溢出。
我不确定我做错了什么。
{
node n;
... add 10mill elements
} <-- crash here
你没有做错任何事。
当您创建包含 1000 万个元素的列表时,为每个节点分配 make_unique
一切都很好(当然数据不在堆栈上,除了第一个节点!)。
问题是当你去掉列表的头部时:unique_ptr
将负责删除它拥有的下一个节点,其中还包含一个 unique_ptr
注意删除下一个节点...等等...
所以最后 1000 万个元素被递归删除,每个递归调用在堆栈上占用一些 space。
默认情况下 std::unique_ptr
调用结构 std::default_delete
的运算符函数,它只执行运算符 delete
.
因此结构 std::default_delete
的每个运算符函数递归地调用自身以获取结构 node
.
的数据成员 next
结果导致堆栈溢出。
如果您使用普通指针而不是 std::unique_ptr
类型的指针,但通过以下方式向结构节点添加析构函数,您将得到相同的结果
struct node {
node* next;
int v;
~node() { delete next; }
};
甚至喜欢
struct node {
node* next;
int v;
~node() { if ( next ) delete next; }
};
对于节点数较多的列表,因为析构函数会被递归调用
因为当您销毁头节点元素时,它会调用析构函数 ounique_ptr,它会销毁调用第三个元素的析构函数的第二个元素,而第三个元素调用 ... 等 100 万次。
因此,您有 100 万次嵌套函数调用(析构函数)。每个函数调用至少在堆栈中占用内存以存储 return 地址(如果需要,还包括参数和局部变量)。自然,stack 无法提供这样的内存量。你应该重新设计代码来解决它。例如,重写 Node class 的析构函数,使其找到最后一个列表元素,然后销毁它和循环中末尾的所有其他节点,而不是递归地销毁。
如其他答案中所述,您由于递归隐式析构函数而出现段错误。可以在不诉诸原始指针的情况下解决这个问题,必须信任编译器或编写自定义分配器:
~node() {
for (std::unique_ptr<node> current = std::move(next);
current;
current = std::move(current->next));
}
在这里,您迭代地遍历指针链。这将一次一个地解开一个指针并将所有权 std::move(current->next)
更改为当前。同时,current
拥有的先前未链接的指针将在被移动赋值覆盖时被释放。
您可能会发现显式变体更直接:
current.reset(current->next.release()));
实际上等同于:
current = std::move(current->next));
我更喜欢 move
版本,因为它不会给您留下原始指针。但在那种情况下它没有什么区别。
我已经转换了以下链表结构
struct node {
node* next;
int v;
};
进入 c++11 版本 - 即不使用指针。
struct node {
unique_ptr<node> next;
int v;
};
添加、删除元素和遍历工作正常,但是当我插入大约 100 万个元素时,调用头节点的析构函数时出现堆栈溢出。
我不确定我做错了什么。
{
node n;
... add 10mill elements
} <-- crash here
你没有做错任何事。
当您创建包含 1000 万个元素的列表时,为每个节点分配 make_unique
一切都很好(当然数据不在堆栈上,除了第一个节点!)。
问题是当你去掉列表的头部时:unique_ptr
将负责删除它拥有的下一个节点,其中还包含一个 unique_ptr
注意删除下一个节点...等等...
所以最后 1000 万个元素被递归删除,每个递归调用在堆栈上占用一些 space。
默认情况下 std::unique_ptr
调用结构 std::default_delete
的运算符函数,它只执行运算符 delete
.
因此结构 std::default_delete
的每个运算符函数递归地调用自身以获取结构 node
.
next
结果导致堆栈溢出。
如果您使用普通指针而不是 std::unique_ptr
类型的指针,但通过以下方式向结构节点添加析构函数,您将得到相同的结果
struct node {
node* next;
int v;
~node() { delete next; }
};
甚至喜欢
struct node {
node* next;
int v;
~node() { if ( next ) delete next; }
};
对于节点数较多的列表,因为析构函数会被递归调用
因为当您销毁头节点元素时,它会调用析构函数 ounique_ptr,它会销毁调用第三个元素的析构函数的第二个元素,而第三个元素调用 ... 等 100 万次。
因此,您有 100 万次嵌套函数调用(析构函数)。每个函数调用至少在堆栈中占用内存以存储 return 地址(如果需要,还包括参数和局部变量)。自然,stack 无法提供这样的内存量。你应该重新设计代码来解决它。例如,重写 Node class 的析构函数,使其找到最后一个列表元素,然后销毁它和循环中末尾的所有其他节点,而不是递归地销毁。
如其他答案中所述,您由于递归隐式析构函数而出现段错误。可以在不诉诸原始指针的情况下解决这个问题,必须信任编译器或编写自定义分配器:
~node() {
for (std::unique_ptr<node> current = std::move(next);
current;
current = std::move(current->next));
}
在这里,您迭代地遍历指针链。这将一次一个地解开一个指针并将所有权 std::move(current->next)
更改为当前。同时,current
拥有的先前未链接的指针将在被移动赋值覆盖时被释放。
您可能会发现显式变体更直接:
current.reset(current->next.release()));
实际上等同于:
current = std::move(current->next));
我更喜欢 move
版本,因为它不会给您留下原始指针。但在那种情况下它没有什么区别。