【C++小白日记】手搓STL list容器!从0到1实现双向链表

【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容器,成就感爆棚~

📝 学习心得和避坑指南

  1. 双向链表一定要画图 🖌️:刚开始我光靠脑子想,结果越想越乱。后来在纸上画节点和指针,一下子就清晰了!
  2. 迭代器的Ref和Ptr模板参数 🤯:这是最难的部分!记住:普通迭代器传T&和T*,const迭代器传const T&和const T*,这样就能用一个类实现两种迭代器。
  3. 头节点是关键 🔑:永远不要动头节点本身,只动它的指针!这样不管链表是否为空,操作方式都统一。

🌟 总结

实现list容器让我彻底明白了双向链表的原理,也终于懂了迭代器为什么那么重要。虽然过程中踩了不少坑(比如忘记处理头节点导致崩溃💥),但完成后真的超有成就感!

如果你也是C++初学者,强烈建议亲手实现一遍STL容器~ 这比看十篇教程都有用!有问题欢迎在评论区交流,我们一起进步!😊

相关推荐
瑾曦6 分钟前
Maven高级
后端
bobz9658 分钟前
操作系统驱动崩溃为什么会导致系统卡顿?
后端
JVM高并发11 分钟前
MySQL 中处理 JSON 数组并为每个元素拼接字符串
后端·mysql
lgaof65822@gmail.com17 分钟前
ASP.NET Core Web API 中集成 DeveloperSharp.RabbitMQ
后端·rabbitmq·asp.net·.netcore
天天摸鱼的java工程师36 分钟前
面试官说:“设计一个消息中间件你会怎么做?”我当场就不困了 ☕️🚀
java·后端·面试
七七&5561 小时前
Spring全面讲解(无比详细)
android·前端·后端
Java水解1 小时前
【MySQL基础】MySQL复合查询全面解析:从基础到高级应用
后端·mysql
小马哥聊DevSecOps1 小时前
将 RustFS 用作 GitLab 对象存储后端
后端
生无谓1 小时前
java中的异常
后端
这世界那么多上官婉儿1 小时前
多实例的心跳检测不要用lock锁
java·后端