跳到主要内容
极客日志极客日志
首页博客AI提示词GitHub精选代理工具
搜索
|注册
博客列表
C++算法

C++ 手写 List 容器:双向链表原理及实现

C++ 手写 List 容器基于带头双向循环链表实现。对比 vector 分析插入删除效率与迭代器失效差异。涵盖节点结构、迭代器重载、容器类接口(构造、插入、删除、遍历)及内存管理。通过测试用例验证 push_back、erase、clear 等功能正确性,帮助理解底层逻辑与指针操作。

灵魂摆渡发布于 2026/3/15更新于 2026/4/255 浏览
C++ 手写 List 容器:双向链表原理及实现

C++ 手写 List 容器:双向链表原理及实现

前言

日常开发中,我们频繁调用 std::list 的 push_back、erase 等接口,却常忽略其'为何插入删除高效''迭代器为何只在删除时失效'等核心问题。面试被要求手写 List 时卡壳、开发中因迭代器失效导致程序崩溃,根源都在对底层逻辑的不理解。

一、底层原理:List 容器的骨架——带头双向循环链表

要手写 List,先明确其底层结构——带头双向循环链表,这是所有接口高效实现的基础。

1.1 结构组成与优势

结构部分功能说明
哨兵头节点不存储有效数据,仅作为操作锚点,统一空/非空链表的插入、删除逻辑,无需额外判断边界。例如:尾插时无需检查'是否为第一个节点',直接通过头节点的前驱指针定位尾节点,简化代码逻辑。
数据节点每个节点含 _prev(前驱指针)、_next(后继指针)、_data(数据域),支持双向遍历。既可以从当前节点向前追溯前驱节点,也能向后访问后继节点,为迭代器的 ++/-- 操作提供底层支持。
循环特性尾节点 _next 指向头节点,头节点 _prev 指向尾节点,形成闭环。例如:遍历到尾节点后,通过 _next 可直接回到头节点;获取尾节点无需遍历整个链表,只需访问 _head->_prev,提升尾操作效率。

通过这种结构设计,List 容器实现了任意位置插入/删除 O(1) 效率、遍历逻辑统一、边界处理简化三大核心优势,是区别于 vector 动态数组的关键设计。

1.2 核心特性(对比 vector)

特性List(双向链表)vector(动态数组)
插入删除效率任意位置 O(1)(仅需调整节点前驱/后继指针,无需移动其他元素)。例如:在链表中间插入新节点时,只需修改目标位置前后节点的 _prev 和 _next 指针,操作耗时与链表长度无关。中间位置 O(N)(插入/删除后需搬移后续所有元素);尾端操作(无扩容时)接近 O(1)。例如:在数组第 5 位插入元素,需将第 5 位及之后的所有元素向后移动 1 位,元素越多耗时越长。
随机访问不支持(需从表头/表尾遍历,时间复杂度 O(N))。无法通过'容器名 [下标]'直接访问元素,必须通过迭代器逐步移动(++/--)才能定位目标位置。支持(基于原生指针偏移,时间复杂度 O(1))。可通过'容器名 [下标]'或 at(下标) 直接访问元素,例如 vec[3] 能瞬间定位到第 4 个元素,无需遍历。
迭代器失效仅被删除节点的迭代器失效,其他迭代器(指向未删除节点的)仍有效。例如:删除链表中第 3 个节点后,指向第 2、4 个节点的迭代器可正常使用。插入元素时:若触发扩容(原有内存空间不足),所有迭代器、指针、引用均失效;若未触发扩容,仅插入位置之后的迭代器失效。删除元素时,删除位置之后的迭代器均失效。
内存利用率按需分配节点(每个节点存储数据 +2 个指针),无冗余空间,但存在'指针开销'(每个节点额外占用 2 个指针的内存)。内存分配分散,可能存在内存碎片。扩容时会预分配额外内存(通常为当前容量的 1.5 倍或 2 倍),可能产生冗余空间(例如容量为 10 但仅存储 5 个元素,剩余 5 个空间闲置)。内存分配连续,缓存命中率更高。

