【C++小白日记】手搓STL list容器!从0到1实现双向链表🚀
🧩 链表到底长啥样?手把手画图给你看!
刚开始学list的时候,我一直搞不懂它和vector的区别...直到看到这张图👇
list就像手拉手的小朋友们 👫👭👬,每个小朋友(节点)都牵着前一个和后一个的手,这样就形成了一条长长的队伍。而vector更像军训时的方阵,大家排得整整齐齐,每个人都有自己固定的位置。
![双向链表结构示意图] (图:带头节点的双向循环链表,头节点像个小队长,两边都指向自己)
这种结构的好处是:插入删除超方便!就像队伍中加人,只要让前后的小朋友松开手再重新牵上就行,不用像vector那样整个队伍都要移动位置~🎉
🔨 手搓开始!先搭个"节点"积木
1. 节点类:每个小朋友的"身体"
cpp
template<class T>
struct ListNode {
ListNode(const T& val = T()) : _pPre(nullptr), _pNext(nullptr), _val(val) {}
// 每个节点有三个部分:
ListNode<T>* _pPre; // 左手牵的小朋友 👈
ListNode<T>* _pNext; // 右手牵的小朋友 👉
T _val; // 节点里存的数据 📦
};
✨ 小贴士:这个结构体就像给每个小朋友穿衣服,指定了他们要有左右手(指针)和肚子里的数据(_val)。构造函数用了缺省参数,不传值也能创建节点~
🔄 迭代器:链表的"遥控器"
为什么需要迭代器?🤔
刚开始我也纳闷:直接用指针访问节点不就行了吗?后来老师一句话点醒我:迭代器就像电视遥控器📺,不管电视内部多复杂,我们只要按按钮就能换台。STL容器都用迭代器,这样不管是list还是vector,遍历方式都一样!
迭代器类实现:给遥控器装按钮
cpp
template<class T, class Ref, class Ptr>
struct ListIterator {
typedef ListNode<T>* PNode;
typedef ListIterator<T, Ref, Ptr> Self;
PNode _pNode; // 遥控器指向的当前节点 🎯
// 构造函数:拿到节点地址就能创建遥控器
ListIterator(PNode pNode = nullptr) : _pNode(pNode) {}
// 解引用:按"获取数据"按钮
Ref operator*() { return _pNode->_val; }
// ->运算符:按"获取数据地址"按钮(结构体专用)
Ptr operator->() { return &(operator*()); }
// 前置++:按"下一个"按钮 ⏭️
Self& operator++() {
_pNode = _pNode->_pNext;
return *this;
}
// 后置++:按"下一个"但先记住当前频道 ⏭️⏮️
Self operator++(int) {
Self temp(*this);
_pNode = _pNode->_pNext;
return temp;
}
// 前置--和后置--同理,就像"上一个"按钮 ⏮️
// 比较运算符:判断是否指向同一个节点
bool operator!=(const Self& it) const { return _pNode != it._pNode; }
bool operator==(const Self& it) const { return _pNode == it._pNode; }
};
🤯 我踩过的坑:刚开始把前置++和后置++写反了!记住前置++是"先用后加",后置++是"先加后用",画个图就清楚了~
📦 List容器:把零件组装成完整玩具
1. 先搭个架子:List类的成员和别名
cpp
template<class T>
class list {
typedef ListNode<T> Node;
typedef Node* PNode;
public:
// 迭代器别名,这样用起来和vector一样方便!
typedef ListIterator<T, T&, T*> iterator;
typedef ListIterator<T, const T&, const T*> const_iterator;
// ...后面填各种功能函数...
private:
PNode _pHead; // 头节点(小队长)
size_t _size; // 节点个数(队伍长度)
};
2. 构造函数:创建队伍的几种方式
2.1 默认构造:先让小队长自己牵自己的手
cpp
void CreateHead() {
_size = 0;
_pHead = new Node; // 创建头节点
_pHead->_pNext = _pHead; // 右手牵自己
_pHead->_pPre = _pHead; // 左手牵自己
}
list() { CreateHead(); } // 默认构造直接调用CreateHead
🌟 小技巧:把头节点初始化封装成CreateHead函数,后面其他构造函数也能用,减少重复代码~
2.2 填充构造:创建n个相同元素的链表
cpp
list(int n, const T& value = T()) {
CreateHead();
for (int i = 0; i < n; ++i) {
push_back(value); // 后面会实现的尾插函数
}
}
💡 我一开始直接写循环new节点,后来发现用push_back更简洁!复用代码yyds~
2.3 迭代器区间构造:把其他容器的数据拿过来
cpp
template<class Iterator>
list(Iterator first, Iterator last) {
CreateHead();
while (first != last) {
push_back(*first); // 遍历区间,一个个尾插
++first;
}
}
3. 迭代器接口:给容器装"门"
cpp
iterator begin() { return iterator(_pHead->_pNext); } // 第一个数据节点
iterator end() { return iterator(_pHead); } // 头节点(尾后位置)
const_iterator begin() const { return const_iterator(_pHead->_pNext); }
const_iterator end() const { return const_iterator(_pHead); }
🚪 想象:begin()是前门(第一个房间),end()是后门(最后一个房间后面),遍历就是从前门走到后门~
4. 容量和访问:看看队伍多长,第一个人是谁
cpp
size_t size() const { return _size; } // 队伍长度
bool empty() const { return begin() == end(); } // 是不是空队伍
T& front() { return *begin(); } // 第一个人的数据
const T& front() const { return *begin(); }
T& back() { return *(--end()); } // 最后一个人的数据(先--end())
const T& back() const { return *(--end()); }
5. 增删改查:队伍的基本操作
尾插和尾删:队伍末尾加人/减人
cpp
void push_back(const T& val) {
Node* newNode = new Node(val); // 新节点
Node* tail = _pHead->_pPre; // 找到当前尾节点
// 原来的尾节点和头节点牵手
tail->_pNext = newNode;
newNode->_pPre = tail;
newNode->_pNext = _pHead;
_pHead->_pPre = newNode;
_size++; // 队伍人数+1
}
void pop_back() {
if (empty()) return; // 空队伍不能删
Node* tail = _pHead->_pPre;
Node* prev = tail->_pPre;
prev->_pNext = _pHead; // 倒数第二个人牵回头节点
_pHead->_pPre = prev;
delete tail; // 删掉尾节点
_size--;
}
🧩 小口诀:尾插四步走------找尾巴、新节点前后牵手、头节点回头牵手、人数加一!
🎬 完整代码和测试:跑起来看看!
测试代码:创建链表并遍历
cpp
void TestList() {
list<int> l;
l.push_back(1);
l.push_back(2);
l.push_back(3);
cout << "遍历链表:";
for (auto it = l.begin(); it != l.end(); ++it) {
cout << *it << " "; // 应该输出 1 2 3
}
cout << endl;
cout << "链表长度:" << l.size() << endl; // 应该输出 3
cout << "第一个元素:" << l.front() << endl; // 应该输出 1
}
🎉 当我第一次成功跑通这段代码时,激动得差点拍桌子!原来自己也能实现STL容器,成就感爆棚~
📝 学习心得和避坑指南
- 双向链表一定要画图 🖌️:刚开始我光靠脑子想,结果越想越乱。后来在纸上画节点和指针,一下子就清晰了!
- 迭代器的Ref和Ptr模板参数 🤯:这是最难的部分!记住:普通迭代器传T&和T*,const迭代器传const T&和const T*,这样就能用一个类实现两种迭代器。
- 头节点是关键 🔑:永远不要动头节点本身,只动它的指针!这样不管链表是否为空,操作方式都统一。
🌟 总结
实现list容器让我彻底明白了双向链表的原理,也终于懂了迭代器为什么那么重要。虽然过程中踩了不少坑(比如忘记处理头节点导致崩溃💥),但完成后真的超有成就感!
如果你也是C++初学者,强烈建议亲手实现一遍STL容器~ 这比看十篇教程都有用!有问题欢迎在评论区交流,我们一起进步!😊