C++ list 模拟实现(一):手撕双向链表,从节点到迭代器

C++ list 模拟实现(一):手撕双向链表,从节点到迭代器

摘要:上一篇我们讲了 list 迭代器为什么不是指针,很多读者留言说懂了理论但不知道怎么手写。本文将承接上一篇,手把手带你从零实现一个带头双向循环链表。涵盖节点设计、迭代器封装、核心接口实现。文末附完整可运行代码。

一、结构设计:带头双向循环链表

SGI STL 的 list 底层是一个带头节点的双向循环链表。为什么要带头节点?

  • 省事:插入和删除操作不用特判"是否在头部"或"是否为空",代码复用率极高。

  • 闭环 :头节点的 _next 指向第一个真实节点,头节点的 _prev 指向最后一个真实节点。

    复制代码
         ┌────────────────────────────────────────┐
         │                                        │
         ▼                                        │
    [头节点] <──> [节点1] <──> [节点2] <──> [节点3] ─┘

    (不存数据) 存数据 存数据 存数据

当链表为空时,头节点的 _next 和 _prev 都指向自己。

二、节点类 ListNode 设计

cpp 复制代码
template <typename T>
struct ListNode {
    ListNode* _prev;
    ListNode* _next;
    T _data;

    ListNode(const T& val = T())
        : _prev(nullptr), _next(nullptr), _data(val) {}
};

三、迭代器类 ListIterator 封装

这是 list 设计的灵魂。它内部包含一个节点指针,通过重载各种运算符来伪装成一个指针。

cpp 复制代码
template <typename T, typename Ref, typename Ptr>
struct ListIterator {
    typedef ListNode<T> Node;
    typedef ListIterator<T, Ref, Ptr> Self;
    Node* _node;

    ListIterator(Node* node) : _node(node) {}

    // 解引用
    Ref operator*() { return _node->_data; }
    // 成员访问
    Ptr operator->() { return &(_node->_data); }

    // 前置++
    Self& operator++() {
        _node = _node->_next;
        return *this;
    }
    // 后置++
    Self operator++(int) {
        Self tmp(*this);
        _node = _node->_next;
        return tmp;
    }
    // 前置--
    Self& operator--() {
        _node = _node->_prev;
        return *this;
    }
    // 后置--
    Self operator--(int) {
        Self tmp(*this);
        _node = _node->_prev;
        return tmp;
    }

    bool operator!=(const Self& it) const { return _node != it._node; }
    bool operator==(const Self& it) const { return _node == it._node; }
};

💡 设计亮点 :使用了三个模板参数 T, Ref, Ptr,通过传入 T&, T*(普通迭代器)或 const T&, const T*(const迭代器)来复用同一套代码。这是 STL 的经典技巧,面试说出这点绝对加分。

四、list 类的核心骨架

cpp 复制代码
template <typename T>
class list {
    typedef ListNode<T> Node;
public:
    typedef ListIterator<T, T&, T*> iterator;
    typedef ListIterator<T, const T&, const T*> const_iterator;

    // 构造函数:创建空的头节点
    list() {
        _head = new Node();
        _head->_next = _head;
        _head->_prev = _head;
    }

    // 析构函数:清理所有节点
    ~list() {
        clear();
        delete _head;
        _head = nullptr;
    }

    iterator begin() { return iterator(_head->_next); }
    iterator end() { return iterator(_head); }
    const_iterator begin() const { return const_iterator(_head->_next); }
    const_iterator end() const { return const_iterator(_head); }

    void push_back(const T& x) { insert(end(), x); }
    void push_front(const T& x) { insert(begin(), x); }

    // 在 pos 位置前插入节点
    iterator insert(iterator pos, const T& x) {
        Node* cur = pos._node;
        Node* prev = cur->_prev;
        Node* newnode = new Node(x);

        prev->_next = newnode;
        newnode->_prev = prev;
        newnode->_next = cur;
        cur->_prev = newnode;

        return iterator(newnode);
    }