通过对比可见:

  • 若场景以 **频繁插入/删除(尤其是中间位置)** 为主,优先选择 List;
  • 若场景以 **频繁随机访问、尾端插入** 为主,优先选择 vector。
  • 补充: List 虽然封装了成员 sort(底层类似归并排序),但是效率是不如算法库里的,所以如果需要排序还是 vector 比较好。

二、模块实现:List 核心代码

按 **'节点→迭代器→容器类'** 的依赖顺序来逐步实现 List。

2.1 模块 1:链表节点(list_node)—— 容器的基本单元

节点是存储数据的载体,用模板类实现泛型支持,适配任意数据类型(如 int、string)。

2.1.1 代码实现(来自 list.h)
#pragma once
#include <iostream>
#include <list>
using namespace std;

namespace Lotso {
    // 链表节点结构:存储数据与双向指针
    template<class T>
    struct list_node {
        list_node<T>* _prev; // 前驱节点指针
        list_node<T>* _next; // 后继节点指针
        T _data;             // 节点数据

        // 构造函数:默认值初始化,指针置空
        list_node(const T& x = T()) : _prev(nullptr), _next(nullptr), _data(x) {}
    };
}
2.1.2 核心解析
  • 模板参数 T:支持任意数据类型,例如 Lotso::list<int> 存储整数,Lotso::list<string> 存储字符串,与 string 的泛型设计一致。
  • 默认构造参数:T() 确保内置类型(如 int)默认初始化为 0,自定义类型自动调用其默认构造函数,兼容性强。
  • 指针初始化:_prev 与 _next 初始化为 nullptr,避免野指针风险,后续由容器类统一管理指针链接。

2.2 模块 2:迭代器(list_iterator)—— 容器的导航工具

List 的迭代器不是原生指针而是封装 list_node* 的类,通过运算符重载模拟指针行为,同时用'三模板参数'复用普通 /const 迭代器。

2.2.1 代码实现(来自 list.h)
namespace Lotso {
    // 迭代器类:T-数据类型,Ref-引用类型,Ptr-指针类型
    template<class T, class Ref, class Ptr>
    struct list_iterator {
        using Self = list_iterator<T, Ref, Ptr>; // 简化自身类型名
        using Node = list_node<T>;               // 节点类型别名
        Node* _node;                             // 迭代器指向的节点指针

        // 迭代器构造:接收节点指针初始化
        list_iterator(Node* node) : _node(node) {}

        // 1. 解引用运算符:返回数据引用(普通迭代器可修改,const 不可)
        Ref operator*() { return _node->_data; }

        // 2. 箭头运算符:支持复杂类型成员访问(如 struct.field)
        Ptr operator->() { return &_node->_data; }

        // 3. 前置++:向后移动(指向后继节点)
        Self& operator++() {
            _node = _node->_next;
            return *this;
        }

        // 4. 后置++:先返回当前,再移动
        Self operator++(int) {
            Self tmp(*this);
            _node = _node->_next;
            return tmp;
        }

        // 5. 前置--:向前移动(指向前驱节点)
        Self& operator--() {
            _node = _node->_prev;
            return *this;
        }

        // 6. 后置--:先返回当前,再移动
        Self operator--(int) {
            Self tmp(*this);
            _node = _node->_prev;
            return tmp;
        }

        // 7. 相等判断:比较节点指针
        bool operator!=(const Self& s) const { return _node != s._node; }
        bool operator==(const Self& s) const { return _node == s._node; }
    };
}
2.2.2 核心解析
  • 三模板参数复用:参考 string 的 const 迭代器设计,Ref 为 T& 时是普通迭代器(可修改数据),为 const T& 时是 const 迭代器(只读);Ptr 同理,避免单独定义 const 迭代器的代码冗余。
  • 运算符重载:完全模拟指针行为,遍历(++/--)、访问数据(*/->)的用法与原生指针一致,无需改变使用习惯。
  • 无内存管理:迭代器仅作为'导航工具',不负责节点内存的创建与释放,避免与容器类的内存逻辑耦合。

