
🔥小叶-duck:个人主页
❄️个人专栏:《Data-Structure-Learning》
✨未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游
目录
[一、底层原理:List 容器的"骨架"------ 带头双向循环链表](#一、底层原理:List 容器的“骨架”—— 带头双向循环链表)
[二、模块实现:List 核心代码](#二、模块实现:List 核心代码)
[1、模块 1:链表(List)及其节点(List_node)底层结构](#1、模块 1:链表(List)及其节点(List_node)底层结构)
[1.1 链表及其结点构造初始化](#1.1 链表及其结点构造初始化)
[1.2 核心解析](#1.2 核心解析)
[1.3 功能测试(Test.cpp)](#1.3 功能测试(Test.cpp))
[2、模块 2:迭代器(List_iterator)的实现(重点掌握)](#2、模块 2:迭代器(List_iterator)的实现(重点掌握))
[2.1 简单 List 迭代器的模拟实现](#2.1 简单 List 迭代器的模拟实现)
[2.1.1 测试简单迭代器的功能](#2.1.1 测试简单迭代器的功能)
[2.2 const 迭代器(List_const_iterator)实现](#2.2 const 迭代器(List_const_iterator)实现)
[2.3 "多模板参数" 复用普通 / const 迭代器](#2.3 “多模板参数” 复用普通 / const 迭代器)
[3、模块 3:容器类(list)------ List 功能的 "中枢"](#3、模块 3:容器类(list)—— List 功能的 “中枢”)
[3.1 插入数据(insert、push_back、push_front)](#3.1 插入数据(insert、push_back、push_front))
[3.1.1 insert(任意位置插入) 实现](#3.1.1 insert(任意位置插入) 实现)
[3.1.2 push_back(尾插)、push_front(头插) 实现(复用insert)](#3.1.2 push_back(尾插)、push_front(头插) 实现(复用insert))
[3.1.3 代码测试(Test.cpp)](#3.1.3 代码测试(Test.cpp))
[3.2 删除数据(erase、pop_back、pop_front)](#3.2 删除数据(erase、pop_back、pop_front))
[3.2.1 erase(任意位置删除) 实现(有缺陷)](#3.2.1 erase(任意位置删除) 实现(有缺陷))
[3.2.2 pop_back(尾删)、pop_front(头删) 实现(复用erase)](#3.2.2 pop_back(尾删)、pop_front(头删) 实现(复用erase))
[3.2.3 代码测试(Test.cpp)](#3.2.3 代码测试(Test.cpp))
[3.2.4 迭代器失效](#3.2.4 迭代器失效)
[4.1 析构 ~List() 和清除数据 clear()](#4.1 析构 ~List() 和清除数据 clear())
[4.2 拷贝构造](#4.2 拷贝构造)
[4.3 赋值重载=](#4.3 赋值重载=)
一、底层原理:List 容器的"骨架"------ 带头双向循环链表
要手写实现 List,首先要明确其底层结构 ------ 带头双向循环链表,这是所有接口高效实现的基础。
|-----------|-------------------------------------------------------------------------------------------------------------|
| 结构部分 | 功能说明 |
| 哨兵头节点 | 不存储有效数据,仅作为操作锚点,统一空/非空链表的插入、删除逻辑,无需额外判断边界。例如:尾插时无需检查"是否为第一个节点",直接通过头节点的前驱指针定位尾节点,简化代码逻辑。 |
| 数据节点 | 每个节点含 _prev (前驱指针)、 _next (后继指针)、 _data (数据域),支持双向遍历。既可以从当前节点向前追溯前驱节点,也能向后访问后继节点,为迭代器的 ++/-- 操作提供底层支持。 |
| 循环特性 | 尾节点 _next 指向头节点,头节点 _prev 指向尾节点,形成闭环。例如:遍历到尾节点后,通过 _next 可直接回到头节点;获取尾节点无需遍历整个链表,只需访问 _head->_prev,提升尾操作效率。 |
二、模块实现:List 核心代码
1、模块 1:链表(List)及其节点(List_node)底层结构
节点是存储数据的载体,用模板类实现泛型支持,适配任意数据类型(如 int、string)。
cpp
//List.h
#include<iostream>
#include<assert.h>
using namespace std;
namespace MyList
{
template<class T>
//链表结点的类结构:存储数据与双向指针
struct List_node
{
//struct不写限定默认是public,可以让类List访问里面成员变量
T _data; // 节点数据
List_node<T>* _next; // 后继节点指针
List_node<T>* _prev; // 前驱节点指针
};
template<class T>
//链表的类结构
class List
{
private:
List_node<T>* _head; //哨兵位结点(头节点)
size_t _size = 0; //链表节点个数
};
}
1.1 链表及其结点构造初始化
链表初始化:
cpp
template<class T>
class List
{
public:
//无参构造(空链表)
List()
{
_head = new List_node<T>;
_head->_next = _head;
_head->_prev = _head;
_size = 0;
}
private:
List_node<T>* _head; //哨兵位结点(头节点)
size_t _size = 0; //链表节点个数
};
链表结点初始化:
cpp
template<class T>
struct List_node
{
//全缺省构造结点:默认值初始化,指针置空
List_node(const T& x = T())
:_data(x)
,_next(nullptr)
,_prev(nullptr)
{}
//struct不写限定默认是public,可以让类List访问里面成员变量
T _data; // 节点数据
List_node<T>* _next; // 后继节点指针
List_node<T>* _prev; // 前驱节点指针
};
1.2 核心解析
- 模板参数
T:支持任意数据类型,例如 MyList::List<int> 存储整数 MyList::List<string> 存储字符串,与 string 的泛型设计一致。 - 默认构造参数 :T()确保内置类型(如 int )默认初始化为 0,自定义类型自动调用其默认构造函数,兼容性强。
- 指针初始化:_prev 与 _next 初始化为 nullptr,避免野指针风险,后续由容器类统一管理指针链接。
1.3 功能测试(Test.cpp)
cpp
//Test.cpp
#include "List.h"
int main()
{
MyList::List<int> l1; //空链表
MyList::List_node<int> n1(1); //空结点
return 0;
}

2、模块 2:迭代器(List_iterator)的实现(重点掌握)
上篇文章最后我讲过 list 本身接口的使用非常简单但是模拟实现比较难,难点就在于接下来要模拟实现的迭代器。
List 的迭代器不再是像前面所学的 string 和 vector 那种简单的原生指针,而是封装 List_node* 的类 ,通过运算符重载模拟指针行为。
之所以要这么麻烦原因在于不管是之前学的 string 还是 vector 底层都是数组,对其某个位置解引用获取到的就是该位置的数据;并且由于数组是连续的空间,++ 就能获取到下一个位置的地址,这是基于数组结构的优势。
但是 list 对链表的某个位置进行解引用得到的是一个结点 ,获取结点本身是没有意义 的,我们想要的是获取结点存放的数据;并且由于 list 并不是连续的空间 ,++ 也就不能获取到下一个结点的地址了,这也是 list 底层结构带来的劣势。
那我们怎么去解决上面这些问题呢?就是通过对 解引用* 以及 ++ 等运算符进行运算符重载 ,手动去改变它们原有的逻辑,让我们能够通过 解引用* 来获取到结点存放的数据,通过 ++ 能够获取到下一个结点的地址。
2.1 简单 List 迭代器的模拟实现
cpp
//List.cpp
//简单迭代器实现
template<class T>
struct List_iterator //和List_node一样用struct,便于后续可以直接访问类中的成员变量
{
typedef List_iterator<T> Self;
typedef List_node<T> Node; //重命名简化类型名称便于使用
//迭代器构造
List_iterator(Node* node)
:_node(node)
{ }
//运算符重载 * (直接获取结点存放的数据)
T& operator*()
{
return _node->_data;
}//引用传参减少拷贝构造
//运算符重载 前置++ (直接获取下一个结点的指针)
Self& operator++()
{
_node = _node->_next;
return *this;
//因为隐式的this指针是类的地址,解引用得到的就是类
//然后为了减少拷贝构造,所以返回类型是Self&
}
//运算符重载 -- (直接获取上一个结点的指针)
Self& operator--()
{
_node = _node->_prev;
return *this;
}
//运算符重载 != (比较两个迭代器的大小:通过判断指向的结点是否相同)
//it != lt.end()
bool operator!=(const Self& s)
{
return _node != s._node;
}
//运算符重载 ==
bool operator==(const Self& s)
{
return _node == s._node;
}
Node* _node; // List 迭代器的逻辑仍然是结点的指针
};
template<class T>
//链表的类结构
class List
{
public:
//利用手动实现的迭代器实现begin/end
typedef List_iterator<T> iterator;
iterator begin()
{
/*iterator it(_head->_next);
return it;*/
//这里是构造一个类型是iterator的对象it
//因为不像前面学习的string和vector那样迭代器本身就是原生指针
//这里List的迭代器是封装成了一个类,需要通过构造一个iterator对象来返回
//通过前面学习的匿名对象我们可以优化代码:
return iterator(_head->_next);
//但其实也可以直接返回_head->_next,因为编译器会走隐式类型转换
//将List_node<T>*(结点的指针)转换为iterator:
//return _head->_next;
}
iterator end()
{
return iterator(_head);
//List的end()指向的是最后一位有效数据的下一个位置,也就是头节点
}
private:
List_node<T>* _head; //哨兵位结点(头节点)
size_t _size = 0; //链表节点个数
};
2.1.1 测试简单迭代器的功能
cpp
#include "List.h"
int main()
{
MyList::List<int> l1; //空链表
l1.push_back(1);
l1.push_back(2);
l1.push_back(3);
l1.push_back(4);//push_back修改接口的实现下面会讲解,这里先用一下
//迭代器遍历
MyList::List<int>::iterator it = l1.begin();
//MyList::List_iterator<int> it = l1.begin();
//两种写法,第一种更符合 list 原本的接口使用
while (it != l1.end())
{
cout << *it << " ";
//这里*it就是调用*的运算符重载直接获取结点存放的数据
++it; //++也是如此
}
cout << endl;
return 0;
}

2.2 const 迭代器(List_const_iterator)实现
我们会发现上面每次要遍历链表,都需要写迭代器,那我们就可以将遍历打印写进一个函数,调用函数来进行遍历:
cpp
//List.cpp
//遍历打印链表(类外)
template<class Container>
void print_container(const Container& con)
{
for (auto e : con)
{
cout << e << " ";
}
cout << endl;
}
//Test.cpp
int main()
{
MyList::List<int> l1; //空链表
l1.push_back(1);
l1.push_back(2);
l1.push_back(3);
l1.push_back(4);
MyList::print_container(l1);
return 0;
}

但是会发现出现了报错,其实看到红框的报错原因应该就不难发现就是权限放大导致的,因为函数 print_container 的形参是 const 修饰,而函数体内使用的范围for底层的迭代器却是普通迭代器,这就导致了报错,所以解决方法就是再实现一个 const 迭代器:
cpp
//List.cpp
//const迭代器
template<class T>
struct List_const_iterator
{
typedef List_const_iterator<T> Self;
typedef List_node<T> Node;
//迭代器构造
List_const_iterator(Node* node)
:_node(node)
{}
const T& operator*()const
{
return _node->_data;
}
Self& operator++()
{
_node = _node->_next;
return *this;
}
Self& operator--()
{
_node = _node->_prev;
return *this;
}
bool operator!=(const Self& s)
{
return _node != s._node;
}
bool operator==(const Self& s)
{
return _node == s._node;
}
Node* _node;
};
template<class T>
//链表的类结构
class List
{
public:
typedef List_iterator<T> iterator;
typedef List_const_iterator<T> const_iterator;
iterator begin()
{
return iterator(_head->_next);
}
const_iterator begin()const
{
return const_iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
const_iterator end()const
{
return const_iterator(_head);
}
private:
List_node<T>* _head;
size_t _size = 0;
};
这是绝大多数人的解决方法,就直接再多写一个 const 迭代器的类。
但是我们会发现我们实现的 const 迭代器和普通迭代器其实是非常雷同的,除了类名不同之外也就只有类里面的 解引用* 的运算符重载不一样(因为 const 迭代器不能修改迭代器指向的内容),那为了这个而大费周章再写一个非常雷同的 const 迭代器有必要吗?其实是没必要的。
2.3 "多模板参数" 复用普通 / const 迭代器
所以接下来的方法就是基于前面模板相关知识所实现的------"多模板参数" 复用普通 / const 迭代器。
cpp
//"多模板参数" 复用普通 / const 迭代器
template<class T, class Ref>
//迭代器类:T-数据类型,Ref-引用类型
struct List_iterator
{
typedef List_iterator<T, Ref> Self;
typedef List_node<T> Node;
List_iterator(Node* node)
:_node(node)
{
}
Ref operator*()
{
return _node->_data;
}//返回类型是引用类型模板参数Ref,有没有const就看实例化的情况而定
Self& operator++()
{
_node = _node->_next;
return *this;
}
Self& operator--()
{
_node = _node->_prev;
return *this;
}
bool operator!=(const Self& s)
{
return _node != s._node;
}
bool operator==(const Self& s)
{
return _node == s._node;
}
Node* _node;
};
template<class T>
//链表的类结构
class List
{
public:
//利用手动实现的迭代器实现begin/end
/*typedef List_iterator<T> iterator;
typedef List_const_iterator<T> const_iterator;*/
typedef List_iterator<T, T&> iterator;
typedef List_iterator<T, const T&> const_iterator;
iterator begin()
{
return iterator(_head->_next);
}
const_iterator begin()const
{
return const_iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
const_iterator end()const
{
return const_iterator(_head);
}
private:
List_node<T>* _head; //哨兵位结点(头节点)
size_t _size = 0; //链表节点个数
};
一看到上面这样的写法可能有些人还是不理解是什么意思,我用下面的图可以更加形象的展示出来:

所以对于 List<int>::const_iterator it 这个对象而言,逻辑和上面图一样,首先 const_iterator 是 List_iterator<T, const T&> 的别名,所以迭代器类模板进行实例化 ,将模板参数 Ref 实例化成 const T& ,但此时实例化出的仍然是一个 const 迭代器的类模板 (相当于从多模板参数实例化成单模板参数),然后再将模板参数 T 实例化成 int 类型生成最终的 const 迭代器类。
比起前面简单学习的模板,这里所讲的"多模板参数"就是对模板更深入的学习,之前我们学习的单参数模板进行实例化出的就是一个函数或者类,但是这次我们讲解的"多模板参数"让我们知道了模板进行实例化也可能还是一个模板(只是参数减少了)。
3、模块 3:容器类(list)------ List 功能的 "中枢"
3.1 插入数据(insert、push_back、push_front)
3.1.1 insert(任意位置插入) 实现
cpp
template<class T>
//链表的类结构
class List
{
public:
typedef List_iterator<T, T&> iterator;
typedef List_iterator<T, const T&> const_iterator;
//insert插入
void insert(iterator pos, const T& x)
{
List_node<T>* cur = pos._node;
List_node<T>* pre = cur->_prev;
//创建临时对象,避免对 pos._node 的指向进行修改
List_node<T>* newnode = new List_node<T>(x);
// prev newnode cur
pre->_next = newnode;
cur->_prev = newnode;
newnode->_prev = pre;
newnode->_next = cur;
_size++;
}
private:
List_node<T>* _head; //哨兵位结点(头节点)
size_t _size = 0; //链表节点个数
};
3.1.2 push_back(尾插)、push_front(头插) 实现(复用insert)
cpp
template<class T>
//链表的类结构
class List
{
public:
typedef List_iterator<T, T&> iterator;
typedef List_iterator<T, const T&> const_iterator;
//尾插(复用insert)
void push_back(const T& x)
{
insert(end(), x);
//上面实现了insert可以直接进行复用
//注意的是list尾插是在头节点之前插入,而end()就是头节点
}
//头插(复用insert)
void push_front(const T& x)
{
insert(begin(), x);
}
private:
List_node<T>* _head; //哨兵位结点(头节点)
size_t _size = 0; //链表节点个数
};
3.1.3 代码测试(Test.cpp)
cpp
//Test.cpp
void test_list1()
{
MyList::List<int> l1; //空链表
l1.push_back(1);
l1.push_back(2);
l1.push_back(3);
l1.push_back(4);
MyList::print_container(l1);
l1.push_front(10);
MyList::print_container(l1);
l1.insert(++l1.begin(), 20);
MyList::print_container(l1);
}
int main()
{
test_list1();
return 0;
}

3.2 删除数据(erase、pop_back、pop_front)
3.2.1 erase(任意位置删除) 实现(有缺陷)
cpp
template<class T>
//链表的类结构
class List
{
public:
typedef List_iterator<T, T&> iterator;
typedef List_iterator<T, const T&> const_iterator;
//erase删除
void erase(iterator pos)
{
//不能删除头节点
assert(pos != end());
List_node<T>* pre = pos._node->_prev;
List_node<T>* next = pos._node->_next;
pre->_next = next;
next->_prev = pre;
delete pos._node;
_size--;
}
private:
List_node<T>* _head; //哨兵位结点(头节点)
size_t _size = 0; //链表节点个数
};
3.2.2 pop_back(尾删)、pop_front(头删) 实现(复用erase)
cpp
template<class T>
//链表的类结构
class List
{
public:
typedef List_iterator<T, T&> iterator;
typedef List_iterator<T, const T&> const_iterator;
//尾删
void pop_back()
{
erase(--end());
}
//头删
void pop_front()
{
erase(begin());
}
private:
List_node<T>* _head; //哨兵位结点(头节点)
size_t _size = 0; //链表节点个数
};
3.2.3 代码测试(Test.cpp)
cpp
//Test.cpp
void test_list1()
{
MyList::List<int> l1; //空链表
l1.push_back(1);
l1.push_back(2);
l1.push_back(3);
l1.push_back(4);
int i = 2;
MyList::List<int>::iterator it = l1.begin();
while (i--)
{
++it;
}
l1.erase(it);
MyList::print_container(l1);
l1.pop_back();
MyList::print_container(l1);
l1.pop_front();
MyList::print_container(l1);
}
int main()
{
test_list1();
return 0;
}

3.2.4 迭代器失效
我们先看一个问题:基于上面的代码将一个链表中的所有偶数数据进行删除:
cpp
//Test.cpp
void test_list2()
{
MyList::List<int> l1; //空链表
l1.push_back(1);
l1.push_back(2);
l1.push_back(3);
l1.push_back(4);
l1.push_back(5);
l1.push_back(6);
//删除所有偶数
MyList::List<int>::iterator it = l1.begin();
while (it != l1.end())
{
if (it._node->_data % 2 == 0)
{
l1.erase(it);
}
else
{
++it;
}
}
MyList::print_container(l1);
}
int main()
{
test_list2();
return 0;
}
在前面模拟实现vector文章中的迭代器失效我们也讲解了这个问题,代码逻辑也很简单,但是当我们运行程序时就会出现这种情况:

如果有看过我前面一篇文章揭秘 C++ vector 底层:三指针掌控内存奥秘并且对迭代器失效的本质大致清楚的应该就知道上面实现的 erase 会导致迭代器失效。
那有些人就会问为什么上面的插入数据 insert 不会导致迭代器失效呢?原因就在于之前学习的 vector 的插入可能会导致扩容,而扩容是会开新空间而删除旧空间 ,就是因为删除了旧空间而导致的迭代器失效 ,List 的插入是结点指针指向的内容发生改变而没有扩容之说 。
而 List 的删除数据 erase 是将 pos 位置结点的前后结点进行连接再删除该节点,所以就会发现迭代器失效的本质原因 就在于 delete 了该位置的空间或者包括该位置的一段空间从而导致不能再访问。
所以解决方法也和之前的迭代器失效一样:返回下一个迭代器来更新迭代器,避免访问失效节点。
cpp
//erase删除
iterator erase(iterator pos)
{
//不能删除头节点
assert(pos != end());
List_node<T>* pre = pos._node->_prev;
List_node<T>* next = pos._node->_next;
pre->_next = next;
next->_prev = pre;
delete pos._node;
//delete了 pos 结点,此时迭代器就会失效
//所以通过添加返回值,返回下一个迭代器,避免访问失效节点
_size--;
return next;
}
//Test.cpp
void test_list2()
{
MyList::List<int> l1; //空链表
l1.push_back(1);
l1.push_back(2);
l1.push_back(3);
l1.push_back(4);
l1.push_back(5);
l1.push_back(6);
//删除所有偶数
MyList::List<int>::iterator it = l1.begin();
while (it != l1.end())
{
if (it._node->_data % 2 == 0)
{
it = l1.erase(it);
//此时it指向的就是下一个迭代器,避免访问失效结点
}
else
{
++it;
}
}
MyList::print_container(l1);
}
int main()
{
test_list2();
return 0;
}

4、补充实现
4.1 析构 ~List() 和清除数据 clear()
cpp
//List.h
template<class T>
//链表的类结构
class List
{
public:
typedef List_iterator<T, T&> iterator;
typedef List_iterator<T, const T&> const_iterator;
void clear()
{
//上面实现的erase返回下一个迭代器在这里就起作用了
iterator it = begin();
while (it != end())
{
it = erase(it);
//通过返回下一个迭代器不仅避免了访问失效结点,还自带++it的功能
}
}
//析构
~List()
{
//析构链表空间之前需要清除链表所有数据
clear();
delete _head;
_head = nullptr;
}
private:
List_node<T>* _head; //哨兵位结点(头节点)
size_t _size = 0; //链表节点个数
};
//Test.cpp
void test_list3()
{
MyList::List<int> l1; //空链表
l1.push_back(1);
l1.push_back(2);
l1.push_back(3);
l1.push_back(4);
//拷贝构造
MyList::List<int> l2(l1);
MyList::print_container(l1);
MyList::print_container(l2);
}
int main()
{
test_list3();
return 0;
}

我们会发现当 l1 拷贝构造 l2 时程序报错了,原因在前面的学习其实已经讲过很多次了,就是因为链表的结点是 new 动态开辟出来的,所以拷贝构造需要进行深拷贝,否则拷贝构造后两者处于同一块空间,就会对同一块空间析构两次,所以解决方法就是手动实现拷贝构造函数。
4.2 拷贝构造
cpp
template<class T>
//链表的类结构
class List
{
public:
typedef List_iterator<T, T&> iterator;
typedef List_iterator<T, const T&> const_iterator;
//拷贝构造
l2(l1)
List(const List<T>& lt)
{
//初始化空链表l2
_head = new List_node<T>;
_head->_next = _head;
_head->_prev = _head;
_size = 0;
//复用push_back进行深拷贝
//push_back是new一个新空间与l2相连,保证l1和l2两者空间不同
for (auto e : lt)
{
push_back(e);
}
}
private:
List_node<T>* _head; //哨兵位结点(头节点)
size_t _size = 0; //链表节点个数
};
4.3 赋值重载=
同理,赋值重载和拷贝构造一样需要我们手动实现,否则通过编译器默认生成的浅拷贝就会导致程序报错:
cpp
template<class T>
//链表的类结构
class List
{
public:
typedef List_iterator<T, T&> iterator;
typedef List_iterator<T, const T&> const_iterator;
void swap(List<T> lt)
{
std::swap(_head, lt._head);
std::swap(_size, lt._size);
}
//赋值重载=
//l3 = l1
List<T>& operator=(List<T> lt)
{
if (this != <)
{
swap(lt);
}//使用现代写法更加方便
return *this;
//当出了函数形参被系统自动销毁,也就自带有将l3的旧空间进行销毁的功能
}
private:
List_node<T>* _head; //哨兵位结点(头节点)
size_t _size = 0; //链表节点个数
};
//Test.cpp
void test_list3()
{
MyList::List<int> l1; //空链表
l1.push_back(1);
l1.push_back(2);
l1.push_back(3);
l1.push_back(4);
//赋值重载=
MyList::List<int> l3;
l3 = l1;
MyList::print_container(l3);
}
int main()
{
test_list3();
return 0;

5、代码总览
由于 List 的模拟实现代码比较长所以我将全部实现代码的gitee链接放在下面,感兴趣的朋友可以看看:
- Gitee仓库:List 模拟实现代码
结束语
到此,List 常见接口的模拟实现就讲解完了,虽然篇幅比较长,但大部分接口的模拟实现和前面学习的vector和string本质没有太大区别,所以讲解起来都比较轻松,最需要重点掌握的就是List 迭代器的模拟实现 ,它不仅让我们对模板的有了更加深入的理解和学习 ,同时也让我们意识到迭代器不是之前学习vector和string那样单纯理解为原生指针 ,也会是其他如封装 List_node* 类的情况。希望这篇文章对大家学习C++能有所帮助!
C++参考文档:
https://legacy.cplusplus.com/reference/
https://zh.cppreference.com/w/cpp
https://en.cppreference.com/w/