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

从双向链表到 List 容器:C++ 手写笔记

通过手写 C++ 中的 list 容器,基于带头双向循环链表实现了节点定义、迭代器封装、插入删除等操作,并验证了迭代器失效处理和哨兵节点简化边界逻辑,加深对 STL list 底层机制的理解。

全栈工匠发布于 2026/6/1更新于 2026/10/2568 浏览
从双向链表到 List 容器:C++ 手写笔记

说到 C++ 的 list,大部分人都知道它底层是双向链表。可真问起来,哨兵节点怎么工作、迭代器失效的坑在哪、指针操作细节,可能马上就会卡壳。面试时手写 List,更是不少人的翻车现场。

我干脆自己写了一个,把一些常被忽略的细节理清楚。

底层结构:带头双向循环链表

std::list 的底层是带头节点的双向循环链表。用一个不存数据的哨兵节点作为锚点,所有插入、删除逻辑都能统一,不用额外判断是不是空链表,也不用单独处理第一个节点或最后一个节点的边界。

节点包含三个部分:前驱指针 _prev、后继指针 _next 和数据域 _data。尾结点的 _next 指向哨兵节点,哨兵节点的 _prev 指向尾结点,形成一个环。这带来的好处是:找尾结点直接 _head->_prev,不用遍历;遍历到尾结点之后,自然又回到哨兵。

和 vector 比,List 的强项是中间位置的插入/删除 O(1),代价是没有随机访问,迭代器更易失效(但只失效被删除的那个),而且每个节点都有指针开销,内存分配分散。

别被 list::sort 迷惑,它的排序效率不比算法库的通用排序,如果要对元素排序,先把数据倒进 vector 再排往往是更好的选择。

实现细节

按节点 → 迭代器 → 容器类的依赖顺序来写,代码都在命名空间 MyList 下。

节点

模板类 list_node 只负责存数据和前后指针,构造函数让指针默认置空,数据用 T() 初始化,这样无论是 int(初始化为 0)还是自定义类型都能正常工作。

#pragma once
#include <iostream>
#include <list>
using namespace std;

namespace MyList {
    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) {}
    };
}

迭代器

List 的迭代器不能是原生指针,因为节点的物理存储不连续。这里用 list_iterator 包装一个 list_node<T>*,通过运算符重载模拟指针行为。

用三个模板参数 T, Ref, Ptr 实现普通 / const 迭代器的复用:Ref 为 T& 时是可读写的迭代器,为 时是只读的; 同理。这样一份代码就能搞定两种迭代器。

const T&
Ptr
namespace MyList {
    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) {}

        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& s) const { return _node != s._node; }
        bool operator==(const Self& s) const { return _node == s._node; }
    };
}

注意迭代器只负责'导航',不管节点内存的分配和释放,那都是容器的事。

容器类

list 类维护哨兵头节点 _head 和有效元素个数 _size。先写一个 empty_init(),创建哨兵节点并让它的前驱和后继都指向自己,形成自环。空链表和非空链表的插入/删除逻辑就统一了。

插入时,只需要调整四个指针:前驱的 _next、后继的 _prev、新节点的前后指针;删除时同理,调整前后节点跳过被删节点,然后释放内存。push_back/push_front 直接复用 insert,pop_back/pop_front 复用 erase,省去重复代码。

erase 返回下一个有效迭代器,这样使用者可以用 it = lst.erase(it) 安全更新迭代器,避免失效问题。

namespace MyList {
    template<class T>
    class list {
        using Node = list_node<T>;
    public:
        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(); }

        list(initializer_list<T> il) {
            empty_init();
            for (auto& e : il) push_back(e);
        }

        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;
        }

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

        void insert(iterator pos, const T& x) {
            Node* cur = pos._node;
            Node* prev = cur->_prev;
            Node* newnode = new Node(x);

            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;

            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;
    };
}

测试验证

写几个用例,覆盖构造、插入、删除、迭代器访问和 const 场景。

基础构造 + 遍历:默认构造,尾插四个数,用迭代器和范围 for 输出。

#define _CRT_SECURE_NO_WARNINGS 1
#include "list.h"
using namespace std;