2.3 模块 3:容器类(list)——List 功能的中枢

容器类整合节点与迭代器,提供构造、插入、删除、遍历等核心接口,底层通过调整指针实现高效的操作。

2.3.1 代码实现(来自 list.h)
namespace Lotso {
    template<class T>
    class list {
        using Node = list_node<T>; // 节点类型别名
    public:
        // 类型重定义:普通/const 迭代器(复用 list_iterator)
        using iterator = list_iterator<T, T&, T*>;
        using const_iterator = list_iterator<T, const T&, const T*>;

        // -------------------------- 迭代器接口 --------------------------
        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 empty_init() {
            _head = new Node;
            _head->_prev = _head;
            _head->_next = _head;
        }

        // 默认构造
        list() { empty_init(); }

        // 初始化列表构造(支持 {1,2,3} 形式)
        list(initializer_list<T> il) {
            empty_init();
            for (auto& e : il) push_back(e);
        }

        // 范围构造(支持 [first, last) 区间)
        template<class InputIterator>
        list(InputIterator first, InputIterator last) {
            empty_init();
            while (first != last) {
                push_back(*first);
                ++first;
            }
        }

        // 析构函数:释放所有节点,避免内存泄漏
        ~list() {
            clear();      // 先删除所有数据节点
            delete _head; // 再删除哨兵头节点
            _head = nullptr; // 置空指针,避免野指针
            _size = 0;
        }

        // -------------------------- 插入删除接口 --------------------------
        // 尾插:复用 insert,简化代码
        void push_back(const T& x) { insert(end(), x); }

        // 头插:复用 insert
        void push_front(const T& x) { insert(begin(), x); }

        // 尾删:复用 erase
        void pop_back() { erase(--end()); }

        // 头删:复用 erase
        void pop_front() { erase(begin()); }

        // 任意位置插入:调整指针实现 O(1) 插入
        void insert(iterator pos, const T& x) {
            Node* cur = pos._node;       // pos 指向的当前节点
            Node* prev = cur->_prev;     // 当前节点的前驱
            Node* newnode = new Node(x); // 新建数据节点

            // 调整指针:prev <-> newnode <-> cur
            newnode->_prev = prev;
            newnode->_next = cur;
            prev->_next = newnode;
            cur->_prev = newnode;
            ++_size; // 有效元素个数递增
        }

        // 任意位置删除:返回下一个有效迭代器,避免失效
        iterator erase(iterator pos) {
            Node* cur = pos._node;   // 待删除节点
            Node* prev = cur->_prev; // 前驱节点
            Node* next = cur->_next; // 后继节点

            // 调整指针:跳过 cur 节点,连接 prev 和 next
            prev->_next = next;
            next->_prev = prev;
            delete cur;              // 释放待删除节点内存
            --_size;                 // 有效元素个数递减
            return iterator(next);   // 返回下一个有效迭代器
        }

        // 清空容器:保留哨兵头节点,便于后续复用
        void clear() {
            iterator it = begin();
            while (it != end()) it = erase(it);
        }

        // -------------------------- 其他接口 --------------------------
        size_t size() const { return _size; }

    private:
        Node* _head;     // 哨兵头节点指针
        size_t _size = 0; // 有效数据节点个数
    };
}
2.3.2 核心解析
  • 空初始化 empty_init():参考 string 的 reserve 初始化逻辑,创建哨兵位头节点统一空链表和非空链表的操作逻辑,避免插入时还需额外判断是'否为头节点'。
  • 接口复用:push_back/push_front 复用 insert,pop_back/pop_front 复用 erase,减少代码冗余,与 string 的 += 复用 push_back 思路一致。
  • 迭代器失效处理:erase 返回下一个有效迭代器,用户可通过 it=erase(it) 更新迭代器,避免访问失效节点,解决 List 迭代器失效的核心痛点。
  • 内存管理:析构函数先 clear() 删除所有数据节点,再释放哨兵位头节点,确保无内存泄漏;clear() 进删除数据节点,保留头节点,便于容器后续复用。

