
🎬 博主名称 :键盘敲碎了雾霭
🔥 个人专栏 : 《C语言》《数据结构》 《C++》 《Matlab》 《Python》
⛺️指尖敲代码,雾霭皆可破

文章目录
- 一、链表节点设计:双向链表的基石
-
- [1.1 节点类的实现](#1.1 节点类的实现)
- 二、list框架与核心成员函数
-
- [2.1 list类的成员变量](#2.1 list类的成员变量)
- [2.2 构造函数与初始化](#2.2 构造函数与初始化)
- [2.3 深拷贝](#2.3 深拷贝)
- [2.4 赋值运算符重载:传统写法 vs 现代写法](#2.4 赋值运算符重载:传统写法 vs 现代写法)
- [2.5 构造函数重载与冲突解决](#2.5 构造函数重载与冲突解决)
- 三、修改操作:插入与删除
-
- [3.1 插入操作insert](#3.1 插入操作insert)
- [3.2 删除操作erase](#3.2 删除操作erase)
- 四、其他关键函数实现
-
- [4.1 容量操作](#4.1 容量操作)
- [4.2 自定义swap的高效实现](#4.2 自定义swap的高效实现)
- 文章结语
一、链表节点设计:双向链表的基石
1.1 节点类的实现
双向链表的核心是节点(Node),每个节点需要存储数据、指向前驱节点的指针和指向后继节点的指针。我们将节点定义为模板结构体,以支持任意类型的数据存储:
cpp
template<class T>
struct ListNode
{
ListNode(const T& x = T())
: _data(x)
, _next(nullptr)
, _pre(nullptr)
{}
T _data; // 存储的数据
ListNode* _next; // 指向下一个节点
ListNode* _pre; // 指向前一个节点
};
设计要点解析:
- 构造函数提供了默认参数
T(),使得节点可以被默认构造(对于内置类型会进行零初始化) - 使用初始化列表而非在构造函数体内赋值,效率更高
- 节点本身不负责内存管理,由链表容器统一控制
二、list框架与核心成员函数
2.1 list类的成员变量
我们的list容器采用带哨兵节点的双向循环链表结构。这种设计可以优雅地处理边界条件,避免空指针判断:
cpp
template<class T>
class list
{
typedef ListNode<T> Node;
private:
Node* _head; // 哨兵节点指针
size_t _size; // 链表元素个数
};
哨兵节点的作用:
_head不存储实际数据,仅作为链表的起点和终点标记- 空链表时,
_head->_next和_head->_pre都指向_head自身 - 这使得
end()可以直接返回_head,而begin()返回_head->_next
2.2 构造函数与初始化
我们提供一个empty_init()函数来完成链表的初始状态设置:
cpp
void empty_init()
{
_head = new Node;
_head->_next = _head;
_head->_pre = _head;
_size = 0;
}
list()
{
empty_init();
}
2.3 深拷贝
拷贝构造函数需要执行深拷贝,即创建新节点并复制数据,而非简单复制指针:
cpp
list(const list& lt)
{
empty_init(); // 先初始化空链表
const_iterator it = lt.begin(); // 使用const迭代器遍历源链表
while (it != lt.end())
{
push_back(*it); // 逐个拷贝元素
it++;
}
}
2.4 赋值运算符重载:传统写法 vs 现代写法
传统写法(不推荐)需要处理自我赋值、释放原有资源、执行深拷贝三个步骤,代码冗长且容易出错。
现代写法(Copy-and-Swap惯用法)利用传值拷贝构造的临时对象,通过交换指针高效完成赋值:
cpp
void Swap(list& tmp)
{
std::swap(_size, tmp._size); // 注意:先交换_size
std::swap(_head, tmp._head); // 再交换_head
}
list& operator=(list tmp) // 传值,自动调用拷贝构造
{
Swap(tmp); // 交换资源
return *this; // tmp析构时自动释放原资源
}
现代写法的优势:
- 天然支持自我赋值
- 异常安全(如果拷贝构造抛出异常,原对象状态不变)
- 代码简洁优雅
2.5 构造函数重载与冲突解决
我们还提供了initializer_list构造函数,支持类似list<int> l = {1, 2, 3, 4, 5}的初始化方式:
cpp
list(std::initializer_list<T> li)
{
empty_init();
for (auto& e : li)
{
push_back(e);
}
}
三、修改操作:插入与删除
3.1 插入操作insert
insert是所有插入操作的底层实现,在指定位置之前插入新元素:
cpp
void insert(iterator pos, const T& x)
{
Node* cur = pos._node; // 当前位置节点
Node* pre = cur->_pre; // 前驱节点
Node* newnode = new Node(x);
// 建立双向连接
cur->_pre = newnode;
pre->_next = newnode;
newnode->_next = cur;
newnode->_pre = pre;
_size++;
}
基于insert,我们可以方便地实现push_back和push_front:
cpp
void push_back(const T& x)
{
insert(end(), x); // end()指向哨兵节点,在其前插入即尾部插入
}
void push_front(const T& x)
{
insert(begin(), x); // begin()指向首节点,在其前插入即头部插入
}
3.2 删除操作erase
erase删除指定位置的节点,并返回下一个有效节点的迭代器:
cpp
iterator erase(iterator pos)
{
assert(pos != end()); // 不能删除哨兵节点
Node* pre = pos._node->_pre;
Node* next = pos._node->_next;
pre->_next = next;
next->_pre = pre;
delete pos._node;
_size--;
return iterator(next); // 返回被删除节点的后继
}
同样,pop_back和pop_front可以基于erase实现:
cpp
void pop_back()
{
erase(--end()); // end()前一个节点即尾节点
}
void pop_front()
{
erase(begin()); // begin()即首节点
}
clear函数的正确实现:
cpp
void clear()
{
iterator it = begin();
while (it != end())
{
it = erase(it); // erase返回下一个有效节点,不需要it++
}
}
四、其他关键函数实现
4.1 容量操作
cpp
size_t size() const
{
return _size;
}
bool empty() const
{
return _size == 0;
}
4.2 自定义swap的高效实现
cpp
void swap(list& other)
{
std::swap(_head, other._head);
std::swap(_size, other._size);
}
这个自定义swap只交换指针和计数器,时间复杂度O(1),远优于默认的逐元素交换。
文章结语
感谢你读到这里~我是「键盘敲碎了雾霭」,愿这篇文字帮你敲开了技术里的小迷雾 💻
如果内容对你有一点点帮助,不妨给个暖心三连吧👇
👍 点赞 | ❤️ 收藏 | ⭐ 关注
(听说三连的小伙伴,代码一次编译过,bug绕着走~)
你的支持,就是我继续敲碎技术雾霭的最大动力 🚀
🐶 小彩蛋:
/^ ^\
/ 0 0 \
V\ Y /V
/ - \
/ |
V__) ||
摸一摸毛茸茸的小狗,赶走所有疲惫和bug~我们下篇见 ✨