C++标准模板库(STL)------list的模拟实现
在C++ 标准模板库(STL)的容器中,list 是一个极具代表性的 "双向循环链表" 实现,在任意位置的插入 /删除操作场景下有着无可替代的效率优势 。你是否好奇过list的底层原理呢?本篇文章将从底层节点结构、迭代器的封装,到容器类的完整实现,一步步讲解list的设计逻辑
文章目录
-
- [1、 list双向链表的节点](#1、 list双向链表的节点)
- [2、 list迭代器的实现](#2、 list迭代器的实现)
-
- [通用迭代器设计(兼容普通迭代器和 const 迭代器)](#通用迭代器设计(兼容普通迭代器和 const 迭代器))
- [3、 list类的实现](#3、 list类的实现)
-
- 1)list类的私有成员
- 2)迭代器类型的定义(在list类中)
- 3)初始化与构造函数
- [4) 赋值运算符重载](#4) 赋值运算符重载)
- 5)析构函数
- 6)插入与删除操作
- 7)其他常用接口
- 4、测试
- 5、完整代码
1、 list双向链表的节点
list的底层是带头双向循环链表 ,链表的所有数据都储存在一个个独立的节点中,节点通过指针串联形成完整的链表。
list由一个哨兵头节点和若干有效数据节点组成,哨兵头节点不储存数据
其他节点储存一个数值(_data),以及指向前后元素的指针(_prev _next) ,因此我们要先定义节点的结构体list_node

