跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像AI 生图工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++算法

C++ STL list 模拟实现:双向链表与迭代器封装

C++ STL list 基于双向循环链表实现,支持常数时间插入删除但不支持随机访问。详细解析了 list 的节点结构、迭代器设计(含运算符重载)、默认成员函数(构造、拷贝、赋值、析构)及核心接口(增删查改)。重点阐述了迭代器失效机制、list::sort 与 std::sort 的效率差异及优化方案,通过代码模拟还原容器底层逻辑。

墨染流年发布于 2026/2/8更新于 2026/7/208.2K 浏览
C++ STL list 模拟实现:双向链表与迭代器封装

一、List 的介绍

list 是 STL 中支持在任意位置高效(常数时间)插入、删除的双向迭代序列式容器,底层基于带头双向链表实现 —— 每个元素存于独立节点,通过指针连接前后元素;它和 forward_list 类似但后者是单链表、仅支持前向迭代;和 array、vector、deque 相比,list 的插入删除效率更优,但缺点是不支持随机访问(访问第 n 个元素需线性遍历),且每个节点的指针会占用额外空间(对存储小元素的大 list 影响较明显)。

二、默认成员函数

1、List 的节点结构、容器结构
节点结构
namespace ljh {
    template<class T> struct list_node {
        list_node(const T& val) : _next(nullptr), _prev(nullptr), _val(val) {}
        T _val;
        list_node<T>* _next;
        list_node<T>* _prev;
    };
}

成员变量设为公有,是为了让后续写 list 容器 / 迭代器时,能直接操作节点的 _next/_prev/_val,省写 get/set,简化代码。

迭代器结构
namespace ljh {
    template<class T> struct _list_iterator {
        typedef list_node<T> Node;
        _list_iterator(Node* node) :_node(node) {}
        Node* _node;
    };
}
链表的迭代器为啥不能直接用原生指针?

因为链表的原生指针只能访问节点本身,而迭代器需要模拟'像普通指针一样解引用取数据、++/-- 遍历'的行为。原生指针解引用得到的是整个节点,不是数据;且链表的'下一个元素'需要通过 _next 指针跳转,原生指针的 ++ 是地址 +1,不符合链表的节点连接逻辑,所以得封装迭代器类来重载 *、++ 等运算符。

迭代器结构为啥用 struct?

迭代器结构体用 struct,是因为迭代器只是遍历容器的工具,不用刻意封装成私有,用 struct 让成员直接暴露,能简化后续迭代器功能的实现。

迭代器为啥不能写析构函数?

不能为迭代器编写析构函数,因为节点的内存是由容器管理的,迭代器只是'借用节点指针来访问元素',本身并不持有节点的所有权。若在迭代器析构时释放节点,会导致容器内的节点被非法销毁。

链表结构
namespace ljh {
    template<class T> class list {
        typedef list_node<T> Node;
    private:
        Node* _head;
        size_t _size;
    };
}
2、List 构造函数
void empty_init() {
    _head = new Node(-1);
    _head->_prev = _head;
    _head->_next = _head;
    _size = 0;
}
list() { empty_init(); }
3、List 拷贝构造函数
list(const list<T>& lt) {
    empty_init();
    for (auto& e : lt) {
        push_back(e);
    }
}
4、List 赋值运算符重载
void swap(list<T>& lt) {
    std::swap(_head, lt._head);
    std::swap(_size, lt._size);
}
list<T>& operator=(list<T> lt) {
    swap(lt);
    return *this;
}

利用拷贝构造 + 交换实现赋值运算符重载,高效且安全。

5、List 析构函数
void clear() {
    iterator it = begin();
    while (it != end()) {
        it = erase(it);
    }
    _size = 0;
}
~list() {
    clear();
    delete _head;
    _head = nullptr;
}

三、迭代器

1、begin/end
typedef _list_iterator<T, T&, T*> iterator;
typedef _list_iterator<T, const T&, const T*> const_iterator;
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); }

目前我们先不写反向迭代器,等学到后面的容器适配器部分时再讲解它的实现方式。