void test_list1() {
    MyList::list<int> lt;
    lt.push_back(1); lt.push_back(2); lt.push_back(3); lt.push_back(4);

    MyList::list<int>::iterator it = lt.begin();
    while (it != lt.end()) { cout << *it << " "; ++it; }
    cout << endl;

    for (auto e : lt) cout << e << " ";
    cout << "\n" << endl;
}

int main() {
    test_list1();
    return 0;
}

头插、头删、尾删:插入顺序和预期一致,size() 能反映变化。

void test_list2() {
    MyList::list<int> lt;
    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;

    lt.pop_back(); lt.pop_back();
    lt.pop_front(); lt.pop_front();
    cout << "删除后 size:" << lt.size() << endl; // 0
    cout << endl;
}

int main() {
    test_list2();
    return 0;
}

const 迭代器和拷贝构造:拷贝后修改原容器不影响副本,const 版本只读,尝试修改会编译报错。

void Print(const MyList::list<int>& lt) {
    MyList::list<int>::const_iterator it = lt.begin();
    while (it != lt.end()) {
        // *it = 10; // 编译报错
        cout << *it << " ";
        ++it;
    }
    cout << endl;
}

void test_list3() {
    MyList::list<int> lt1 = { 1,2,3,4,5,6 };
    MyList::list<int> lt2(lt1);
    cout << "lt2(拷贝 lt1):";
    for (auto e : lt2) cout << e << " "; // 1 2 3 4 5 6
    cout << endl;

    const MyList::list<int>& clt = lt1;
    cout << "const 迭代器遍历 lt1:";
    Print(clt);
    cout << endl;
}

int main() {
    test_list3();
    return 0;
}

insert / erase / clear:在中间位置插入,删除后迭代器有效,clear() 后 size 归零。

void test_list4() {
    MyList::list<int> lt;
    lt.push_back(1); lt.push_back(2); lt.push_back(3); lt.push_back(4);

    auto it = lt.begin();
    ++it;
    lt.insert(it, 100);
    cout << "插入 100 后:";
    for (auto e : lt) cout << e << " "; // 1 100 2 3 4
    cout << endl;

    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_list4();
    return 0;
}

跑通这几个用例,基本功能就算验证完了。

从头到尾写一遍 List,对哨兵节点的妙用、指针操作如何保证 O(1)、迭代器失效的边界,都会比光看文档深刻得多。以后再用 std::list,遇到奇怪的问题,至少知道该往哪个方向排查。

目录

  1. 底层结构:带头双向循环链表
  2. 实现细节
  3. 节点
  4. 迭代器
  5. 容器类
  6. 测试验证

更多推荐文章

查看全部
  • 滑动窗口实战:长度最小的子数组与无重复字符的最长子串
  • C++ 笔试算法实战:排序子序列与贪心策略详解
  • AI Agent 技能机制 Skills:概念原理与实操指南
  • 亚马逊户外帐篷卖家真实吐槽:以前一张主图要等7天、烧3000块,现在用 Megick 5分钟出4K大片,月销量直接翻了2.8倍
  • 企业级私有化 AI 部署:大模型原理与本地智聊机器人实战
  • OpenClaw 架构解析:实现从认知到行动的 AI 智能体
  • 本地部署 Stable Diffusion 环境配置与避坑指南
  • Stable Diffusion 3.5 FP8 镜像部署与商业授权详解
  • C++ 基础实战:从循环控制到算法入门
  • WebGIS 实战:WKT 转 GeoJSON 技巧与 Leaflet 集成
  • AI 零基础入门:从概念到实践的完整指南
  • C++ 继承机制详解:从基础到多继承与组合
  • 企业微信外部群机器人消息推送实现指南
  • DeepSeek 使用指南与高阶提示词技巧
  • 自然语言处理:高级应用、前沿模型与实战开发
  • Python 量化实战:AKshare 获取全市场金融数据
  • Stable Diffusion v1-5-pruned.safetensors本地部署指南
  • 飞书 OpenClaw 机器人 HTTP 401 认证失败排查与解决方案
  • Python 与 CV 大模型下的滑块与点选验证码处理思路
  • SSE 在前端流式输出中的实现与取舍

相关免费在线工具

  • 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

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online