跳到主要内容
C++ 手写 List 容器:双向链表原理与实现细节 | 极客日志
C++ 算法
C++ 手写 List 容器:双向链表原理与实现细节 基于带头双向循环链表结构实现 C++ List 容器,重点讲解哨兵节点简化边界逻辑、迭代器运算符重载及内存管理策略。对比 Vector 分析随机访问与插入删除的效率差异,演示如何通过返回有效迭代器解决失效问题。代码覆盖节点定义、核心接口及测试用例,验证构造、遍历、增删改查功能,助开发者深入理解 STL 容器底层机制。
全栈工匠 发布于 2026/3/29 更新于 2026/4/25 1 浏览C++ 手写 List 容器:双向链表原理与实现细节
日常开发中,我们频繁调用 std::list 的接口,却常忽略其底层逻辑。面试被要求手写 List 时卡壳、开发中因迭代器失效导致程序崩溃,根源都在对底层结构的不理解。今天我们从双向循环链表入手,一步步实现一个完整的 List 容器。
一、底层原理:带头双向循环链表
要手写 List,先明确其底层结构——带头双向循环链表 。这是所有接口高效实现的基础。
1.1 结构组成与优势
结构部分 功能说明 哨兵头节点 不存储有效数据,仅作为操作锚点,统一空/非空链表的插入、删除逻辑,无需额外判断边界。例如:尾插时无需检查'是否为第一个节点',直接通过头节点的前驱指针定位尾节点,简化代码逻辑。 数据节点 每个节点含 _prev(前驱指针)、_next(后继指针)、_data(数据域),支持双向遍历。既可以从当前节点向前追溯前驱节点,也能向后访问后继节点,为迭代器的 ++/-- 操作提供底层支持。 循环特性 尾节点 _next 指向头节点,头节点 _prev 指向尾节点,形成闭环。例如:遍历到尾节点后,通过 _next 可直接回到头节点;获取尾节点无需遍历整个链表,只需访问 _head->_prev,提升尾操作效率。
通过这种结构设计,List 容器实现了任意位置插入/删除 O(1) 效率 、遍历逻辑统一 、边界处理简化 三大核心优势,是区别于 vector 动态数组的关键设计。
1.2 核心特性对比
特性 List(双向链表) Vector(动态数组) 插入删除效率 任意位置 O(1) (仅需调整节点前驱/后继指针,无需移动其他元素)。 中间位置 O(N) (插入/删除后需搬移后续所有元素);尾端操作(无扩容时)接近 O(1)。 随机访问 不支持(需从表头/表尾遍历,时间复杂度 O(N))。 支持(基于原生指针偏移,时间复杂度 O(1))。 迭代器失效 仅被删除节点的迭代器 失效,其他迭代器仍有效。 插入元素时:若触发扩容,所有迭代器、指针、引用均失效 ;删除元素时,删除位置之后的迭代器均失效 。 内存利用率 按需分配节点,存在'指针开销',内存分配分散。 扩容时会预分配额外内存,可能产生冗余空间,内存分配连续,缓存命中率更高。
选型建议:
若场景以 频繁插入/删除(尤其是中间位置) 为主,优先选择 List;
若场景以 频繁随机访问、尾端插入 为主,优先选择 Vector。
注:List 虽然封装了成员 sort(底层类似归并排序),但是效率是不如算法库里的,所以如果需要排序还是 vector 比较好。
二、模块实现:MyList 命名空间下的 List 核心代码
我们按 节点→迭代器→容器类 的依赖顺序来逐步实现 List。
2.1 模块 1:链表节点(list_node) 节点是存储数据的载体,用模板类实现泛型支持,适配任意数据类型。
#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) {}
};
}
模板参数 T :支持任意数据类型,例如 MyList::list<int> 存储整数,MyList::list<string> 存储字符串。
默认构造参数 :T() 确保内置类型(如 int)默认初始化为 0,自定义类型自动调用其默认构造函数,兼容性强。
指针初始化 :_prev 与 _next 初始化为 nullptr,避免野指针风险,后续由容器类统一管理指针链接。
2.2 模块 2:迭代器(list_iterator) List 的迭代器不是原生指针而是封装 list_node* 的类,通过运算符重载模拟指针行为,同时用'三模板参数'复用普通 /const 迭代器。
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; }
};
}
三模板参数复用 :Ref 为 T& 时是普通迭代器(可修改数据),为 const T& 时是 const 迭代器(只读);Ptr 同理,避免单独定义 const 迭代器的代码冗余。
运算符重载 :完全模拟指针行为,遍历(++/--)、访问数据(*/->)的用法与原生指针一致,无需改变使用习惯。
无内存管理 :迭代器仅作为'导航工具',不负责节点内存的创建与释放,避免与容器类的内存逻辑耦合。
2.3 模块 3:容器类(list) 容器类整合节点与迭代器,提供构造、插入、删除、遍历等核心接口,底层通过调整指针实现高效的操作。
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 ;
};
}
空初始化 empty_init() :创建哨兵位头节点统一空链表和非空链表的操作逻辑,避免插入时还需额外判断是否为首节点。
接口复用 :push_back/push_front 复用 insert,pop_back/pop_front 复用 erase,减少代码冗余。
迭代器失效处理 :erase 返回下一个有效迭代器,用户可通过 it=erase(it) 更新迭代器,避免访问失效节点,解决 List 迭代器失效的核心痛点。
内存管理 :析构函数先 clear() 删除所有数据节点,再释放哨兵位头节点,确保无内存泄漏;clear() 进删除数据节点,保留头节点,便于容器后续复用。
三、功能测试:验证 List 正确性 我们用测试用例覆盖构造、遍历、插入、删除等核心场景,验证容器功能。
3.1 测试用例 1:基础构造与遍历 #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 ;
}
结论: 默认构造、尾插及两种遍历方式均正常工作,迭代器 begin()/end() 逻辑正确。
3.2 测试用例 2:头插、头删、尾删 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 << " " ;
cout << endl;
lt.pop_back ();
lt.pop_back ();
lt.pop_front ();
lt.pop_front ();
cout << "删除后 size:" << lt.size () << endl;
cout << endl;
}
int main () {
test_list2 ();
return 0 ;
}
结论: 头插,头删,尾删功能正常,size() 接口能正确反映有效元素个数。
3.3 测试用例 3:const 迭代器与拷贝构造
void Print (const MyList::list<int >& lt) {
MyList::list<int >::const_iterator it = lt.begin ();
while (it != lt.end ()) {
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 << " " ;
cout << endl;
const MyList::list<int >& clt = lt1;
cout << "const 迭代器遍历 lt1:" ;
Print (clt);
cout << endl;
}
int main () {
test_list3 ();
return 0 ;
}
结论: 拷贝构造实现深拷贝(修改 lt1 不影响 lt2),const 迭代器仅支持只读访问,符合设计预期。
3.4 测试用例 4:insert、erase 与 clear 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 << " " ;
cout << endl;
it = lt.begin ();
++it;
it = lt.erase (it);
cout << "删除 100 后:" ;
for (auto e : lt) cout << e << " " ;
cout << endl;
lt.clear ();
cout << "clear 后 size:" << lt.size () << endl;
}
int main () {
test_list4 ();
return 0 ;
}
结论: 任意位置插入 / 删除功能正常,clear() 能正确清空数据节点,erase 返回的迭代器有效。
结尾 至此,我们完成了从双向循环链表原理到 C++ List 容器的实战落地。从哨兵节点简化边界逻辑,到双向指针支撑迭代器操作,每一步都印证了'底层结构决定容器特性'——正是链表的设计,让 List 在频繁插入删除场景中具备 O(1) 效率。手写 List 的核心价值,在于跳出 STL 黑盒,真正吃透迭代器失效、内存分配等细节,夯实指针操作与容器设计思维。
相关免费在线工具 加密/解密文本 使用加密算法(如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