三、功能测试:用 test.cpp 验证 List 正确性

参考测试用例 + 结果分析风格,用 test.cpp 代码,覆盖构造、遍历、插入、删除等核心场景,验证容器功能。

3.1 测试用例 1:基础构造与遍历 (push_back+ 迭代器/范围 for)

3.1.1 代码实现 (来自 test.cpp)
#define _CRT_SECURE_NO_WARNINGS 1
#include "list.h"
using namespace std;

void test_list1() {
    Lotso::list<int> lt; // 尾插 4 个元素
    lt.push_back(1);
    lt.push_back(2);
    lt.push_back(3);
    lt.push_back(4);

    // 1. 迭代器遍历
    Lotso::list<int>::iterator it = lt.begin();
    while (it != lt.end()) {
        cout << *it << " "; // 预期输出:1 2 3 4
        ++it;
    }
    cout << endl;

    // 2. 范围 for 遍历(依赖 begin() 和 end())
    for (auto e : lt) {
        cout << e << " "; // 预期输出:1 2 3 4
    }
    cout << "\n" << endl;
}

int main() {
    test_list1();
    return 0;
}
3.1.2 测试结构与分析

结论:默认构造、尾插及两种遍历方式均正常工作,迭代器 begin()/end() 逻辑正确。

3.2 测试用例 2:头插、头删、尾删

3.2.1 代码实现(来自 test.cpp)
void test_list2() {
    Lotso::list<int> lt;
    // 头插 2 个元素,尾插 2 个元素
    lt.push_front(-2);
    lt.push_front(-1);
    lt.push_back(1);
    lt.push_back(2);
    cout << "头插 + 尾插后:";
    for (auto e : lt) cout << e << " "; // 预期输出:-1 -2 1 2
    cout << endl;

    // 尾删 2 次,头删 2 次
    lt.pop_back();
    lt.pop_back();
    lt.pop_front();
    lt.pop_front();
    cout << "删除后 size:" << lt.size() << endl; // 预期输出:0
    cout << endl;
}

int main() {
    // test_list1();
    test_list2();
    return 0;
}
3.2.2 测试结果与分析

结论: 头插,头删,尾删功能正常,size() 接口能正确反映有效元素个数。

3.3 测试用例 3:const 迭代器与拷贝构造

3.3.1 代码实现(来自 test.cpp)
// 用 const 迭代器遍历的打印函数(验证只读特性)
void Print(const Lotso::list<int>& lt) {
    Lotso::list<int>::const_iterator it = lt.begin();
    while (it != lt.end()) {
        // *it = 10; // 编译报错:const 迭代器不可修改数据
        cout << *it << " ";
        ++it;
    }
    cout << endl;
}

void test_list3() {
    // 初始化列表构造
    Lotso::list<int> lt1 = { 1,2,3,4,5,6 };
    // 拷贝构造
    Lotso::list<int> lt2(lt1);
    cout << "lt2(拷贝 lt1):";
    for (auto e : lt2) cout << e << " "; // 预期输出:1 2 3 4 5 6
    cout << endl;

    // const 迭代器遍历
    const Lotso::list<int>& clt = lt1;
    cout << "const 迭代器遍历 lt1:";
    Print(clt); // 预期输出:1 2 3 4 5 6
    cout << endl;
}

int main() {
    // test_list1();
    // test_list2();
    test_list3();
    return 0;
}
3.3.2 测试结果与分析

结论: 拷贝构造实现深拷贝(修改 lt1 不影响 lt2),const 迭代器仅支持只读访问,符合设计预期。

3.4 测试用例 4:insert、erase 与 clear

