news 2026/11/4 13:32:30

C++中list容器的实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++中list容器的实现

前言


std::list是 C++ 标准库的双向链表容器。它的接口看起来和std::vector相似,但内部结构完全不同:元素分散在独立结点里,每个结点保存前驱和后继指针,插入删除不搬移其他元素,代价是不能随机访问。很多教程只列接口,不讲实现,读者遇到“迭代器为什么只能双向移动”“为什么erase返回下一个迭代器”“为什么空链表的end()还能减一”时,就缺少一个具体的模型。


本文从头实现一个简化版的List,重点讲三件事:带哨兵结点的循环双向链表、迭代器如何把结点指针包装成begin()/end()语义、插入删除时指针如何更新。实现以 C++17 为准,只依赖标准库的头文件和类型标签,不涉及分配器、异常安全细节和const_iterator。它不是std::list的替代品,而是理解标准库设计的教学模型。


先说明一个常见误解:std::list的end()不是空指针,而是一个真实的哨兵结点。正因如此,--end()才能得到最后一个元素,++到尾结点后也能自然回到end()。如果自己用nullptr表示结尾,这些操作都要特判,迭代器很难写一致。


一、结点、哨兵与所有权


链表由结点组成。每个结点需要:



  • value:元素;

  • prev:前一个结点;

  • next:后一个结点。


为了支持end(),我们再引入一个哨兵结点(sentinel)。哨兵不存元素,它的next指向第一个元素,prev指向最后一个元素;空链表时,哨兵的next和prev都指向自己。整条链因此是一个环:


sentinel <-> first <-> ... <-> last <-> sentinel

这样做的好处是插入删除不需要特判头尾。在end()前插入,就是“在哨兵前插入”,等价于追加到尾部;删除第一个元素,就是删除sentinel.next。所有位置的操作都变成同一套“改四个指针”的流程。


哨兵如果直接定义成Node,会要求T可默认构造,因为哨兵里也有一个T value成员。标准库的常见做法是把“链接指针”和“值”分开:定义一个只含prev/next的基类,真正的结点继承它并加上value。本文采用这个结构:


struct NodeBase
{
NodeBase* prev;
NodeBase* next;
};

struct Node : NodeBase
{
T value;
};

哨兵是NodeBase,迭代器持有NodeBase*,解引用时再static_cast<Node*>。只要保证不对哨兵解引用,这个向下转型就是安全的。


二、迭代器:把结点指针包装成双向迭代器


迭代器的本质是一个指针的包装。它需要提供:



  • operator*返回元素引用;

  • operator->返回元素指针;

  • 前置/后置++、--;

  • ==、!=;

  • 标准迭代器所需的类型别名,例如iterator_category、value_type、difference_type。


begin()返回iterator(sentinel_.next),end()返回iterator(&sentinel_)。对最后一个元素执行++,p_变成sentinel_.next的下一结点,也就是哨兵,于是恰好等于end();对end()执行--,p_变成哨兵的prev,也就是最后一个元素。这套对称性是指针链表很难做到的。


迭代器不拥有结点,只是“位置”。插入和删除会让某些迭代器失效:erase使被删元素的迭代器失效,其他迭代器不受影响;insert不使任何已有迭代器失效。这一点和std::vector的“扩容全失效”不同。


三、插入与删除


在位置 pos 前插入,需要拿到 pos 的前驱:



  1. 新建结点,prev指向前驱,next指向 pos;

  2. 前驱的next指向新结点;

  3. pos 的prev指向新结点;

  4. 返回指向新结点的迭代器。


顺序可以调整,但必须先保证新结点的两个指针已经就绪,再让旧结点指向它;否则链上会出现指向未初始化结点的瞬间(单线程下不可见,但异常安全和大对象构造时要考虑)。


删除 pos:



  1. 保存next,用于返回;

  2. pos->prev->next = pos->next;

  3. pos->next->prev = pos->prev;

  4. delete结点;

  5. 返回iterator(next)。


因为有哨兵,pos 是第一个或最后一个元素时也不需要特判。唯一要避免的是删除end()(哨兵本身),那是未定义行为;本文的实现遇到end()直接返回,属于防御性处理,标准库并不保证。


四、完整实现


// 适用于 C++17
#include <cstddef>
#include <iterator>
#include <iostream>

