动态分配的双向链表类实例 segfault C++

Posted

技术标签:

【中文标题】动态分配的双向链表类实例 segfault C++【英文标题】:Dynamically allocated doubly linked circular list class instances segfault C++ 【发布时间】:2015-07-24 00:56:47 【问题描述】:

当 main 使用 dlring 类型的 构造 变量操作时,使用这个模板类工作得非常好,但我的目标是允许 动态分配,所以我可以处理非预定义数量的双向链接循环列表,以允许使用以下功能:

通过使用节点位置(通过 迭代)或值输入。 同样适用于将两个列表链接为一个具有单个头/尾的列表 对。 节点从一个列表(实例)导出到另一个。 等

我很确定有一个我还不知道的优雅的解决方法,但是如果您没有足够的努力来解决,我认为向社区提出问题并不是一件好事。 (检查谷歌

因此,为了实现这个目标,我应该使用某种指针对指针 AFAIK 动态分配内存(通过构造函数调用)。如果有更聪明的方法来实现这些,请告诉我。我的解决方案尝试在这个 sn-p 的末尾给出。随意批评以下所有内容。

双向链表类头(简体)

template <typename T>
class dlring

    struct node
    
        T data;
        node* prev;
        node* next;
        node(T t, node* p, node* n) : data(t), prev(p), next(n) 
    ;
    node* head;
    node* tail;
public:
    dlring():head(nullptr), tail(nullptr)
    bool empty() const  return ( !head || !tail ); 
//operator bool() const  return !empty(); 
    void Push(T);
    T pop_back();
    ~dlring()
    
        while(head)
        
            node* temp(head);
            head=head->next;
            delete temp;
        
    
;

我应该使用注释掉的 operator bool 重载吗?

pop_back 和 Push 方法:

template <typename T>
void dlring<T>::Push(T data)

    head = new node(data, tail, head); 
    if( head->next )
    
        head->next->prev = head;
        tail->next = head;
    
    if( empty() )
    
        tail = head;
        head->next=tail;
        head->prev=tail;
        tail->next=head;
        tail->prev=head;
    

template<typename T>
T dlring<T>::pop_back()

    if( empty() )
        std::cout<<"List empty";
    node* temp(tail);
    T data( tail->data );
    tail = tail->prev ;
    if (tail != temp)
    
        tail->next->next = head; 
        head->prev = tail;
    
    else
    
        head = nullptr;
        tail = nullptr;
    
    delete temp;
    temp = nullptr;
    return data;

我的尝试没有正确的行为:当我尝试通过迭代显示所有列表时,代码失败,在 head->dlist 的数据访问尝试上出现段错误[0] ,其中 0 是 k 的迭代。这是sn-p:

   int main()
    
    int k;
      std::cout<<"Rings count?"<<std::endl;
      std::cin>>k;
        dlring<int>* dlist = new dlring<int>[k]; //I suppose I'm allocating *k*
     //dlring<int> elements. this line is not confirmed to call the constructor.
    (dlist[0]).Push(10);
    (dlist[0]).Push(13);
    (dlist[1]).Push(99);
    /*
    while(!dlist[0].empty())
    std::cout<<(dlist[0]).pop_back()<<" ";
    std::cout<<std::endl;
    while(!dlist[1].empty())
    std::cout<<(dlist[1]).pop_back()<<" ";
    */
    //this section works perfectly fine, while this
      for(int i=0;i<k;i++)
      
        while(!dlist[k].empty())
        std::cout<<(dlist[k]).pop_back()<<" ";
        std::cout<<std::endl;
      
    //is causing a segmentation fault while attempting to access dlist[*0*].tail->data.
    std::cout<<(dlist[0]).head->data;
    //line was checked and is confirmed to be functional, 
    //I suppose dlist[variable] has some trick I don't know yet.
    //what I wish to look like an instance call would be *
    return 0;
    

最好的问候。同样,请随意批评我的代码/逻辑任何

【问题讨论】:

在谷歌漫游时发现这个: // 重载操作符 // Selector T* operator->() return m_obj; // 地址访问 T& operator* () return *m_obj;有什么帮助吗? 我认为pop_back中tail的设置不正确:tail-&gt;next-&gt;next = head;此时tail已经指向new tail,所以我'd set tail-&gt;next = head; @dyp 是的,但要制作 new tail->next=head 我通过 new 引用 old tail i> 尾部-> 下一个。希望我没有弄错。 也许我误解了tail 的目的,但我就是不明白:难道head-&gt;prev == tail &amp;&amp; tail-&gt;next == head 不能保证所有非空列表(当然,在成员函数之外)?如果是这样,为什么需要两个数据成员? 如果你牺牲一点空间并使用头节点(它不会成为集合的一部分),那么你的代码会更简单更快,因为极端情况(头和尾) 可从标头节点访问。而且您不必担心边界条件(push() 为空列表或 pop_back() 为一个元素的列表)。此外,如果您使用 pop_back() 直到列表变为空,您的析构函数也更简单、更安全。 【参考方案1】:
  for(int i=0;i<k;i++)
  
    while(!dlist[k].empty())
    std::cout<<(dlist[k]).pop_back()<<" ";
    std::cout<<std::endl;
  

没有使用迭代器i。应该是:

  for(int i=0;i<k;i++)
  
    while(!dlist[i].empty())
    std::cout<<(dlist[i]).pop_back()<<" ";
    std::cout<<std::endl;
  

由于dlist 是一个大小为k 的数组,因此原始代码会产生越界访问。


由于上述循环使dlist 数组中的每个列表都为空,所有列表中的head 将是一个空指针。请注意,您根本无法访问它,因为它是私有成员。如果这样做,在取消引用时会出现段错误:

std::cout<<(dlist[0]).head->data;

如果这样编译,此时程序中,dlist[0].head == nullptr,因此是段错误。

还请注意,您正在泄漏内存,因为您没有释放动态分配的dlist。附加到程序的末尾:

delete[] dlist;

通过这些更改,我没有收到来自 clang 的地址清理程序的任何段错误、问题或报告。


另一个问题(在您的main 中没有体现)是tailpop_back 中的设置。我将尝试使用一些 ASCII 艺术进行说明。一个盒子

   D ->
<- D

表示具有数据、next 指针和prev 指针的节点。 所有的箭头都是指针。

一个非空列表:

   +-----------------------------+
   v                             |
   D ->    D -> ..    D ->    D -+
+- D    <- D    .. <- D    <- D
|                             ^
+-----------------------------+
   ^                          ^
   |head                      |tail

head-&gt;prevtail 指向同一个对象,同样,tail-&gt;nexthead 指向同一个对象。

现在,pop_back 函数的“动画”。

template<typename T>
T dlring<T>::pop_back()

    if( empty() )
        std::cout<<"List empty";
    node* temp(tail);

    /*
       +-----------------------------+
       v                             |
       D ->    D -> ..    D ->    D -+
    +- D    <- D    .. <- D    <- D
    |                             ^
    +-----------------------------+
       ^                          ^
       |head                      |tail
                                  |temp
    */

    T data( tail->data );
    tail = tail->prev ;

    /*
       +-----------------------------+
       v                             |
       D ->    D -> ..    D ->    D -+
    +- D    <- D    .. <- D    <- D
    |                             ^
    +-----------------------------+
       ^                  ^       ^
       |head              |tail   |temp
    */

    if (tail != temp)
    
        tail->next->next = head; 

        /*
           +-----------------------------+
           v                             |
           D ->    D -> ..    D ->    D -+  (A)
        +- D    <- D    .. <- D    <- D
        |                             ^
        +-----------------------------+
           ^                  ^       ^
           |head              |tail   |temp

        The pointer (A) is what is changed when writing to tail->next->next.
        Yes, nothing has actually changed in the list!
        */

        head->prev = tail;

        /*
           +-----------------------------+
           v                             |
           D ->    D -> ..    D ->    D -+
        +- D    <- D    .. <- D    <- D
        |                     ^
        +---------------------+
           ^                  ^       ^
           |head              |tail   |temp
        */

    
    else
    
        head = nullptr;
        tail = nullptr;
    
    delete temp;

    /*
       D ->    D -> ..    D ->
    +- D    <- D    .. <- D
    |                     ^
    +---------------------+
       ^                  ^       ^
       |head              |tail   |temp
    */

    temp = nullptr;
    return data;

请注意,在最后一个“图片”中,tail-&gt;next 是一个无效指针。

相反,它应该是:

    if (tail != temp)
    
        tail->next = head; 

        /*
           +---------------------+-------+
           v                     |       |
           D ->    D -> ..    D -+    D -+
        +- D    <- D    .. <- D    <- D
        |                             ^
        +-----------------------------+
           ^                  ^       ^
           |head              |tail   |temp

删除temp后(没有进一步的变化),会是这样的:

        /*
           +---------------------+
           v                     |
           D ->    D -> ..    D -+
        +- D    <- D    .. <- D
        |                     ^
        +---------------------+
           ^                  ^       ^
           |head              |tail   |temp

最后但同样重要的是,请注意您违反了Rule of Three。

【讨论】:

请看一下我的主要内容,我在尝试通过 (dlist[k=0]) 调用 pop_back 的 T 数据 (tail->data) 访问时遇到段错误错误,但是 (dlist[0] ) 运行良好。请记住,tail->data 指的是实现的“新”尾部(从弹回结束或第一次尝试弹回,虽然两者都是合法的)。 P.S.:最后,我设法以一种可读的方式写了这个评论,如果我能评价你的努力就好了。非常感谢您的关注。 @ГлушковМакс 哎呀,我没有在您的main 中看到迭代变量事故。在我的回答之前。 我有点伤脑筋。我们有解决方案吗? @ГлушковМакс 我想是的。通过两个额外的更改,我相当有信心该程序应该可以正常运行。请看我答案的开头。 好吧,我们在这里得到的是一个很好的实现和一个可悲的疲惫的程序员。万分感谢! xD UPD:一切正常,检查了你所有的答案,做得很好。我的代码中到处都是与疲劳相关的问题...

以上是关于动态分配的双向链表类实例 segfault C++的主要内容,如果未能解决你的问题,请参考以下文章

c++创建链表为啥要用类模板

访问动态分配数组的越界元素/没有 SegFault

小白上手写的第一个双向链表(C++)

c++实现双向链表的常用功能

双向链表的增删改查C++完整实现

c++链表类模板问题(不要用c语言,用c++)