    // 删除 pos 位置的节点
    iterator erase(iterator pos) {
        Node* cur = pos._node;
        Node* prev = cur->_prev;
        Node* next = cur->_next;

        prev->_next = next;
        next->_prev = prev;
        delete cur;

        return iterator(next);
    }

    void clear() {
        iterator it = begin();
        while (it != end()) {
            it = erase(it);
        }
    }

private:
    Node* _head;
};

五、核心测试代码

cpp 复制代码
void test_list() {
    list<int> lt;
    lt.push_back(1);
    lt.push_back(2);
    lt.push_back(3);
    lt.push_front(0);

    cout << "遍历: ";
    for (auto e : lt) cout << e << " ";
    cout << endl; // 期望:0 1 2 3

    // 测试 erase 返回值
    auto it = lt.begin();
    ++it;
    lt.erase(it);  // 删除 1
    cout << "删除后: ";
    for (auto e : lt) cout << e << " ";
    cout << endl; // 期望:0 2 3
}

六、面试高频 FAQ

Q1:list 的 insert 会导致迭代器失效吗?

不会。 list 插入时只改变相邻节点的指针,原有节点的内存地址并没有改变。除了被删除的那个节点,其他迭代器依然有效。

Q2:为什么 list 的迭代器要重载 ++ 和 -- ?

因为 list 的内存是不连续的,原生指针的 ++(地址加 sizeof)无法找到下一个节点。必须通过重载 ++ 来实现 _node = _node->_next。

Q3:为什么 list 的迭代器不支持 it + 3?

因为链表不支持随机访问(Random Access)。找到第 n 个节点必须从头遍历,时间复杂度是 O(n),所以标准库没有实现 operator+。

七、总结与挑战

核心点 结论
底层结构 带头双向循环链表
迭代器本质 封装了 Node* 的类,重载了运算符
插入删除 O(1),不会导致其他迭代器失效
随机访问 不支持,只能遍历

核心结论 :list 的模拟实现比 vector 更考验面向对象设计能力。理解了迭代器封装,你就掌握了 STL 迭代器模式的核心。建议亲手敲一遍代码,并尝试实现 pop_back、pop_front 和 size()。

👇 评论区互动 :

你觉得 list 的代码难度比 vector 高还是低?

扣1 觉得 list 简单,扣2 觉得迭代器封装太绕了。

评论区见!


🔥 系列爆款推荐 :

👉 [C++ vector 迭代器失效:一张图+3句话,面试直接背](#C++ vector 迭代器失效:一张图+3句话,面试直接背)

👉 [C++ list 迭代器为什么不是指针?一张图讲透双向链表底层](#C++ list 迭代器为什么不是指针?一张图讲透双向链表底层)

相关推荐
wespaul1 小时前
一条自然语言命令的 49 个节点:可视化编排与每段强校验
前端·数据库
码爸9 小时前
线性回归算法
算法·机器学习·线性回归
KIDULT°9 小时前
MySQL系列 (1):数据库原理
运维·数据库·mysql·云计算
我不是程序员三三10 小时前
透明加密软件偶发解密异常排错实录:文件时能打开时乱码
数据库·域智盾
snhna10 小时前
向量数据库深度实战:从 HNSW 到 pgvector、Milvus 选型与 RAG 落地
数据库·人工智能·python·postgresql·milvus
Coder-magician10 小时前
《代码随想录》刷题打卡day51:图论-part09(dijistra堆优化,Bellman_ford算法)
算法·图论
Nil20810 小时前
leetcode 763划分字母区间
算法·leetcode·职场和发展
不会就选b10 小时前
算法日常・每日刷题--<动态规划>20
算法·leetcode·动态规划
天行健,君子而铎10 小时前
数据分类分级的范式转换:从规则匹配到场景化高准确率一键部署
网络·数据库·安全