You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

含Next/Prev指针的Node类拷贝构造与赋值运算符实现错误排查

你的双向链表节点拷贝问题分析与修复方案

嘿,我仔细看了你的Node类拷贝构造函数和赋值运算符的实现,发现几个核心问题,这就是你拷贝后数据不对甚至程序可能崩溃的原因:

  1. 无限递归死循环:你在拷贝构造里直接new Node(*h.Next),这会再次触发拷贝构造函数,要是原链表是循环的,或者哪怕是普通链表的首尾节点(Next/Prev为nullptr),要么会无限递归到栈溢出,要么直接解引用空指针导致程序崩掉。
  2. 空指针没处理:如果原节点的Next或Prev是nullptr,你直接去解引用*h.Next属于未定义行为,肯定要出问题。
  3. 赋值运算符没防自我赋值:要是你把节点赋值给自己,先执行delete Next; delete Prev;会把当前节点的指针先销毁,之后再访问t.Next时,那已经是被释放的内存了,结果完全不可控。
  4. 深拷贝逻辑错了:你想做深拷贝,但现在的写法要么递归炸栈,要么没正确处理指针指向的关系,导致拷贝后的节点链完全混乱。

修复后的代码实现

先明确需求:如果是要拷贝整个双向链表,那得用迭代的方式遍历创建节点;如果只是拷贝单个节点(让新节点的Next/Prev指向原节点对应指针的拷贝),那要加空指针判断。我两种情况都给你写出来:

情况1:拷贝整个双向链表(最常用的场景)

用迭代代替递归,逐个创建节点并维护Next/Prev的指向,避免递归爆炸:

// 拷贝构造函数:完整拷贝整个双向链表
Node::Node(const Node& h) {
    data = h.data;
    Node* originalCurr = h.Next;
    Node* newCurr = this;

    // 遍历原链表:如果是循环链表,就遍历到回到原头节点;普通链表就到nullptr
    while (originalCurr != &h && originalCurr != nullptr) {
        Node* newNode = new Node(originalCurr->data);
        // 绑定当前新节点和下一个新节点的双向指针
        newCurr->Next = newNode;
        newNode->Prev = newCurr;
        // 移动指针继续遍历
        newCurr = newNode;
        originalCurr = originalCurr->Next;
    }

    // 处理链表尾部:循环链表的话,尾部Next指向拷贝后的头节点,头节点Prev指向尾部
    if (originalCurr == &h) {
        newCurr->Next = this;
        this->Prev = newCurr;
    } else {
        // 普通链表的话,尾部Next设为nullptr,头节点Prev设为nullptr
        newCurr->Next = nullptr;
        this->Prev = nullptr;
    }
}

情况2:仅拷贝单个节点(深拷贝当前节点的Next/Prev指向的节点)

这种场景比较少见,但如果是你的需求,要加空指针判断:

// 单个节点的深拷贝:只拷贝当前节点,以及Next/Prev指向的节点(不遍历整个链表)
Node::Node(const Node& h) {
    data = h.data;
    // 原Next不为空才创建新节点,否则设为nullptr
    Next = h.Next ? new Node(*h.Next) : nullptr;
    // 原Prev不为空才创建新节点,否则设为nullptr
    Prev = h.Prev ? new Node(*h.Prev) : nullptr;
}

赋值运算符:用拷贝并交换法最安全

这是C++里处理赋值运算符的最佳实践,自动解决自我赋值和异常安全问题:

#include <algorithm> // 用到std::swap

// 先写个swap辅助函数,用于交换两个Node的成员
void swap(Node& first, Node& second) noexcept {
    using std::swap;
    swap(first.Next, second.Next);
    swap(first.Prev, second.Prev);
    swap(first.data, second.data);
}

Node& Node::operator=(const Node& t) {
    // 先创建临时拷贝,再和当前对象交换,临时对象销毁时自动释放旧资源
    Node temp(t);
    swap(*this, temp);
    return *this;
}

析构函数也要修正

你原来的析构函数如果是处理整个链表,会无限递归删除(因为删除当前节点会删Next,Next的析构又删它的Next,循环起来没完),得调整:

Node::~Node() {
    // 如果是循环链表,先断开循环,避免无限递归删除
    if (Next != nullptr && Next->Prev == this) {
        Next->Prev = nullptr;
    }
    // 只删除Next指针,让递归只朝一个方向走(避免双向递归)
    delete Next;
    // 注意:别删Prev,否则Prev的析构会删它的Next(也就是当前节点),导致重复删除
}

核心要点提醒

  • 别用递归拷贝双向链表,迭代才是靠谱的方式,不然很容易栈溢出或者空指针崩溃。
  • 任何时候访问指针前,先检查是不是nullptr,这是C++里的基本操作。
  • 赋值运算符一定要处理自我赋值,拷贝并交换法是最省心的方案,不用自己写一堆判断。
  • 异常安全很重要:如果创建新节点失败,要保证原有对象的状态不会被破坏,拷贝并交换法天然满足这点。

内容的提问来源于stack exchange,提问作者Jay

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.06 11:07:31