C++析构函数被意外调用双链表

C++ destructor being called unexpectedly doubly linked list

本文关键字:链表 调用 意外 析构函数 C++      更新时间:2023-10-16
doubly_linked_list::~doubly_linked_list()
{
list_item* current = head;
while (current)
{
list_item* next = current->Get_Next();
delete current;
current = next;
}
}

我正在C++编写一个动态图,使用双向链表来保存 Graph 节点,但是我的代码不断失败,因为它调用析构函数的次数超过了它应该调用的次数,例如,在我的图析构函数期间

Dynamic_Graph::~Dynamic_Graph()
{
edges.~doubly_linked_list();
nodes.~doubly_linked_list();
}

双向链表的析构函数被调用了 2 次以上,尽管它被显式调用了两次。

然后我有插入边缘的功能:

Graph_Edge* Dynamic_Graph::Insert_Edge(Graph_Node* from, Graph_Node* to)
{
Graph_Edge* new_edge = new Graph_Edge(from, to);
from->Get_out_Nodes().List_Insert_After(to, from->Get_out_Nodes().Get_Tail());
to->Get_in_Nodes().List_Insert_After(from, to->Get_in_Nodes().Get_Tail());
edges.List_Insert(new_edge);
return new_edge;
}

将 To 节点添加到 from 节点的邻接列表后,代码无法解释地调用双链表的析构函数并删除此插入,但我不确定为什么。

以下是图表和链表的标题:

class doubly_linked_list
{
private:
list_item* head;
list_item* tail;
public:
doubly_linked_list() { this->head = NULL; this->tail = NULL; }
doubly_linked_list(list_item* head){ this->head = head; this->tail = NULL; }
~doubly_linked_list();
list_item* Get_Head() { return head; }
list_item* Get_Tail() { return tail; }
void Set_Head(list_item* h) { head = h; }
void List_Insert(list_item* x);
void List_Insert_After(list_item* x, list_item* y);
void List_Delete(list_item* x);
};

class Dynamic_Graph
{
protected:
doubly_linked_list edges;
doubly_linked_list nodes;
public:
Dynamic_Graph() { edges = doubly_linked_list(); nodes = doubly_linked_list(); }
~Dynamic_Graph();
Graph_Node* Insert_Node(unsigned node_key);
void Delete_Node(Graph_Node* node);
Graph_Edge* Insert_Edge(Graph_Node* from, Graph_Node* to);
void Delete_Edge(Graph_Edge* edge);
Rooted_Tree* SCC() const;
Rooted_Tree* BFS(Graph_Node* source) const;
};

欢迎任何帮助

编辑: 我删除了对析构函数的调用,但是我仍然在插入函数中收到析构函数调用,我不确定为什么。

编辑2: 添加更多相关的代码行:

Graph_Edge* Dynamic_Graph::Insert_Edge(Graph_Node* from, Graph_Node* to)
{
Graph_Edge* new_edge = new Graph_Edge(from, to);
from->Get_out_Nodes().List_Insert_After(to, from->Get_out_Nodes().Get_Tail());
to->Get_in_Nodes().List_Insert_After(from, to->Get_in_Nodes().Get_Tail());
edges.List_Insert(new_edge);
return new_edge;
}

这是触发错误的功能,尽管它不应该被删除,但它访问已删除的指针,它在完成将"to"节点插入"发件人"节点邻接列表后触发。

void doubly_linked_list::List_Insert_After(list_item* x, list_item* y)
{
if (!y)
{
head = x;
tail = x;
return;
}
if (y == tail)
{
tail = x;
}
if (y->Get_Next())
{
y->Get_Next()->Set_Prev(x);
}
x->Set_Next(y->Get_Next());
x->Set_Prev(y);
y->Set_Next(x);
}

此函数是双向链表中的插入后函数。

编辑3: 我试图通过尽可能少的功能来解决这个问题,这就是我得到的:

#pragma once
#include < cstddef >
class List_Item
{
protected:
List_Item* next;
List_Item* prev;
public:
List_Item(): next(NULL), prev(NULL) {}
List_Item* Get_Next() { return next; }
List_Item* Get_Prev() { return prev; }
void Set_Next(List_Item* next) { this->next = next; }
void Set_Prev(List_Item* prev) { this->prev = prev; }
List_Item(const List_Item& item) : next(item.next), prev(item.prev){}
List_Item& operator=(const List_Item& item) { this->next = item.next; this->prev = item.prev; return *this; }
};
class Doubly_Linked_List
{
protected:
List_Item* head;
List_Item* tail;
public:
Doubly_Linked_List() : head(NULL), tail(NULL) {}
~Doubly_Linked_List();
List_Item* Get_Head() { return head; }
List_Item* Get_Tail() { return tail; }
void Set_Head(List_Item* h) { head = h; }
void Set_Tail(List_Item* t) { tail = t; }
void insert(List_Item* item);
void insert_after(List_Item* item, List_Item* after);
Doubly_Linked_List(const Doubly_Linked_List& list) : head(list.head), tail(list.tail) {}
Doubly_Linked_List& operator=(const Doubly_Linked_List& list) { this->head = list.head; this->tail = list.tail; return *this; }
};
Doubly_Linked_List::~Doubly_Linked_List()
{
List_Item* item = head;
while (item)
{
List_Item* next = item->Get_Next();
delete item;
item = next;
}
}
void Doubly_Linked_List::insert(List_Item* item)
{
item->Set_Next(head);
if (head)
head->Set_Prev(item);
if (!head)
tail = item;
head = item;
item->Set_Prev(NULL);
}
void Doubly_Linked_List::insert_after(List_Item* item, List_Item* after)
{
if (!after)
{
head = item;
tail = item;
return;
}
if (after->Get_Next())
{
after->Get_Next()->Set_Prev(item);
}
else
{
tail = item;
}
item->Set_Next(after);
item->Set_Prev(after);
after->Set_Next(item);
}