2、迭代器的运算符重载
template<class T, class Ref, class Ptr >
struct _list_iterator {
    typedef list_node<T> Node;
    typedef _list_iterator<T, Ref, Ptr> self;
    Node* _node;
    _list_iterator(Node* node) :_node(node) { }
    Ref operator*() { return _node->_val; }
    Ptr operator->() { return &this->_node->_val; }
    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) { return _node == it._node; }
};
2.1 operator*

这里返回的是 _node 节点存储的数据,返回类型用 T&,目的是避免传递自定义类型时产生不必要的拷贝开销。

Ref 参数的作用?

用 Ref 替代具体的返回值类型,是为了通过这个模板参数适配 const 迭代器:当第二个模板参数传入的是 const T& 时,就代表这是一个 const 迭代器(此时解引用返回的是只读引用)。

2.2 operator->

重载这个运算符,是为了应对数据是结构体的场景。为了让迭代器的用法和原生指针一致,C++ 编译器对迭代器的 operator-> 做了'语法糖'优化,允许省略一次 ->,让 it->_a2 等价于 (it.operator->())->_a2。

ptr 模版参数的作用?

Ptr 的作用在此体现:区分普通迭代器返回的 T*(可读写结构体成员)和 const 迭代器返回的 const T*(只读),实现一套模板复用,保证 const 迭代器的安全性。

2.3 operator 前置++/--

前置 ++ 和 -- 的逻辑很相似:都是直接修改当前迭代器的节点指针,然后返回自身的引用。返回自身引用的好处是支持链式操作,而且没有临时对象的开销,效率更高。

2.4 operator 后置++/--

后置 ++ 和 -- 的核心是'先返回原状态,再移动'。函数参数里的 int 是个占位符,实现时会先创建一个临时迭代器保存当前状态,然后移动 _node,最后返回这个临时迭代器。因为会创建临时对象,它的效率比前置版本略低。

2.5 operator!=/==

这两个运算符的逻辑很直接:判断两个迭代器的 _node 指针是否指向同一个节点。其中 operator!= 后面加了 const,是为了保证调用这个函数时不会修改当前迭代器的状态。

2.6 迭代器拷贝构造问题?

lt.begin() 返回临时迭代器,it 是用它拷贝构造而来的。编译器默认生成的拷贝构造是浅拷贝,只会复制内部节点指针,让 it 和原临时迭代器指向链表中同一个节点。这刚好满足需求,深拷贝不仅多此一举,还会带来巨大开销,因此迭代器不用写拷贝构造。

四、list 增删查改

1、push_back
void push_back(const T& x) {
    Node* tail = _head->_prev;
    Node* newnode = new Node(x);
    tail->_next = newnode;
    newnode->_prev = tail;
    newnode->_next = _head;
    _head->_prev = newnode;
    ++_size;
}

先找到链表的尾节点,再创建新节点;接着把 tail 的 next 指向新节点、新节点的 prev 指向 tail,完成新节点和原尾节点的连接;最后让新节点的 next 指向头节点、头节点的 prev 指向新节点,维持链表的循环结构。

2、insert
iterator insert(iterator pos, const T& x) {
    Node* cur = pos._node;
    Node* prev = cur->_prev;
    Node* newnode = new Node(x);
    prev->_next = newnode;
    newnode->_next = cur;
    cur->_prev = newnode;
    newnode->_prev = prev;
    ++_size;
    return newnode;
}

先从迭代器 pos 里拿到它指向的节点 cur,再找到 cur 的前驱节点 prev,同时创建新节点 newnode;把 prev 的 next 指向新节点、新节点的 next 指向 cur,再让 cur 的 prev 指向新节点、新节点的 prev 指向 prev—— 这样就把新节点'夹'在了 prev 和 cur 之间。

3、push_front
void push_front(const T& x) {
    insert(begin(), x);
}

直接复用 insert,在 begin() 前插入元素 x,既简化代码又保证逻辑一致。

4、pop_back
void pop_back() {
    assert(_head->_next != _head);
    Node* del = _head->_prev;
    Node* tail = del->_prev;
    tail->_next = _head;
    _head->_prev = tail;
    delete del;
    _size--;
}