3.4.1 代码实现(来自 test.cpp)
void test_list4() {
    Lotso::list<int> lt;
    lt.push_back(1);
    lt.push_back(2);
    lt.push_back(3);
    lt.push_back(4);

    // 任意位置插入:在第 2 个元素(2)前插入 100
    auto it = lt.begin();
    ++it;
    lt.insert(it, 100);
    cout << "插入 100 后:";
    for (auto e : lt) cout << e << " "; // 预期输出:1 100 2 3 4
    cout << endl;

    // 任意位置删除:删除 100
    it = lt.begin();
    ++it;
    it = lt.erase(it);
    cout << "删除 100 后:";
    for (auto e : lt) cout << e << " "; // 预期输出:1 2 3 4
    cout << endl;

    // 清空容器
    lt.clear();
    cout << "clear 后 size:" << lt.size() << endl; // 预期输出:0
}

int main() {
    // test_list1();
    // test_list2();
    // test_list3();
    test_list4();
    return 0;
}
3.4.2 测试结果与分析

结论: 任意位置插入 / 删除功能正常,clear() 能正确清空数据节点,erase 返回的迭代器有效。

目录

  1. C++ 手写 List 容器:双向链表原理及实现
  2. 前言
  3. 一、底层原理:List 容器的骨架——带头双向循环链表
  4. 1.1 结构组成与优势
  5. 1.2 核心特性(对比 vector)
  6. 二、模块实现:List 核心代码
  7. 2.1 模块 1:链表节点(list_node)—— 容器的基本单元
  8. 2.1.1 代码实现(来自 list.h)
  9. 2.1.2 核心解析
  10. 2.2 模块 2:迭代器(list_iterator)—— 容器的导航工具
  11. 2.2.1 代码实现(来自 list.h)
  12. 2.2.2 核心解析
  13. 2.3 模块 3:容器类(list)——List 功能的中枢
  14. 2.3.1 代码实现(来自 list.h)
  15. 2.3.2 核心解析
  16. 三、功能测试:用 test.cpp 验证 List 正确性
  17. 3.1 测试用例 1:基础构造与遍历 (push_back+ 迭代器/范围 for)
  18. 3.1.1 代码实现 (来自 test.cpp)
  19. 3.1.2 测试结构与分析
  20. 3.2 测试用例 2:头插、头删、尾删
  21. 3.2.1 代码实现(来自 test.cpp)
  22. 3.2.2 测试结果与分析
  23. 3.3 测试用例 3:const 迭代器与拷贝构造
  24. 3.3.1 代码实现(来自 test.cpp)
  25. 3.3.2 测试结果与分析
  26. 3.4 测试用例 4:insert、erase 与 clear
  27. 3.4.1 代码实现(来自 test.cpp)
  28. 3.4.2 测试结果与分析
  • 💰 8折买阿里云服务器限时8折了解详情
  • 💰 8折买阿里云服务器限时8折购买
  • 🦞 5分钟部署阿里云小龙虾了解详情
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

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

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

更多推荐文章

查看全部
  • Python 技术实战:爬虫、数据分析与自动化应用指南
  • Moondream2 本地工具优化 Stable Diffusion 提示词案例对比
  • Easylogger 开源日志库配置解析与 STM32 移植指南
  • 常用国内 Python 镜像站地址及 pip 配置指南
  • 通义万相 2.1 核心功能解析与部署实践
  • Web Worker 多线程编程详解与实战
  • Docker 部署 music-tag-web 音乐标签编辑器
  • 动态规划时间复杂度和空间复杂度计算方法
  • C++条件判断、循环与数组详解
  • Qwen3-4B-Instruct 模型快速部署与 AI 写作应用指南
  • PDFShuffler:简单高效的 PDF 页面管理工具
  • 大模型、Sora 与世界模型的关系及对自动驾驶的意义
  • 基于 SpringBoot 的乡镇居民诊疗信息系统设计与实现
  • C++ STL String 类模拟实现详解
  • DeerFlow 2.0:基于 LangGraph 的开源智能体编排框架
  • Rust 集合类型与迭代器详解
  • 苹果M系列芯片安装Vivado进行FPGA开发
  • 前端动画最佳实践:替代 jQuery animate 的现代方案
  • AI 数字助理评测:ToClaw 如何超越纯聊天工具
  • Cookie 与 Session:Web 用户状态管理详解

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如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