含Next/Prev指针的Node类拷贝构造与赋值运算符实现错误排查
你的双向链表节点拷贝问题分析与修复方案
嘿,我仔细看了你的Node类拷贝构造函数和赋值运算符的实现,发现几个核心问题,这就是你拷贝后数据不对甚至程序可能崩溃的原因:
- 无限递归死循环:你在拷贝构造里直接
new Node(*h.Next),这会再次触发拷贝构造函数,要是原链表是循环的,或者哪怕是普通链表的首尾节点(Next/Prev为nullptr),要么会无限递归到栈溢出,要么直接解引用空指针导致程序崩掉。 - 空指针没处理:如果原节点的Next或Prev是
nullptr,你直接去解引用*h.Next属于未定义行为,肯定要出问题。 - 赋值运算符没防自我赋值:要是你把节点赋值给自己,先执行
delete Next; delete Prev;会把当前节点的指针先销毁,之后再访问t.Next时,那已经是被释放的内存了,结果完全不可控。 - 深拷贝逻辑错了:你想做深拷贝,但现在的写法要么递归炸栈,要么没正确处理指针指向的关系,导致拷贝后的节点链完全混乱。
修复后的代码实现
先明确需求:如果是要拷贝整个双向链表,那得用迭代的方式遍历创建节点;如果只是拷贝单个节点(让新节点的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
相关产品推荐
相关产品推荐