先断言链表非空,找到尾节点和它的前驱节点,重新建立前驱节点与头节点的循环连接,释放原尾节点内存,最后更新链表长度。

5、erase
iterator erase(iterator pos) {
    assert(pos != end());
    Node* cur = pos._node;
    Node* prev = cur->_prev;
    Node* next = cur->_next;
    prev->_next = next;
    next->_prev = prev;
    delete cur;
    --_size;
    return next;
}

先断言避免删除无效节点,找到待删节点的前后节点并重新建立连接,释放待删节点内存,更新链表长度后返回后继节点迭代器防止失效。

6、pop_front
void pop_front() {
    erase(begin());
}

直接复用 erase 函数,删除 begin() 对应的头节点,既简化代码又保证逻辑一致。

7、list 迭代器失效问题

insert 操作:list 节点物理离散,插入仅新增节点、不改变原有节点地址,因此原迭代器不会失效;返回新节点迭代器,可按需更新使用。 erase 操作:仅被删除节点的迭代器失效(节点内存被释放),其他迭代器仍有效;返回被删节点的后继节点迭代器,需用它更新原迭代器,避免访问失效节点。

五、其他接口

1、swap
void swap(list<T>& lt) {
    std::swap(_head, lt._head);
    std::swap(_size, lt._size);
}

通过 std::swap 分别交换两个链表的头节点指针和元素个数,实现两个链表数据的高效交换(无需拷贝 / 移动节点,仅交换两个核心成员,时间复杂度 O(1))。

2、clear
void clear() {
    iterator it = begin();
    while (it != end()) {
        it = erase(it);
    }
    _size = 0;
}

通过遍历 + 复用 erase 删除所有节点,利用 erase 返回的后继迭代器避免失效,最后重置 _size 完成清空。

3、迭代器性质方面分类

单向迭代器:仅支持 ++(向后移动),对应底层是单向链表 / 哈希结构的容器(如 forward_list、unordered_map); 双向迭代器:支持 ++/--(前后移动),对应底层是双向链表 / 树形结构的容器(如 list、map、set); 随机迭代器:支持 ++/--/+/-(任意位置跳转),对应底层是连续存储的容器(如 vector、string、deque)。

4、sort
问题 1:list 不能用 std::sort 的原因

std::sort 算法要求迭代器是随机迭代器(支持 +/- 等随机跳转操作),但 list 的迭代器是双向迭代器(仅支持 ++/--),不满足 std::sort 的要求,因此 list 无法直接使用全局的 std::sort。

问题 2:list 的内置 sort 成员函数

list 提供了自己的 sort 成员函数,但它的效率存在局限性:因为链表是离散存储的,元素不连续,缓存利用率低,所以 list::sort 的效率比 std::sort 要差;仅在数据量较小时,list::sort 的效率尚可;数据量较大时,和 std::sort 的效率差异会非常明显。

std::sort 和 list::sort 效率对比
void test_op() {
    srand((unsigned)time(NULL));
    const int N = 5000000;
    vector<int> v;
    v.reserve(N);
    list<int> lt1;
    for (int i = 0; i < N; ++i) {
        auto e = rand();
        v.push_back(e);
        lt1.push_back(e);
    }
    int begin1 = clock();
    sort(v.begin(), v.end());
    int end1 = clock();
    int begin2 = clock();
    lt1.sort();
    int end2 = clock();
    printf("vector sort:%d\n", end1 - begin1);
    printf("list sort:%d\n", end2 - begin2);
}

数据量越小,差异越小:当数据量仅 5 万时,两者效率接近; 数据量越大,差异越悬殊:数据量到 500 万时,vector sort 耗时远低于 list sort; 核心原因:vector 是连续存储,std::sort 能利用缓存高效排序;而 list 是离散节点,list::sort 缓存利用率低,数据量放大后效率劣势会被急剧放大。

优化方案

1、拷贝数据:把 std::list 中的元素拷贝到 std::vector 中; 2、高效排序:用 std::sort 对 vector 中的数据排序; 3、拷贝回写:将排序后的 vector 数据再拷贝回 std::list。 借助 vector 的连续存储 + 缓存友好,结合 std::sort 的高效实现,能大幅提升 list 数据的排序性能。