template <typename T>
class List
{
private:
struct NodeBase
{
NodeBase* prev;
NodeBase* next;
};

struct Node : NodeBase
{
T value;

Node(const T& v, NodeBase* p, NodeBase* n)
: NodeBase{p, n}, value(v)
{
}
};

public:
class iterator
{
public:
using iterator_category = std::bidirectional_iterator_tag;
using value_type = T;
using difference_type = std::ptrdiff_t;
using pointer = T*;
using reference = T&;

iterator() = default;
explicit iterator(NodeBase* p) : p_(p) {}

T& operator*() const
{
return static_cast<Node*>(p_)->value;
}

T* operator->() const
{
return &static_cast<Node*>(p_)->value;
}

iterator& operator++()
{
p_ = p_->next;
return *this;
}

iterator operator++(int)
{
iterator old(*this);
++(*this);
return old;
}

iterator& operator--()
{
p_ = p_->prev;
return *this;
}

iterator operator--(int)
{
iterator old(*this);
--(*this);
return old;
}

bool operator==(const iterator& other) const { return p_ == other.p_; }
bool operator!=(const iterator& other) const { return p_ != other.p_; }

private:
friend class List;
NodeBase* p_ = nullptr;
};

List() noexcept
{
sentinel_.prev = &sentinel_;
sentinel_.next = &sentinel_;
}

~List()
{
clear();
}

List(const List&) = delete;
List& operator=(const List&) = delete;

iterator begin() noexcept { return iterator(sentinel_.next); }
iterator end() noexcept { return iterator(&sentinel_); }

bool empty() const noexcept { return sentinel_.next == &sentinel_; }
std::size_t size() const noexcept { return size_; }

void push_back(const T& value)
{
insert(end(), value);
}

void push_front(const T& value)
{
insert(begin(), value);
}

iterator insert(iterator pos, const T& value)
{
NodeBase* current = pos.p_;
NodeBase* before = current->prev;

Node* node = new Node(value, before, current);

before->next = node;
current->prev = node;

++size_;
return iterator(node);
}

iterator erase(iterator pos)
{
NodeBase* current = pos.p_;

if (current == &sentinel_)
return end();

NodeBase* next = current->next;

current->prev->next = current->next;
current->next->prev = current->prev;

delete static_cast<Node*>(current);
--size_;

return iterator(next);
}

void clear()
{
NodeBase* p = sentinel_.next;

while (p != &sentinel_)
{
NodeBase* next = p->next;
delete static_cast<Node*>(p);
p = next;
}

sentinel_.prev = &sentinel_;
sentinel_.next = &sentinel_;
size_ = 0;
}

private:
NodeBase sentinel_;
std::size_t size_ = 0;
};

int main()
{
List<int> values;

values.push_back(1);
values.push_back(2);
values.push_front(0);

for (int v : values)
std::cout << v << ' ';
std::cout << '\n';

auto it = values.begin();
++it;
values.erase(it); // 删除 1

for (int v : values)
std::cout << v << ' ';
std::cout << '\n';

return 0;
}

这段代码有几个值得核对的细节。


哨兵的初始化:构造函数把sentinel_.prev和sentinel_.next都指向自己,形成空环。empty()判断sentinel_.next == &sentinel_;如果忘记初始化,begin()会返回野指针,遍历直接崩溃。


迭代器的向下转型:static_cast<Node*>(p_)只在p_确实指向Node时有效。end()的p_指向NodeBase哨兵,不能解引用;标准库同样规定对end()解引用是未定义行为。insert和erase返回的迭代器都指向真实结点或end(),不会把哨兵当元素。


插入的四条赋值:before->next = node; current->prev = node;与Node构造函数里的prev/next初始化配合,保证新结点两侧都连通。size_在链接完成后自增。如果new抛异常,链表尚未修改,仍保持一致。


删除的返回值:erase返回next,调用方可以继续遍历而不必重新begin()。这是std::list的接口约定,也是“边遍历边删除”能写成it = list.erase(it)的原因。删除后current已释放,不能再访问它的任何成员。


析构与 clear:先保存next再delete,因为delete之后p->next不可读。清空后把哨兵重新指向自己,size_归零,对象可以继续使用。


拷贝被删除:List拥有所有结点,默认拷贝会复制哨兵指针和size_,两个对象析构同一批结点。需要拷贝时应实现深拷贝:遍历源链表,对每个元素调用push_back;需要移动时,把源对象的哨兵重新指向自己,并接管其结点。


五、和 std::list 的差距




特性本文实现std::list



双向迭代器支持支持

哨兵支持支持

const_iterator未实现支持

拷贝/移动删除拷贝,未实现移动完整支持

分配器未使用支持自定义分配器

emplace/insert重载仅const T&完整转发与就地构造

异常安全基本保证标准库实现保证

splice/merge/sort未实现支持



这些差距不是“实现得不够好”,而是教学模型有意裁剪。真正要补的第一项是const_iterator:它需要一套只读的迭代器类型,并让begin()/end()在 const 对象上返回它。第二项是移动语义:移动构造可以把源对象的结点整体接过来,只需重新指向哨兵,复杂度 O(1)。第三项是emplace_back:把参数完美转发到结点构造,避免先构造临时对象再拷贝。


常见坑点



  1. ❌ 构造函数忘记把哨兵的prev/next指向自己,空链表遍历进入野指针。


✅ 构造时立即形成自环;empty()用sentinel_.next == &sentinel_判断。



  1. ❌ 对end()解引用,或对空链表的begin()解引用。