#pragma once
#include "List_Item.h"
#include"Doubly_Linked_List.h"
class Graph_Node :
public List_Item
{
protected:
const unsigned key;
Doubly_Linked_List in_nodes;
Doubly_Linked_List out_nodes;
public:
Graph_Node(unsigned key): key(key) {}
Doubly_Linked_List Get_In_Nodes() { return in_nodes; }
Doubly_Linked_List Get_Out_Nodes() { return out_nodes; }
};

class Graph_Edge :
public List_Item
{
protected:
Graph_Node* from;
Graph_Node* to;
public:
Graph_Edge(Graph_Node* f, Graph_Node* t): from(f), to(t) {}
Graph_Node* Get_From() { return from; }
Graph_Node* Get_To() { return to; }
};
int main()
{
unsigned node_key = 1;
Graph_Node* from = new Graph_Node(node_key++);
Graph_Node* to = new Graph_Node(node_key++);
from->Get_Out_Nodes().insert_after(to, from->Get_Out_Nodes().Get_Tail());
to->Get_In_Nodes().insert_after(from, to->Get_In_Nodes().Get_Tail());
}

您不应该手动调用析构函数。它们会自动为您调用。

您只需要使用delete(它也调用析构函数(,并且仅在您实际通过调用new创建的对象上使用。当到达其范围的尽头时,将自动销毁所有其他对象,然后将自动调用其析构函数。对于类的成员也是如此。

这里唯一的例外是,如果您使用放置-new来创建对象,或者如果您打算重复使用其存储。但你可能不打算做这样的事情。

完全删除Dynamic_Graph的析构函数。不需要。


您的类doubly_linked_list违反了 0/3/5 规则。该规则说,如果您定义自定义析构函数,则还应定义自定义 copy(/move( 构造函数和 copy(/move( 赋值运算符。

如果您违反了此规则,并且碰巧创建了该类对象的隐式或显式副本,则可能会有未定义的行为,因为析构函数将被调用两次并尝试删除内存两次。


问题后使用完整示例进行编辑:

在您现在显示的代码中,Doubly_Linked_List的析构函数被调用了四次(参见 https://godbolt.org/z/KmStwy(。这些调用是因为Get_In_NodesGet_Out_Nodes返回Doubly_Linked_List列表按值作为Graph_Node类成员的副本。由于您在main中调用这些函数四次,因此需要销毁四个Doubly_Linked_List临时对象(编译器会自动执行(。

您没有正确实现Doubly_Linked_List(const Doubly_Linked_List& list)(和复制赋值运算符(来实际复制整个列表,因此您的程序仍然具有未定义的行为。您需要以深度复制整个列表的方式实现复制操作,否则原始节点和副本的析构函数调用将delete相同的节点。

或者,您可以将复制操作定义为已删除:

Doubly_Linked_List(const Doubly_Linked_List& list) : head(list.head), tail(list.tail) = delete
Doubly_Linked_List& operator=(const Doubly_Linked_List& list) = delete;

在这种情况下,编译器将不允许复制,并在您尝试复制时给出编译时错误。但是您当前的实现和以前一样破碎。您基本上只是复制了隐式复制操作的作用。再次仔细阅读有关 0/3/5 规则的链接,因为避免未定义的行为非常重要。

析构函数被多次调用的事实本身并不是问题。如果涉及副本,这是正常的。问题只是您的类复制操作已损坏。如果您不想在Get_In_Nodes中创建副本并Get_Out_Nodes,请改为返回 by-reference。


一般来说,您不应该像在现代C++中那样使用new/delete。相反,您可以使用std::make_uniquestd::unique_ptr来拥有指针,如果您这样做,那么您显示的任何类都不需要自定义析构函数(至少不需要具有与隐式析构函数不同的有效行为的自定义析构函数(,并且您始终可以使用 0 规则,这意味着您也不需要实现任何复制操作。

如果这不是一个学习练习,你不应该首先写自己的清单。std::list工作正常,几乎可以肯定比你想出的要好。