void test_op() {
    srand((unsigned)time(NULL));
    const int N = 5000000;
    list<int> lt1;
    list<int> lt2;
    for (int i = 0; i < N; ++i) {
        auto e = rand();
        lt1.push_back(e);
        lt2.push_back(e);
    }
    int begin1 = clock();
    vector<int> v;
    v.reserve(N);
    for (auto& e : lt1) {
        v.push_back(e);
    }
    sort(v.begin(), v.end());
    int i = 0;
    for (auto& e : lt1) {
        e = v[i++];
    }
    int end1 = clock();
    int begin2 = clock();
    lt2.sort();
    int end2 = clock();
    printf("list→vector→std::sort 耗时:%d\n", end1 - begin1);
    printf("list::sort 直接排序 耗时:%d\n", end2 - begin2);
}

从运行结果能明显看到:'list→vector→std::sort' 效率远高于 'list::sort 直接排序',充分体现了借助 vector+std::sort 优化 list 排序的显著优势。

目录

  1. 一、List 的介绍
  2. 二、默认成员函数
  3. 1、List 的节点结构、容器结构
  4. 节点结构
  5. 迭代器结构
  6. 链表的迭代器为啥不能直接用原生指针?
  7. 迭代器结构为啥用 struct?
  8. 迭代器为啥不能写析构函数?
  9. 链表结构
  10. 2、List 构造函数
  11. 3、List 拷贝构造函数
  12. 4、List 赋值运算符重载
  13. 5、List 析构函数
  14. 三、迭代器
  15. 1、begin/end
  16. 2、迭代器的运算符重载
  17. 2.1 operator*
  18. Ref 参数的作用?
  19. 2.2 operator->
  20. ptr 模版参数的作用?
  21. 2.3 operator 前置++/--
  22. 2.4 operator 后置++/--
  23. 2.5 operator!=/==
  24. 2.6 迭代器拷贝构造问题?
  25. 四、list 增删查改
  26. 1、push_back
  27. 2、insert
  28. 3、push_front
  29. 4、pop_back
  30. 5、erase
  31. 6、pop_front
  32. 7、list 迭代器失效问题
  33. 五、其他接口
  34. 1、swap
  35. 2、clear
  36. 3、迭代器性质方面分类
  37. 4、sort
  38. 问题 1:list 不能用 std::sort 的原因
  39. 问题 2:list 的内置 sort 成员函数
  40. std::sort 和 list::sort 效率对比
  41. 优化方案
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • VSCode 中使用 Git 进行版本控制及同步至 Gitee
  • Python Web 框架对比与实战:Django、Flask 与 FastAPI
  • Android 设备 Termux 安装 llama.cpp 并启动 WebUI
  • Microsoft Visual C++ Redistributable 运行库安装与修复指南
  • 2024 第十六届蓝桥杯模拟赛第二期 Python 题解
  • IntelliJ IDEA 与 Git 本地及远程分支创建与合并指南
  • C++ 网络编程实战:基于 TCP 协议的简易计算器
  • MiniCPM-V-2_6 数字孪生:工厂实景图识别与 IoT 数据关联可视化
  • ROS 机器人开发实战:Linux 终端命令与基础环境搭建指南
  • Rust 内存管理与零成本抽象深度解析
  • Python 办公自动化核心知识点与实战指南
  • Clang 17 下的 C++26 特性实战指南
  • VSCode 远程配置下 GitHub Copilot 无法使用问题排查与解决
  • 基于 Rokid AR 眼镜的 Android 喝水提醒应用开发
  • GitHub 7 大 Claude Skills 开源项目:Skill Creator、Superpowers 与 Code Review 实战指南
  • LLaMA-Factory 安装与配置指南
  • 前端函数防抖详解:原理、手写实现与实战应用
  • Python 并发编程:多线程与多进程实战
  • AI 大模型算法工程师求职指南与高薪职位攻略
  • llama.cpp 本地部署性能调优:从启动瓶颈到推理效率优化

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online