✅end()只是位置标记,不解引用;空链表遍历前先判断begin() != end()。



  1. ❌ 析构或clear时先delete p再读p->next。


✅ 先保存NodeBase* next = p->next;,再删除,最后移动p。



  1. ❌ 插入时先改旧结点的指针,再构造新结点,异常时链表断裂。


✅ 先完成新结点的构造和初始化,再修改旧结点的链接;或保证构造不抛异常后再链接。



  1. ❌erase返回void或返回错误的迭代器,导致边遍历边删除无法写。


✅ 返回被删元素的后继迭代器;调用方用it = list.erase(it)继续。



  1. ❌ 删除结点时只改prev->next,忘记next->prev。


✅ 双向链表的两侧都要更新;有哨兵时头尾也不需要特判。



  1. ❌ 让List可拷贝但使用默认拷贝,两个对象共享结点。


✅ 删除拷贝,或实现深拷贝;析构重复释放同一结点是未定义行为。



  1. ❌ 认为insert会让所有迭代器失效。


✅list的插入不使已有迭代器失效;只有被删除元素的迭代器失效,其他位置不受影响。


总结




组件作用关键实现



NodeBase只含前后指针让哨兵不必构造T

Node指针 + 元素继承NodeBase

哨兵表示end(),连接首尾空表时自环

iterator包装结点指针++/--沿环移动

insert在 pos 前插入改前驱 next 与 pos prev

erase删除 pos两侧互连,返回后继

clear释放全部结点先存 next 再 delete



实现list的关键不是记住多少个指针,而是理解哨兵如何把“头、尾、空表”三种特殊情况统一成一种情况。一旦整条链是一个环,插入删除就只剩“在某个位置前接入/摘除一个结点”这一个动作,迭代器的begin/end/--end也都自然成立。理解这一点,再去看std::list的接口,就不会觉得那些看似奇怪的约定是随意设计的。




版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/4 13:30:50

.NET桌面应用自动更新方案对比与Squirrel实战

1. 为什么桌面应用需要自动更新机制 在Windows平台开发.NET桌面应用程序时&#xff0c;版本更新一直是个令人头疼的问题。传统方式需要用户手动下载安装包&#xff0c;不仅用户体验差&#xff0c;还经常因为用户长期不更新导致各种兼容性问题。我们团队维护的一个WPF医疗影像处…

作者头像 李华
网站建设 2026/11/4 13:28:11

VB6.0 SP6补丁:COM互操作稳定性加固与Windows兼容性关键修复

简介&#xff1a;本资源为Visual Basic 6.0官方最终服务包SP6完整安装补丁&#xff0c;专为仍在维护或迁移遗留VB6项目的开发者、企业IT支持人员及高校教学实践者提供。它解决VB6.0 RTM版本长期存在的稳定性缺陷、安全漏洞与新系统兼容性问题&#xff0c;尤其适用于需在Windows…

作者头像 李华
网站建设 2026/11/4 13:26:41

SAP库存调拨原理与实战:311/301/303/305移动类型本质解析

简介&#xff1a;本资源是一份面向SAP MM模块初学者与实施顾问的实务操作指南&#xff0c;聚焦库存调拨与发货核心流程&#xff0c;解决企业在跨库存地、跨工厂场景下库存移动不规范、在途库存难追踪、后台配置易出错等典型问题。文档系统梳理MB1B事务代码下的五类移动类型&…

作者头像 李华
网站建设 2026/11/4 13:24:30

深度优先搜索实现图的克隆与哈希表应用

1. 克隆图问题解析与深度优先搜索实现 在算法面试中&#xff0c;图相关的问题一直是考察重点&#xff0c;而图的克隆问题更是经典中的经典。今天我们就来深入剖析LeetCode第133题"克隆图"&#xff0c;这道题不仅考察对图数据结构的理解&#xff0c;更是检验递归思维和…

作者头像 李华
网站建设 2026/11/4 13:24:10

Kimi的“月之暗面”:从长文本聊天到AI编程与Agent生态补全

最近不管是技术群还是职场群&#xff0c;画风都出奇一致&#xff1a;有人晒出Kimi网页版的排队截图&#xff0c;有人在问Kimi兑换码到底怎么领&#xff0c;还有人已经开始折腾Kimi Code和Kimi Work。热搜词“和Kimi聊天的人太多了”几乎成了这一阶段的真实写照。与此同时&#…

作者头像 李华
网站建设 2026/11/4 13:23:37

AI建站工具如何实现中小企业数字化经营闭环

1. 项目概述&#xff1a;这不是又一个“AI建站工具”&#xff0c;而是中小企业数字化落地的第一块真实垫脚石“阿里云万小智3.0版本AI建站&#xff1a;新用户赠2000灵感值&#xff0c;从建站到经营&#xff0c;AI驱动中小企业数字化升级”——这个标题里藏着三个被多数人忽略的…

作者头像 李华