cpp
//创建LI命名空间,让自己模拟实现的list与std::list隔离
namespace LI
{
//创建类模板,接收不同类型的数据(内置类型、自定义类型)
//用struct结构体,因为struct默认成员是公有的,方便list容器直接访问
template<class T>
struct list_node
{
T _data;//节点储存的数据
list_node<T>* _next;//指向后继节点的指针
list_node<T>* _prev;//指向前驱节点的指针
//构造函数,传引用避免拷贝
list_node(const T& x = T())
:_data(x)
,_next(nullptr)
,_prev(nullptr)
{}
};
}
- 模板参数T,支持任意数据类型(内置类型如 int,自定义类型如 string)
- 双向指针_next和_prev,保证了list双向遍历的能力,为后续迭代器的++、- -提供基础
- 默认构造的参数x = T(),支持无参构造。如果T是内置类型,T()是"零值",比如int()是0,double()是0.0,char()是'\0'。如果T是自定义类型,T()会调用默认构造函数创建对象
2、 list迭代器的实现
在类(或结构体)中, . 和 -> 本质都是访问成员的运算符
• 对象.成员 :左边是类 / 结构体的实例对象(或引用) ,直接访问其成员 ;
• 指针->成员 :左边是指向类 / 结构体的指针,通过指针间接访问目标对象的成员
指针->成员 本质是 (*指针).成员 的简化写法,两者完全等价,但 -> 更简洁(尤其嵌套场景)
- list的迭代器不是单纯的原生指针,而是节点指针的封装 。为了统一迭代器的使用,我们将 "链表节点指针" 包装成 "像指针一样用" 的对象 ------ 支持*取值、->访问成员、++/--遍历
- 为什么需要迭代器封装?
链表的底层并不是连续的空间,而是一个个分散的节点 ,不能直接通过指针++跳转到下一个节点。需要通过迭代器对象,将节点指针的操作(_node->next)封装为统一的operator++ - 为什么要使用三个模板参数?
模板参数 template<class T, class Ref, class Ptr>,目的是 一份代码复用,同时实现普通迭代器和 const 迭代器,不用写两份重复代码
如果我们要实现 list 的普通迭代器和 const 迭代器,不用模板复用的话,需要写两份几乎一样的代码,非常的冗余
cpp
//普通版本
template<class T>
struct __list_iterator
{
typedef list_node<T> Node;
Node* _node;
__list_iterator(Node* node)
:_node(node)
{}
T& operator*()
{
return _node->_data;
}
// 不能实现,因为const __list_iterator对象才能调用这个重载
// 但是const __list_iterator对象不能调用++
//我们的const需求是不能改变迭代器指向的东西,而不是迭代器本身不能改变
/*const T& operator*() const
{
return _node->_data;
}*/
__list_iterator<T>& operator++()
{
_node = _node->_next;
return *this;
}
T* operator->()
{
return &_node->_data; // 返回数据的普通指针,支持修改成员
}
__list_iterator<T> operator++(int)
{
__list_iterator<T> tmp(*this);
_node = _node->_next;
return tmp;
}
__list_iterator<T>& operator--()
{
_node = _node->_prev;
return *this;
}
__list_iterator<T> operator--(int)
{
__list_iterator<T> tmp(*this);
_node = _node->_prev;
return tmp;
}
bool operator!=(const __list_iterator<T>& it) const
{
return _node != it._node;
}
bool operator==(const __list_iterator<T>& it) const
{
return _node == it._node;
}
};
//const版本
template<class T>
struct __list_const_iterator
{
typedef list_node<T> Node;
Node* _node;
__list_const_iterator(Node* node)
:_node(node)
{}
//返回const int& ,不能改值
const T& operator*() const
{
return _node->_data;
}
const T* operator->() const
{
return &_node->_data; // 返回数据的const指针,只读成员
}
__list_const_iterator<T>& operator++()
{
_node = _node->_next;
return *this;
}
__list_const_iterator<T> operator++(int)
{
__list_const_iterator<T> tmp(*this);
_node = _node->_next;
return tmp;
}
__list_const_iterator<T>& operator--()
{
_node = _node->_prev;
return *this;
}
__list_const_iterator<T> operator--(int)
{
__list_const_iterator<T> tmp(*this);
_node = _node->_prev;
return tmp;
}
bool operator!=(const __list_const_iterator<T>& it) const
{
return _node != it._node;
}
bool operator==(const __list_const_iterator<T>& it) const
{
return _node == it._node;
}
};
可以看到上面两份代码几乎一模一样,我们可以运用模板类,通过设置模板参数,区分普通迭代器和const迭代器,由编译器自动生成两种的函数,完成各自的功能
通用迭代器设计(兼容普通迭代器和 const 迭代器)
通过 Ref 和 Ptr 两个模板参数,我们假设用Ref代替T&/const T&,用Ptr代替T*/const T*我们只需要写一份
_list_iterator,再通过不同的参数实例化,就能同时得到两种迭代器
为了方便理解,先展示具体类型的实例化
cpp
//普通迭代器
_list_iterator<int, int&, int*>;
//const迭代器
_list_iterator<int, const int&, const int*>;
现在我们来逐步拆解迭代器代码,接下来讲的东西会很绕,让我们再来理一遍逻辑。
- 首先要明确迭代器本质 :是包装了节点指针(_node)的对象 ,不管是解引用(*)、箭头访问(->),还是前后移动(++/- -),所有操作的底层都是在操控这个节点指针------我们之所以要封装这个对象,就是为了屏蔽链表节点的底层细节(比如不用手动写 _node->_next),提供一套统一、简单的遍历访问接口。
- 其次要区分"迭代器类"和"实例化的迭代器"
我们现在创建的迭代器只是一个类,并没有实例化,类相当于建房图纸一样规划有多少个房间,房间大小功能等,所以我们构建迭代器类的时候,也只是在规划它要实现怎样的功能,只有当我们在list容器中实例化它(比如通过 typedef 确定普通/const迭代器的具体类型),这份"图纸"才会变成"能住人的房子",迭代器的读写权限、返回值类型等才会有具体意义理清这两点,再看后续的模板参数、运算符重载,就不会陷入细节迷失方向
cpp
//当前三个模板参数只是规划,只是一个图纸,等到后面实例化迭代器时
//才会替换成具体类型,真正区分处普通迭代器和const迭代器
//定义一个模板类----迭代器类
template<class T,class Ref,class Ptr>
struct _list_iterator
{
//如果觉得这些类太陌生,可以想想我们熟悉的vector,它也是一个类
//重命名节点结构体,不用每次都写 list_node<T>,直接写 Node
typedef list_node<T> Node;
//// 重命名自身类型,不用每次写 _list_iterator<T, Ref, Ptr>,直接写 Self
typedef _list_iterator<T, Ref, Ptr> Self;
//成员变量:节点指针
//迭代器通过这个指针,找到对应的链表节点,进而访问节点里面的数据(_data,_prev,_next)
Node* _node;
//构造函数,接收节点指针,初始化迭代器
//创建迭代器时,必须绑定一个具体的节点,(如iterator(_head->_next))
_list_iterator(Node*node)
:_node(node)
{ }
//Ref决定了返回值是普通引用(int&),还是const引用(const int&)
//解引用操作运算符重载,返回数据的引用
Ref operator*()
{
return _node->_data;
}
//Ptr决定了返回值是普通地址(T*),还是const地址(const T*)
//箭头操作,返回数据的地址,访问结构体/类成员
//简化结构体 / 类成员的访问,不用写 (*it).name,直接写 it->name
Ptr operator->()
{
return &_node->_data;//返回_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& it) const
{
return _node != it._node;//比较是否指向不同节点
}
bool operator==(const Self& it) const
{
return _node == it._node; //比较两个迭代器的_node 是否指向同一个节点
}
};
用Self,Self&做返回值的函数,允许所有迭代器使用
用Ref和Ptr,做返回值的函数,根据实例化的结果自动生成对应的有无const修饰的函数
没理解也没关系,我们只需要记住迭代器现在可以像指针一样++/- -/*/->/使用就好了
3、 list类的实现
list 类封装了链表的核心操作(初始化、拷贝构造、赋值、插入、删除等),内部持有哨兵节点指针_head和节点个数_size
哨兵节点(_head)是一个 "不存储有效数据" 的节点,它让空链表和非空链表的操作逻辑统一(比如push_back都是 "在哨兵前插入",pop_back都是 "删除哨兵前一个节点"),非常方便简洁
1)list类的私有成员
cpp
private:
Node* _head;//指向哨兵节点的指针,链表的核心控制指针
size_t _size = 0;//链表中有效元素的个数,不包含哨兵节点
- _head:是整个双向循环链表的 "总开关",通过它可以找到所有有效节点(_head->_next 是第一个有效节点,_head->_prev 是最后一个有效节点)
- _size:记录有效元素个数
2)迭代器类型的定义(在list类中)
通过模板参数实例化,分别得到普通迭代器和 const 迭代器
之前的 _list_iterator<T, Ref, Ptr> 是 "通用模板"(图纸),三个参数都是占位符。现在通过 typedef
给模板传了 "具体参数",直接生成两个 "专用迭代器类型"
cpp
template<class T>
class list
{
public:
typedef list_node<T> Node;
//重命名变成我们熟知的普通迭代器和const迭代器
//普通迭代器:Ref=T&,Ptr=T*
typedef _list_iterator<T, T&, T*>iterator;
// const迭代器:Ref=const T&,Ptr=const T*
typedef _list_iterator<T, const T&, const T*> const_iterator;
};
迭代器核心接口(begin/end) list 是双向循环链表,通常用 "哨兵节点"(_head)简化边界处理
• begin():指向第一个有效节点(_head->_next)。
• end():指向哨兵节点(_head),作为尾后迭代器(不存储数据)
cpp
iterator begin()
{
return iterator(_head->_next); //用第一有效节点初始化迭代器
}
iterator end()
{
return iterator(_head); //用哨兵节点初始化迭代器
}
//const版本
const_iterator begin() const
{
return const_iterator(_head->_next);
}
const_iterator end() const
{
return const_iterator(_head);
}
3)初始化与构造函数

cpp
public:
//空链表初始化
void empty_init()
{
_head = new Node; //创建哨兵节点(默认构造,数据为T())
_head->_next = _head;//后继指向自身
_head->_prev = _head;//前驱指向自身
}
//无参构造:直接调用empty_init创建空链表
list()
{
empty_init();
}
//拷贝构造(lt2(lt1))深拷贝传入链表的所有节点
list(const list<T>& lt)
{
empty_init();//先初始化自身为空头链表
//遍历lt,将每一个元素插入到当前链表尾部
for (const auto& e : lt)
{
push_back(e);
}
_size = lt._size;//同步原链表的大小
}
//使用场景list<int> l = {1,2,3,4};
//初始化列表构造(C++11)
list(initializer_list<T> il)
{
empty_init();
// 遍历初始化列表中的所有元素,逐个尾插到当前链表
for (const auto& e : il)
{
push_back(e);
}
4) 赋值运算符重载
采用拷贝交换法,简洁高效且天然支持自赋值
cpp
//交换函数:交换两个链表的哨兵节点和大小
void swap(list<T>& lt)
{
std::swap(_head, lt._head);
std::swap(_size, lt._size);
}
//赋值运算符(lt1=lt3)
list<T>& operator=(list<T> lt)//传值参数,自动拷贝一份lt3
{
// 交换当前对象和lt,并且函数返回lt自动析构
swap(lt);
return *this;
}
为什么交换两个链表的哨兵节点和大小呢?
因为 list 的所有有效节点都通过 _head 关联,交换 _head 就相当于交换了整个链表的所有节点(不用逐个交换节点,O (1) 时间复杂度,极高效)
5)析构函数
先清空所有有效节点,再删除哨兵节点
cpp
//析构函数
~list()
{
clear();//清空有效节点
delete _head;//删除哨兵节点
_head = nullptr;//避免野指针
}
//清空所有有效节点,保留哨兵节点
void clear()
{
iterator it = begin();
while (it != end())
{
it = erase(it);//用 erase 的返回值更新 it,避免迭代器失效
}
_size = 0;
}
6)插入与删除操作

insert
在迭代器pos指向的节点前插入新节点
cpp
iterator insert(iterator pos, const T& val)
{
//确保pos指向的节点有效
assert(pos._node != nullptr);
//取出pos封装的节点指针
Node* cur = pos._node;
//创建新节点
Node* newnode = new Node(val);
//找到cur的前驱节点
Node* prev = cur->_prev;
// 调整指针关系:prev -> newnode -> cur
prev->_next = newnode;
newnode->_next = cur;
cur->_prev = newnode;
newnode->_prev = prev;
++_size;//节点+1
//返回指向新节点的迭代器
return iterator(newnode);
}

erase
删除迭代器pos指向的节点,返回下一个节点的迭代器,避免迭代器失效
cpp
//erase
iterator erase(iterator pos)
{
//不能删除哨兵节点
assert(pos != end());
//取出 pos 指向的节点
Node* cur = pos._node;
// 保存前后节点,避免删除后找不到
Node* prev = cur->_prev;
Node* next = cur->_next;
// 调整指针关系:prev -> next,跳过cur
prev->_next = next;
next->_prev = prev;
delete cur;//释放cur节点内存
--_size;//节点个数-1
return iterator(next);//返回下一个节点的迭代器
}

7)其他常用接口
push_back/push_front
pop_back/pop_front
empty()/size()
基于 insert 和 erase 封装常用操作
cpp
//尾插:复用 insert,在哨兵节点前插入
void push_back(const T& x)
{
insert(end(), x);
}
//头插:复用 insert,在第一个有效节点前插入
void push_front(const T& x)
{
insert(begin(), x);
}
//尾删(--end()指向最后一个有效节点)
void pop_back()
{
erase(--end());
}
//头删,复用 erase,删除第一个有效节点(begin())
void pop_front()
{
erase(begin());
}
// 判断链表是否为空(有效节点个数为 0)
bool empty() const
{
return _size == 0;
}
// 返回节点个数
size_t size() const
{
return _size;
}
4、测试
其实应该写一段代码,就检测一段代码的正确性,为了方便阅读,我将测试用例都写在一起了
cpp
#include"list.h"
//初始化列表构造,拷贝构造,赋值运算符、尾插尾删,头插头删
void test1()
{
LI::list<int> l1 = { 1,2,3,4,5 };
for (auto e : l1)
{
cout << e << " ";
}
cout<<"size:"<<l1.size() << endl;//输出:1 2 3 4 5 size:5
LI::list<int> l2(l1);
l2.push_back(8);
l2.pop_front();
for (auto e : l2) cout << e << " ";
cout << "size:" << l2.size() << endl;//输出:2 3 4 5 8 size:5
LI::list<int> l3;
l3 = l1;
l3.push_front(8);
l3.pop_back();
for(auto e:l3) cout << e << " "; //输出8 1 2 3 4
cout << endl;
}
//const迭代器遍历
void test2()
{
const LI::list<int> l4 = { 1,2,3,4,5 };
for (LI::list<int>::const_iterator it = l4.begin();it!=l4.end();++it)
{
cout << *it << " ";//输出:1 2 3 4 5
}
cout << endl;
}
//插入,删除,清空
void test3()
{
LI::list<int> l5;
l5.push_back(1);
l5.push_back(3);
auto it = l5.begin();
++it;
l5.insert(it, 2);//// 在1和3之间插入2
for (auto it : l5)
{
cout << it << " ";//输出1 2 3
}
cout << endl;
l5.erase(l5.begin()++);
for (auto it : l5)
{
cout << it << " ";//输出2 3
}
cout << endl;
l5.clear();
//输出清空后empty: yes
cout << "清空后empty: " << (l5.empty() ? "yes" : "no") << endl;
}
int main()
{
//test1();
//test2();
test3();
}
5、完整代码
list.h
cpp
#pragma once
#include<iostream>
using namespace std;
#include<assert.h>
//创建LI命名空间,让自己模拟实现的list与std::list隔离
namespace LI
{
//创建类模板,接收不同类型的数据(内置类型、自定义类型)
//用struct结构体,因为struct默认成员是公有的,方便list容器直接访问
template<class T>
struct list_node
{
T _data;//节点储存的数据
list_node<T>* _next;//指向后继节点的指针
list_node<T>* _prev;//指向前驱节点的指针
//构造函数,传引用避免拷贝
list_node(const T& x = T())
:_data(x)
, _next(nullptr)
, _prev(nullptr)
{
}
};
//当前三个模板参数只是规划,只是一个图纸,等到后面实例化迭代器时
//才会替换成具体类型,真正区分处普通迭代器和const迭代器
//定义一个模板类----迭代器类
template<class T, class Ref, class Ptr>
struct _list_iterator
{
//如果觉得这些类太陌生,可以想想我们熟悉的vector,它也是一个类
//重命名节点结构体,不用每次都写 list_node<T>,直接写 Node
typedef list_node<T> Node;
//// 重命名自身类型,不用每次写 _list_iterator<T, Ref, Ptr>,直接写 Self
typedef _list_iterator<T, Ref, Ptr> Self;
//成员变量:节点指针
//迭代器通过这个指针,找到对应的链表节点,进而访问节点里面的数据(_data,_prev,_next)
Node* _node;
//构造函数,接收节点指针,初始化迭代器
//创建迭代器时,必须绑定一个具体的节点,(如iterator(_head->_next))
_list_iterator(Node* node)
:_node(node)
{
}
//Ref决定了返回值是普通引用(int&),还是const引用(const int&)
//解引用操作,返回数据的引用
Ref operator*() const
{
return _node->_data;
}
//Ptr决定了返回值是普通地址(T*),还是const地址(const T*)
//箭头操作,返回数据的地址,访问结构体/类成员
//简化结构体 / 类成员的访问,不用写 (*it).name,直接写 it->name
Ptr operator->() const
{
return &_node->_data;//返回_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& it) const
{
return _node != it._node;//比较是否指向不同节点
}
bool operator==(const Self& it) const
{
return _node == it._node; //比较两个迭代器的_node 是否指向同一个节点
}
};
template<class T>
class list
{
public:
typedef list_node<T> Node;
//重命名变成我们熟知的普通迭代器和const迭代器
//普通迭代器:Ref=T&,Ptr=T*
typedef _list_iterator<T, T&, T*>iterator;
// const迭代器:Ref=const T&,Ptr=const T*
typedef _list_iterator<T, const T&, const T*> const_iterator;
iterator begin()
{
return iterator(_head->_next); //用第一有效节点初始化迭代器
}
iterator end()
{
return iterator(_head); //用哨兵节点初始化迭代器
}
//const版本
const_iterator begin() const
{
return const_iterator(_head->_next);
}
const_iterator end() const
{
return const_iterator(_head);
}
private:
Node* _head;//指向哨兵节点的指针,链表的核心控制指针
size_t _size = 0;//链表中有效元素的个数,不包含哨兵节点
public:
//空链表初始化
void empty_init()
{
_head = new Node; //创建哨兵节点(默认构造,数据为T())
_head->_next = _head;//后继指向自身
_head->_prev = _head;//前驱指向自身
}
//无参构造:直接调用empty_init创建空链表
list()
{
empty_init();
}
//拷贝构造(lt2(lt1))深拷贝传入链表的所有节点
list(const list<T>& lt)
{
empty_init();//先初始化自身为空头链表
//遍历lt,将每一个元素插入到当前链表尾部
for (const auto& e : lt)
{
push_back(e);
}
_size = lt._size;//同步原链表的大小
}
//使用场景list<int> l = {1,2,3,4};
//初始化列表构造(C++11)
list(initializer_list<T> il)
{
empty_init();
// 遍历初始化列表中的所有元素,逐个尾插到当前链表
for (const auto& e : il)
{
push_back(e);
}
//直接复用初始化列表的大小
_size = il.size();
}
//交换函数:交换两个链表的哨兵节点和大小
void swap(list<T>& lt)
{
std::swap(_head, lt._head);
std::swap(_size, lt._size);
}
//赋值运算符(lt1=lt3)
list<T>& operator=(list<T> lt)//传值参数,自动拷贝一份lt3
{
// 交换当前对象和lt,并且函数返回lt自动析构
swap(lt);
return *this;
}
//析构函数
~list()
{
clear();//清空有效节点
delete _head;//删除哨兵节点
_head = nullptr;//避免野指针
}
//清空所有有效节点,保留哨兵节点
void clear()
{
iterator it = begin();
while (it != end())
{
it = erase(it);//用 erase 的返回值更新 it,避免迭代器失效
}
_size = 0;
}
iterator insert(iterator pos, const T& val)
{
//确保pos指向的节点有效
assert(pos._node != nullptr);
//取出pos封装的节点指针
Node* cur = pos._node;
//创建新节点
Node* newnode = new Node(val);
//找到cur的前驱节点
Node* prev = cur->_prev;
// 调整指针关系:prev -> newnode -> cur
prev->_next = newnode;
newnode->_next = cur;
cur->_prev = newnode;
newnode->_prev = prev;
++_size;//节点+1
//返回指向新节点的迭代器
return iterator(newnode);
}
//erase
iterator erase(iterator pos)
{
//不能删除哨兵节点
assert(pos != end());
//取出 pos 指向的节点
Node* cur = pos._node;
// 保存前后节点,避免删除后找不到
Node* prev = cur->_prev;
Node* next = cur->_next;
// 调整指针关系:prev -> next,跳过cur
prev->_next = next;
next->_prev = prev;
delete cur;//释放cur节点内存
--_size;//节点个数-1
return iterator(next);//返回下一个节点的迭代器
}
//尾插:复用 insert,在哨兵节点前插入
void push_back(const T& x)
{
insert(end(), x);
}
//头插:复用 insert,在第一个有效节点前插入
void push_front(const T& x)
{
insert(begin(), x);
}
//尾删(--end()指向最后一个有效节点)
void pop_back()
{
erase(--end());
}
//头删,复用 erase,删除第一个有效节点(begin())
void pop_front()
{
erase(begin());
}
// 判断链表是否为空(有效节点个数为 0)
bool empty() const
{
return _size == 0;
}
// 返回节点个数
size_t size() const
{
return _size;
}
};
}