文章目录
- 前言
- 一、list容器的模拟实现思路
-
- [1.1 模块分析](#1.1 模块分析)
- [1.2 作用分析](#1.2 作用分析)
- [1.3 注意事项](#1.3 注意事项)
- 二、list的节点类设计
- 三、list的迭代器类设计
-
- [3.1 存在的意义](#3.1 存在的意义)
- [3.2 模拟实现](#3.2 模拟实现)
-
- [3.2.1 模板参数和成员变量](#3.2.1 模板参数和成员变量)
- [3.2.2 构造函数](#3.2.2 构造函数)
- [3.2.3 ++运算符的重载](#3.2.3 ++运算符的重载)
- [3.2.4 --运算符的重载](#3.2.4 --运算符的重载)
- [3.2.5 重载!=和==](#3.2.5 重载!=和==)
- [3.2.6 * 运算符的重载](#3.2.6 * 运算符的重载)
- [3.2.7 ->运算符的重载](#3.2.7 ->运算符的重载)
- [3.2.8 迭代器类总体代码](#3.2.8 迭代器类总体代码)
- [3.2.9 补充知识点:迭代器分类](#3.2.9 补充知识点:迭代器分类)
- 四、list的模拟实现
-
- [4.1 成员变量和模板参数](#4.1 成员变量和模板参数)
- [4.2 默认成员函数](#4.2 默认成员函数)
-
- [4.2.1 构造函数](#4.2.1 构造函数)
- [4.2.2 拷贝构造](#4.2.2 拷贝构造)
- [4.2.3 迭代器区间构造](#4.2.3 迭代器区间构造)
- [4.2.4 n个相同元素构造](#4.2.4 n个相同元素构造)
- [4.2.5 赋值重载](#4.2.5 赋值重载)
- [4.2.6 析构函数](#4.2.6 析构函数)
- [4.3 迭代器相关函数](#4.3 迭代器相关函数)
-
- [4.3.1 begin和end](#4.3.1 begin和end)
- [4.4 访问容器相关函数](#4.4 访问容器相关函数)
-
- [4.4.1 front和back](#4.4.1 front和back)
- [4.5 增删查改相关函数](#4.5 增删查改相关函数)
-
- [4.5.1 insert](#4.5.1 insert)
- [4.5.2 erase](#4.5.2 erase)
- [4.5.3 push_back和pop_back](#4.5.3 push_back和pop_back)
- [4.5.4 push_front和pop_front](#4.5.4 push_front和pop_front)
- [4.6 容量相关函数](#4.6 容量相关函数)
-
- [4.6.1 size](#4.6.1 size)
- [4.6.2 empty](#4.6.2 empty)
- [4.6.3 resize](#4.6.3 resize)
- [4.6.4 clear](#4.6.4 clear)
- [4.6.5 swap](#4.6.5 swap)
- 五、list容器的模拟实现整体代码
-
- [5.1 list.h](#5.1 list.h)
- [5.2 list.cpp](#5.2 list.cpp)
前言
STL 中的 list 是一个带头双向循环链表,作为链表的终极形态,各项操作性能都很优秀,本文将会带大家一起从0~1 去模拟实现STL库中的 list 容器,以便于让大家更好的巩固之前学习过的 缺省参数、封装、类的6大默认函数等
一、list容器的模拟实现思路

1.1 模块分析
我们可以根据list容器图分析一下模拟实现list容器需要准备什么

- 存储元素需要节点-->节点类
- 使用迭代器访问节点-->迭代器类
- 总体-->list类
1.2 作用分析
1、节点类

作用:存储list容器的元素,因为list里面要存储各种类型的元素,所以节点类需要定义成模板。
节点类中的成员变量则有三个,分别是:指向前一个节点的_prev指针,指向后一个节点的_next指针,存储节点元素的_data变量
2、迭代器类
我们在实现vector类的时候,是直接用节点指针作为迭代器来使用的,并没有自己实现迭代器类,list中为什么要单独实现迭代器类?

原因:
- 如上图所示,vector容器是数组,他的空间是连续的,所以结点指针完全可以通过自增的方式来指向下一个链表节点
- 但是list容器是链表,它的空间并不连续,自然不可能直接通过节点指针的自增来指向下一个链表节点,所以我们才需要自己实现迭代器类,并且重载自增与自减运算符,这样就可以通过迭代器的自增或自减来指向前后节点了
3、list类

作用:实现链表各项功能的类,为主要部分
1.3 注意事项
节点类和迭代器类用struct,list类用class
这涉及到 C++ 中 struct 和 class 的区别以及设计哲学
1、struct 和 class 的区别
cpp
struct MyStruct {
int x; // 默认是 public
};
class MyClass {
int x; // 默认是 private
};
- struct: 成员默认是 public
- class: 成员默认是 private
2、为什么这样设计?
- 节点类 (ListNode) 用 struct
cpp
template<class T>
struct ListNode // 使用 struct
{
ListNode<T>* _prev; // 默认 public,可以直接访问
ListNode<T>* _next; // 默认 public
T _data; // 默认 public
ListNode(const T& x = T()) { ... }
};
原因:
- 节点是内部数据结构,主要用于存储数据
- 节点的成员需要被 list 类频繁访问
- 使用 struct 让所有成员默认 public,方便访问
- 节点类通常不需要复杂的封装和保护
- 迭代器类 (_list_iterator) 用 struct
cpp
template<typename T, typename Ref, typename Ptr>
struct _list_iterator // 使用 struct
{
typedef ListNode<T> Node;
Node* _node; // 默认 public
// 各种运算符重载...
};
原因:
- 迭代器也是辅助工具类
- 主要提供操作接口(运算符重载)
- _node 成员可能需要被 list 类访问
- 使用 struct 简化代码,减少 public/private 的声明
- list 类用 class
cpp
template<class T>
class list // 使用 class
{
public:
void push_back(const T& x); // 需要显式声明 public
// 用户可以使用的接口
private:
ListNode<T>* _head; // 默认 private,需要保护
size_t _size; // 内部数据,不能随意访问
};
原因:
- list 是对外提供服务的类,需要严格的封装
- 内部数据(如 _head、_size)需要保护,防止用户误操作
- 使用 class 默认 private,强制程序员明确哪些是公有接口
- 体现数据隐藏原则
3、设计准则
cpp
// 1. 纯数据结构 → 用 struct
struct Point {
int x;
int y;
};
// 2. 需要封装的类 → 用 class
class BankAccount {
private:
double balance; // 需要保护
public:
void deposit(double amount);
};
// 3. 内部辅助类 → 通常用 struct
struct Node {
int data;
Node* next;
};
在我们设计的链表中:
- ListNode (struct) - 简单的数据容器
- _list_iterator (struct) - 工具类
- list (class) - 需要封装保护的容器类
实际例子对比:
cpp
// 如果节点类用 class
template<class T>
class ListNode { // 用 class
private: // 默认 private
ListNode<T>* _prev;
ListNode<T>* _next;
T _data;
public: // 需要显式声明 public
ListNode(const T& x = T()) { ... }
// 还需要为 list 类提供访问接口
friend class list<T>; // 或者声明友元
};
这样代码会更复杂,所以选择 struct 更简洁
二、list的节点类设计
list本身 和 list的结点 是两个不同的结构,需要分开设计。以下是list的节点结构:

cpp
namespace xt
{
template<class T>
struct ListNode
{
ListNode<T>* _prev;//节点的前指针
ListNode<T>* _next;//节点的后指针
T _data; //节点的数据
ListNode(const T& x = T())
:_prev(nullptr)
, _next(nullptr)
, _data(x)
{
}
};
}
首先,我们在自己的命名空间内模拟实现 list (为了防止与库冲突),上面的代码就是list节点的结构。在这里是用并没有使用 class,因为 struct 默认访问权限是 public,又因为节点是需要经常访问的,所以使用struct更好
我们将这个类加上 template< class T> 后,就能够实现节点存储不同类型的数据,这也是C++模板 的好处
若构造结点时未传入数据,则默认以list容器所存储类型的默认构造函数所构造出来的值为传入数据
三、list的迭代器类设计
3.1 存在的意义
之前 模拟实现 string 和 vector 时都没有说要实现一个迭代器类,为什么实现list的时候就需要实现一个迭代器类了呢?
- 因为 string 和 vector 对象都将其数据存储在了一块连续的内存空间,我们通过指针进行自增、自减以及解引用等操作,就可以对相应位置的数据进行一系列操作,因此string和vector当中的迭代器就是原生指针

- 但是对于 list 来说,其各个结点在内存当中的位置是随机的,并不是连续的,我们不能仅通过结点指针的自增、自减以及解引用等操作对相应结点的数据进行操作

- 而迭代器的意义就是,让使用者可以不必关心容器的底层实现,可以用简单统一的方式对容器内的数据进行访问
- 既然 list 的结点指针的行为不满足迭代器定义,那么我们可以对这个结点指针进行封装,对结点指针的各种运算符操作进行重载,使得我们可以用和string和vector当中的迭代器一样的方式使用list当中的迭代器。例如,当你使用 list 当中的迭代器进行自增操作时,实际上执行了p = p->next语句,只是你不知道而已
- 迭代器模式的核心思想:迭代器作为容器元素的访问器,内部持有一个当前位置的指针/引用,通过重载运算符提供统一的访问接口
总结:list迭代器类,实际上就是对结点指针进行了封装,对其各种运算符进行了重载,使得结点指针的各种行为看起来和普通指针一样。(例如,对结点指针自增就能指向下一个结点)
3.2 模拟实现
3.2.1 模板参数和成员变量
我们为迭代器类 设置了 三个模板参数
cpp
template<class T, class Ref, class Ptr>
1、为什么有三个模板参数
- 在 list 的模拟实现当中,我们 typedef 了两个迭代器类型,普通迭代器 和 const迭代器
cpp
typedef _list_iterator<T, T&, T*> iterator;
typedef _list_iterator<T, const T&, const T*> const_iterator;
- 这里我们就可以看出,迭代器类的模板参数列表当中的 Ref 和 Ptr 分别代表的是 引用类型(T&) 和 指针类型(T *)
- 当我们使用 普通迭代器 时,编译器就会实例化出一个 普通迭代器 对象;当我们使用 const迭代器时,编译器就会实例化出一个 const迭代器对象
- 若该迭代器类不设计三个模板参数,那么就不能很好的区分-- 普通迭代器 和-- const迭代器
2、三个模板参数的作用
- 这三个参数分别是:T - 数据类型(如 int, string),Ref - 解引用操作符 operator*() 返回的类型,Ptr - 箭头操作符 operator->() 返回的类型
- 为什么要区分引用和指针类型
① operator*() 需要返回引用
cpp
Ref operator*() {
return _node->_data; // 返回类型是 Ref
}
- 对于 iterator: Ref = T&,返回 T&(可修改)
- 对于 const_iterator: Ref = const T&,返回 const T&(不可修改)
② operator->() 需要返回指针
cpp
Ptr operator->() {
return &(_node->_data); // 返回类型是 Ptr
}
- 对于 iterator: Ptr = T*,返回 T*(可修改)
- 对于 const_iterator: Ptr = const T*,返回 const T*(不可修改)
实际使用示例:
cpp
struct Person {
string name;
int age;
void print() { cout << name << endl; }
void const_print() const { cout << name << endl; }
};
// 使用 iterator(可修改)
xas_list::List<Person>::iterator it = people.begin();
(*it).name = "Alice"; // operator*() 返回 Person&
it->age = 25; // operator->() 返回 Person*
// 使用 const_iterator(不可修改)
xas_list::List<Person>::const_iterator cit = people.cbegin();
(*cit).name = "Bob"; // 编译错误!operator*() 返回 const Person&
cit->age = 30; // 编译错误!operator->() 返回 const Person*
3、typedef关键字 取别名
- 因为 结点类 和 迭代器类 自身的类型名太长,写起来太麻烦,所以我们用 typedef关键字 给这两个类型取了别名
- 我们为 结点类 的类型取的别名是 Node,为 迭代器类 取的别名是 Self
- 迭代器类 中的成员变量只有一个,那就是 结点类类型的指针 _node,因为 迭代器的本质就是指针
cpp
//迭代器类
template<typename T, typename Ref, typename Ptr>
struct _list_iterator
{
typedef ListNode<T> Node; //为节点类取别名
typedef _list_iterator<T, Ref, Ptr>self; //为正向迭代器类取别名
//成员变量
Node* _node; //指向节点的指针
};
3.2.2 构造函数
迭代器类实际上就是对结点指针进行了封装,其成员变量就只有一个,那就是结点指针,其构造函数直接根据所给节点指针构造一个迭代器对象即可
cpp
//正向迭代器构造函数
_list_iterator(Node* node = nullptr) //默认构造函数
:_node(node)
{
}
注意:迭代器类不需要写拷贝构造函数
迭代器只是一个指向list节点的指针,它不拥有那些节点,只是借用来访问而已,所以拷贝迭代器时只需要复制指针的值就行(浅拷贝),两个迭代器指向同一个节点完全没问题,因为迭代器析构时不会去释放节点内存。
而list才是真正拥有所有节点的对象,它负责创建和销毁节点,所以拷贝list时必须深拷贝,创建一套新的节点,否则两个list对象会指向同一块内存,当其中一个list析构释放内存后,另一个list就会访问到已经被删除的内存导致程序崩溃。简单说就是:迭代器是观察者不管内存,list是所有者负责内存
同时,如果迭代器做了深拷贝或者试图管理内存,就会和list冲突。因为:迭代器只是指针,它指向list里的节点,而list析构时会删除所有节点,如果迭代器也试图删除节点,就会double free(重复释放),导致程序崩溃,所以迭代器只能浅拷贝指针值,不能管理内存
3.2.3 ++运算符的重载
自增运算符的重载是迭代器类的核心
前置++重载中,要让当前迭代器指向下一个节点后,再把迭代器返回
后置++中,是把当前迭代器用临时变量保存一份,再把迭代器指向下一个节点,然后返回临时变量
注意:重载后置++或后置--时,必须在函数参数列表加一个int变量,这是语法规定
- 重载前置++和后置++时的返回值有所不同,前置++返回值类型是------迭代器类型的引用,而后置++返回值类型是------迭代器类型
- 前置++中,返回的是对this的解引用 ,this并不是局部变量,函数结束后依然存在,所以可以返回它的引用,减少值拷贝次数
- 后置++中,返回的temp是函数中创建的局部对象 ,在函数结束后会被销毁,所以返回值类型不可以是引用,这里就必须通过值拷贝来返回值
- 建议尽量使用前置++(++it)而不是后置++(it++),因为前置++直接修改后返回自己,不需要拷贝,后置++需要先保存旧值,再修改,再返回旧值,多了一次拷贝开销,对于内置类型(int、指针等)两者性能一样,但对于自定义类型(迭代器这种类类型),前置++效率更高
cpp
//重载前置++
//返回迭代器对象自身的引用
//因为对象自身并不是该函数中的局部对象
self& operator++()
{
_node = _node->_next;
return *this;
}
//重载后置++
//此时需要返回temp对象,而不是引用
//因为temp对象是局部的对象,函数结束后就被释放
self operator++(int a)
{
self temp(*this);
_node = _node->_next;
return temp;
}
3.2.4 --运算符的重载
前置--和后置--关于函数的返回类型跟重载++类似,这里就不再赘述
cpp
//重载前置--
self& operator--()
{
_node = _node->_prev;
return *this;
}
//重载后置--
self operator--(int a)
{
self temp = (*this);
_node = _node->_prev;
return temp;
}
3.2.5 重载!=和==
这里只需要比较_node是否相同即可,因为_node本身就是指向结点的指针,保存着结点的地址,只要地址相同,那自然就是同一个结点了
cpp
//重载!=
bool operator!=(const self& s)const
{
return _node != s._node;
}
//重载==
bool operator!=(const self& s)const
{
return _node == s._node;
}
3.2.6 * 运算符的重载
当我们使用 解引用操作符 时,是想得到该位置的数据内容。因此,我们直接返回当前结点指针所指结点的数据即可,但是这里需要使用引用返回,因为解引用后可能需要对数据进行修改
cpp
//重载*,返回迭代器指向的节点的值域
//T& operator*()
Ref operator*()
{
return _node->_data;
}
3.2.7 ->运算符的重载
有时候,实例化的模板参数是自定义类型 ,我们想要像 指针 一样访问访问自定义类型力的成员变量,这样显得更通俗易懂,所以就要重载 -> 运算符,它的返回值是 T*
我们来看一个场景:
当 list容器 当中的每个结点存储的不是内置类型,而是自定义类型,例如数据存储类,那么当我们拿到一个位置的迭代器时,我们可能会使用 ->运算符访问 Data 的成员:
cpp
#include<iostream>
#include<list>
using namespace std;
struct Data
{
Data(int a = int(), double b = double(), char c = char())
:_a(a)
, _b(b)
, _c(c)
{}
int _a;
double _b;
char _c;
};
void TestList()
{
list<Data> lt;
lt.push_back(Data(1, 2.2, 'A'));
auto it = lt.begin();
cout << (*it)._a << endl; //不使用 operator->() 比较别扭
cout << it.operator->()->_b << endl; //这种写法是真实调用情况
cout << it->_c << endl; //编译器直接优化为 it->
}
int main()
{
TestList();
return 0;
}

- operator->() 存在的意义:使得 迭代器 访问自定义类型中的成员时更加方便
- 如果没有这个函数,只能通过 (*迭代器).成员 的方式进行成员访问,很不方便
- 注意: 编译器将 迭代器.operator->()->成员 直接优化为 迭代器->成员
- 对于 -> 运算符的重载,我们直接返回结点当中所存储数据的地址即可
cpp
//重载 -> 操作符---实现指针访问元素
//T* operator->()
Ptr operator->()
{
return &_node->_data;
}
3.2.8 迭代器类总体代码
cpp
//迭代器类
template<typename T, typename Ref, typename Ptr>
struct _list_iterator
{
typedef ListNode<T> Node; //为节点类取别名
typedef _list_iterator<T, Ref, Ptr> self; //为正向迭代器类取别名
//成员变量
Node* _node; //指向节点的指针
//正向迭代器构造函数
_list_iteartor(Node* node = nullptr) //默认构造函数
:_node(node)
{
}
//重载前置++
//返回迭代器对象自身的引用
//因为对象自身并不是该函数中的局部对象
self& operator++()
{
_node = _node->_next;
return *this;
}
//重载后置++
//此时需要返回temp对象,而不是引用
//因为temp对象是局部的对象,函数结束后就被释放
self operator++(int a)
{
self temp(*this);
_node = _node->_next;
return temp;
}
//重载前置--
self& operator--()
{
_node = _node->_prev;
return *this;
}
//重载后置--
self operator--(int a)
{
self temp = (*this);
_node = _node->_prev;
return temp;
}
//重载!=
bool operator!=(const self& s)const
{
return _node != s._node;
}
//重载==
bool operator==(const self& s)const
{
return _node == s._node;
}
//重载*,返回迭代器指向的节点的值域
//T& operator*()
Ref operator*()
{
return _node->_data;
}
//重载 -> 操作符---实现指针访问元素
//T* operator->()
Ptr operator->()
{
return &_node->_data;
}
};
3.2.9 补充知识点:迭代器分类
- 单向迭代器(Forward Iterator)
支持操作: ++(只能向前)
对应容器:forward_list(单向链表),unordered_map / unordered_set / unordered_multimap / unordered_multiset(哈希表)
原因:单向链表只有指向下一个节点的指针,哈希表的元素位置由哈希函数决定,没有明确的"前后"关系
- 双向迭代器(Bidirectional Iterator)
支持操作: ++ 和 --(可前进和后退)
对应容器:list(双向链表),map / set / multimap / multiset(红黑树)
原因:双向链表有前驱和后继指针,红黑树节点可以找到前驱和后继
cpp
list<int> lt = {1, 2, 3, 4};
auto it = lt.begin();
++it; // ✓ 可以前进
--it; // ✓ 可以后退
// it + 2; // ✗ 不能随机访问
- 随机访问迭代器(Random Access Iterator)
支持操作: ++ / -- / + / - / [] / < / >
对应容器:vector(动态数组),string(字符串),deque(双端队列),array(数组)
原因:底层是连续内存或类似连续的结构,可以通过指针运算直接访问任意位置
cpp
vector<int> v = {1, 2, 3, 4, 5};
auto it = v.begin();
it = it + 3; // ✓ 直接跳转到第4个元素
int x = it[2]; // ✓ 随机访问
if (it < v.end()) { ... } // ✓ 比较大小
四、list的模拟实现
上面我们对 节点结构、正向迭代器实现原理及注意点一一做了介绍,最后一步也是最重要的一步,那就是将list结构完善起来,实现list的功能
4.1 成员变量和模板参数
- 因为 list 可以存储各种类型的元素,因此 list 类要设置为模板,T就是存储的元素的类型
- 因为 结点类 和 迭代器类 的类名太长,所以用 typedef 关键为它们取了别名。这里迭代器的三个参数之所以设置为<T , T& , T*>,是因为list类只给出了一个模板参数,而迭代器类应该有三个,因此用 T& 和 T* 作为另外两个参数
cpp
//带头节点的双向链表
template<class T>
class list
{
typedef ListNode<T> Node;
public:
//正向迭代器
typedef _list_iterator<T, T&, T*>iterator;
typedef _list_iterator<T, const T&, constT*>const_iterator;
private:
Node* _head; //指向头结点的指针
};
注意:
- Node 别名 → private
cpp
private:
typedef ListNode<T> Node; // 内部使用,外部不可见
原因:
- Node 是 list 类的实现细节
- 用户不需要知道链表内部是用什么节点实现的
- 只在 list 类内部使用(如 Node* _head;)
- 体现信息隐藏原则
错误示例(如果放在 public):
cpp
// 用户可以这样写,但这是不应该的!
list<int>::Node* myNode = new list<int>::Node(10);
// 用户不应该直接操作节点
- Iterator 和 const_Iterator → public
cpp
public:
typedef _list_iterator<T, T&, T*> iterator;
typedef _list_iterator<T, const T&, const T*> const_iterator;
原因:
- 迭代器类型是对外接口的一部分
- 用户必须使用迭代器来遍历链表
- STL 容器都将迭代器类型公开
- 若是搞反了
- ❌ 错误做法 1:把 iterator 放 private
cpp
class list
{
private:
typedef _list_iterator<T, T&, T*> iterator; // 错误!
public:
iterator begin(); // 编译错误!iterator 不可访问
};
// 用户代码
list<int> lt;
list<int>::iterator it = lt.begin(); // 编译错误!无法访问 iterator 类型
- ❌ 错误做法 2:把 Node 放 public
cpp
class list
{
public:
typedef ListNode<T> Node; // 不好的设计
private:
Node* _head;
};
// 用户可能这样乱搞(破坏封装)
list<int> lt;
list<int>::Node* badNode = new list<int>::Node(999);
// 用户直接操作节点,绕过了 list 的管理,可能导致内存泄漏或崩溃!
4.2 默认成员函数
4.2.1 构造函数
list的成员变量是一个节点类,在构造头节点时,需要将这单个头结点构造成一个双向循环链表

cpp
//构造函数
list()
{
_head = new Node; //new一个节点
_head->_prev = _head;
_head->_next = _head;//_prev和_next同时指向了头结点,形成了双向循环链表
}
4.2.2 拷贝构造
拷贝构造是用一个已有对象去构造出另一个对象,首先将待构造对象进行初始化,然后利用迭代器区间去构造一个和 lt1 一样的临时的 tmp 对象,再进行数据的交换,达到深拷贝的目的

cpp
//拷贝构造----现代写法 lt2(lt1)
list(const list<T>& lt)
{
_head = new Node;
_head->_prev = _head;
_head->_next = _head;
list<T> tmp(lt.begin(), lt.end());
std::swap(_head, tmp._head);
}
4.2.3 迭代器区间构造
由于list可以存储各种类型的元素,所以区间构造时自然也会用到各种类型的迭代器,因此区间构造也应该定义为模版,需要给出模版参数列表。具体实现和上一个函数是差不多的
cpp
//迭代器区间构造
template<class InputIterator >
//命名InputIterator避免与typedef的iterator冲突
list(InputIterator first, InputIterator last)
{
_head = new Node;
_head->_prev = _head;
_head->_next = _head;
while (first != last)
{
push_back(*first); //尾插数据,会根据不同类型的迭代器进行调用
++first;
}
}
4.2.4 n个相同元素构造
通过用 n 个 val 来对对象进行初始化,需要注意这里的 T( )是一个匿名对象,作为 val 的缺省参数,因为我们并不知道传给val的是一个对象还是一个整形(或其他),给缺省参数的好处在于,对于自定义类型编译器会去调用自定义类型的构造函数来对val进行初始化,如果是内置类型,它也是有自己的构造函数
cpp
//用n个val进行构造
list(int n, const T& val = T())
{
_head = new Node;
_head->_prev = _head;
_head->_next = _head;
for (int i = 0; i < n; i++)
{
push_back(val);
}
}
T() 表示创建一个 T 类型的临时对象,其中:
- T 是你的模板参数(比如 int、string、double 等)
- 括号 () 表示调用该类型的默认初始化
这样写的目的是:
- 如果用户传了 val:用用户传的值
- 如果用户没传 val:用 T() 这个默认值
具体例子:
cpp
// 假设 T = int
list<int> mylist1(5); // 相当于 list(5, int())
// int() = 0
// 创建5个值为0的节点
list<int> mylist2(5, 100); // 创建5个值为100的节点
// 假设 T = string
list<string> mylist3(3); // 相当于 list(3, string())
// string() = ""(空字符串)
// 创建3个空字符串节点
list<string> mylist4(3, "hello"); // 创建3个值为"hello"的节点
不同类型的 T() 表现
| 类型 | T() 的结果 |
|---|---|
| int | 0 |
| double | 0.0 |
| char | '\0' |
| string | ""(空字符串) |
| vector< int > | 空的vector |
| 自定义类型 | 调用默认构造函数 |
cpp
// 匿名对象(临时对象)
int(); // 创建了一个int对象,但没有名字
string(); // 创建了一个string对象,但没有名字
T(); // 创建了一个T类型对象,但没有名字
4.2.5 赋值重载
传统写法:将赋值运算符重载的参数定义为 list 类型对象的常引用,首先判断当前对象的地址和参数对象的地址是否相同来避免自己给自己赋值的情况。
如果不是自赋值,则先调用 clear 函数清空当前对象中的所有节点并释放内存,然后使用范围 for 循环遍历参数对象 lt 中的每个元素,通过 push_back 函数将这些元素一个个尾插到当前对象的链表后面,从而完成深拷贝。最后返回当前对象的引用以支持连续赋值操作。
这种写法的特点是思路清晰直观,通过显式的清空和逐个拷贝来完成赋值,但需要手动处理自赋值检查和资源管理
cpp
//传统写法
list<T>& operator=(const list<T>& lt)
{
if (this != <)//避免自己给自己赋值
{
clear();//清空容器
for (const auto& e : lt)
{
push_back(e);//将容器lt当中的数据一个一个尾插到链表后面
}
return *this;//支持连续赋值
}
}
现代写法:
将赋值运算符重载的参数定义为 list 类型的对象而不是对象的引用,传参时会自动调用拷贝构造函数创建一个临时副本。
接着我们调用 swap 函数交换当前对象(this)和参数对象(lt)的内部成员(头指针和size等),交换后当前对象就拥有了参数副本的数据,而参数对象lt则持有了当前对象原来的旧数据。当函数结束时,作为局部对象的参数lt会自动析构,它持有的旧数据也随之被释放,这样就完成了赋值操作。
这种写法的巧妙之处在于利用了拷贝构造、swap交换和自动析构三个机制,避免了手动检查自赋值、手动清空和手动拷贝数据的繁琐操作,代码更简洁且自动异常安全
cpp
void swap(list<T>& lt)
{
std::swap(_head, lt._head); // 交换头指针(哨兵节点指针)
}
// 现代写法(拷贝交换法)
list<T>& operator=(list<T> lt) // 参数是对象,不是引用
{
swap(lt); // 交换当前对象和参数对象的内容
return *this; // 支持连续赋值
}
注意:类名和类型
cpp
list& operator= (const list& x);
list<T>& operator=(list<T> lt)
list 是类名(class name),在类模板内部可以省略模板参数,直接使用类名 list 来代表当前类
list 是类型(type),这是完整的模板类型写法,明确包含了模板参数
关键区别:
cpp
template<class T>
class list {
// 在类内部:
list& operator=(const list& x); // ✓ 可以省略<T>,list就是类名
list<T>& operator=(const list<T>& x); // ✓ 也可以写完整,更明确(推荐)
};
// 在类外部:
template<class T>
list<T>& list<T>::operator=(const list<T>& x) { // 必须写完整的 list<T>
// ...
}
总结:
- 类名:list(简写形式,仅在类内部有效)
- 类型:list(完整形式,任何地方都适用)
- 类模板在类里面写的时候既可以写类名也可以写类型,我们推荐写类型,但是在类模板以外要写完整类型,不能只写类名
4.2.6 析构函数
对对象进行析构时,首先调用clear函数清理容器当中的数据,然后将头结点释放,最后将头指针置空即可
cpp
//析构函数
~list()
{
clear();//清理容器
delete _head;//释放头结点
_head = nullptr;//头指针置空
}
4.3 迭代器相关函数
4.3.1 begin和end
首先我们应该明确的是:begin 函数返回的是第一个有效数据的迭代器,end函数返回的是最后一个有效数据的下一个位置的迭代器。
对于 list 这个 带头双向循环链表 来说,其第一个有效数据的迭代器就是使用头结点后一个结点的地址构造出来的迭代器,而其最后一个有效数据的下一个位置的迭代器就是使用头结点的地址构造出来的迭代器。(最后一个结点的下一个结点就是头结点)
cpp
iterator begin()
{
//返回使用头结点后一个结点的地址构造出来的普通迭代器,显式调用构造函数,直接用 Node* 构造一个 iterator 对象
return iterator(_head->_next);
//return _head->_next;//这个写法也是可以的,单参数的构造函数支持隐式类型转换,编译器自动调用构造函数
//如果构造函数用 explicit 修饰,那么第二种写法会编译错误,只能用第一种显式调用的方式,加上 explicit,禁止隐式转换
}
iterator end()
{
//返回使用头结点的地址构造出来的普通迭代器
return iterator(_head);
}
同时,我们还需要重载一对用于const对象的begin函数和end函数
cpp
const_iterator begin()const
{
//返回使用头结点后一个结点的地址构造出来的const迭代器,显式调用构造函数,直接用 Node* 构造一个 const_iterator 对象
return const_iterator(_head->_next);
}
const_iterator end()const
{
//返回使用头结点的地址构造出来的const迭代器
return const_iterator(_head);
}
注意:const的修饰位置问题
- const T ptr(或const T ptr1):
- 指针指向的内容是const,不能通过这个指针修改内容
- 但指针本身可以改变,可以指向别的地方
cpp
const int* ptr = &a;
*ptr = 10; // 错误!不能修改指向的值
ptr = &b; // 正确!可以改变指针指向
- T const ptr(或T const ptr2):
- 指针本身是const,不能改变指向
- 但可以通过指针修改指向的内容
cpp
int* const ptr = &a;
*ptr = 10; // 正确!可以修改指向的值
ptr = &b; // 错误!不能改变指针指向
- const T const ptr:
- 指针和内容都是const,什么都不能改
4.4 访问容器相关函数
4.4.1 front和back
front 和 back 函数分别用于获取第一个有效数据和最后一个有效数据,因此,实现front和back函数时,直接返回第一个有效数据和最后一个有效数据的引用即可
cpp
//获取第一个有效数据
T& front()
{
return *begin();//返回第一个有效数据的引用
}
T& back()
{
return*(--end());//返回最后一个有效数据的引用
}
同时,我们也需要重载一对用于const对象的front函数和back函数,因为const对象调用front和back函数后所得到的数据不能被修改
cpp
//获取第一个有效数据
const T& front() const
{
return *begin();//返回第一个有效数据的const引用
}
const T& back() const
{
return*(--end());//返回最后一个有效数据的const引用
}
4.5 增删查改相关函数
4.5.1 insert
insert函数可以在所给迭代器之前插入一个新结点

先根据所给迭代器得到该位置处的结点指针cur,然后通过cur指针找到前一个位置的结点指针prev,接着根据所给数据x构造一个待插入结点,之后再建立新结点与cur之间的双向关系,最后建立新结点与prev之间的双向关系即可
cpp
//插入函数
void insert(iterator pos, const T& x)
{
assert(pos._node);//检测pos的合法性
Node* cur = pos._node;//迭代器pos处的结点指针
Node* prev = cur->_prev;//迭代器pos前一个位置的结点指针
Node* newnode = new Node(x);//根据所给数据x构造一个待插入结点
//建立newnode与cur之间的双向关系
newnode->_next = cur;
cur->_prev = newnode;
//建立newnode与prev之间的双向关系
newnode->_prev = prev;
prev->_next = newnode;
}
4.5.2 erase
先根据所给迭代器得到该位置处的结点指针cur,然后通过cur指针找到前一个位置的结点指针prev,以及后一个位置的结点指针next,紧接着释放cur结点,最后建立prev和next之间的双向关系即可

cpp
//删除函数
iterator erase(iterator pos)
{
assert(pos._node);//检测pos的合法性
assert(pos != end());//删除的节点不能是头结点
Node* cur = pos._node;//迭代器pos处的节点指针
Node* prev = cur->_prev;
Node* next = cur->_next;
delete cur;//释放cur节点
//建立prev与next之间的双向关系
prev->_next = next;
next->_prev = prev;
// 函数要求返回 iterator 对象,不是 node* 指针
return iterator(next); // 用构造函数把指针包装成迭代器,进行类型转换
}
注意:next 是原始指针,而函数需要返回迭代器对象。iterator(next) 就是调用构造函数,把指针包装成迭代器,这是 C++ 类型系统和封装设计的体现!
优点:
✅ 隐藏实现细节:用户不需要知道内部是指针
✅ 统一接口:所有容器的迭代器用法一致
✅ 类型安全:防止错误操作
✅ 扩展性好:可以添加额外功能(如越界检查)
cpp
iterator erase(iterator pos)
{
node* next = cur->_next;
return iterator(next);
// ✅ 调用 iterator 的构造函数
// 把 node* 指针包装成 iterator 对象
}
图解转换过程
┌─────────────────────┐
│ erase() 函数 │
├─────────────────────┤
│ node* next = ... │ ← next 是裸指针(node*)
│ │
│ return iterator(next) │
│ ↓ │
│ 调用构造函数 │
└─────────────────────┘
↓
┌─────────────────────┐
│ iterator 对象 │
├─────────────────────┤
│ _node = next │ ← 迭代器封装了指针
└─────────────────────┘
4.5.3 push_back和pop_back
push_back函数就是在头结点前插入结点,而pop_back就是删除头结点的前一个结点
在已经实现了insert和erase函数的情况下,我们可以通过复用函数来实现push_back和pop_back函数
cpp
//尾插,在头结点前插入结点
void push_back(const T& x)
{
insert(end(), x);
}
//尾删,删除头结点的前一个结点
void pop_back()
{
erase(--end());
}
4.5.4 push_front和pop_front
push_front函数就是在第一个有效结点前插入结点,而pop_front就是删除第一个有效结点
当然,用于头插和头删的push_front和pop_front函数也可以复用insert和erase函数来实现
cpp
//头插,在第一个有效结点前插入结点
void push_front(const T& x)
{
insert(begin(), x);
}
//头删,删除第一个有效结点
void pop_front()
{
erase(begin());
}
4.6 容量相关函数
4.6.1 size
size函数用于获取当前容器中的有效数据个数,因为list是链表,所以只能通过遍历的方式逐个统计有效数据的个数
cpp
//获取容器的有效数据个数
size_t size() const
{
size_t sz = 0;//统计有效数据个数
const_iterator it = begin();//获取第一个有效数据的迭代器
while (it != end())
{
sz++;
it++;
}
return sz;//返回有效数据的个数
}
扩展: 其实也可以给list多设置一个成员变量size,用于记录当前容器内的有效数据个数
4.6.2 empty
empty函数用于判断容器是否为空,我们直接判断该容器的begin函数和end函数所返回的迭代器,是否是同一个位置的迭代器即可。(此时说明容器当中只有一个头结点)
cpp
//判断容器是否为空
bool empty() const
{
return begin() == end();//判断是否只有头结点
}
4.6.3 resize
规则:
- 若当前容器的size小于所给n,则尾插结点,直到size等于n为止
- 若当前容器的size大于所给n,则只保留前n个有效数据
注意:实现resize函数时,不要直接调用size函数获取当前容器的有效数据个数,因为当你调用size函数后就已经遍历了一次容器了,而如果结果是size大于n,那么还需要遍历容器,找到第n个有效结点并释放之后的结点
实现方法:设置一个变量len,用于记录当前所遍历的数据个数,然后开始变量容器,在遍历过程中:
- 当len大于或是等于n时遍历结束,此时说明该结点后的结点都应该被释放,将之后的结点释放即可
- 当容器遍历完毕时遍历结束,此时说明容器当中的有效数据个数小于n,则需要尾插结点,直到容器当中的有效数据个数为n时停止尾插即可
cpp
//resize函数实现
void resize(size_t n, const T& val = T())
{
iterator i = begin();//获取第一个有效数据的迭代器
size_t len = 0; //记录当前所遍历的数据个数
while (len < n && i != end())
{
len++;
i++;
}
if (len == n) //说明容器当中的有效数据个数大于或是等于n
{
while (i != end()) //只保留前n个有效数据
{
i = erase(i); //每次删除后接收下一个数据的迭代器
}
}
else //说明容器当中的有效数据个数小于n
{
while (len < n) //尾插数据为val的结点,直到容器当中的有效数据个数为n
{
push_back(val);
len++;
}
}
}
4.6.4 clear
clear函数用于清空容器,我们通过遍历的方式,逐个删除结点,只保留头结点即可
cpp
//清空函数
void clear()
{
iterator it = begin();
//逐个删除结点,,只保留头结点
while (it != end())
{
it = erase(it);
}
}
4.6.5 swap
swap函数用于交换两个容器,list容器当中存储的实际上就只有链表的头指针,我们将这两个容器当中的头指针交换即可
cpp
void swap(list<T>& lt)
{
std::swap(_head, lt._head); // 交换头指针(哨兵节点指针)
}
注意: 在此处调用库当中的swap函数需要在swap之前加上"::"(作用域限定符),告诉编译器这里优先在全局范围寻找swap函数,否则编译器会认为你调用的就是你正在实现的swap函数(就近原则)
五、list容器的模拟实现整体代码
5.1 list.h
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include<assert.h>
namespace xt
{
//节点类
template<class T>
struct ListNode
{
ListNode<T>* _prev;//节点的前指针
ListNode<T>* _next;//节点的后指针
T _data; //节点的数据
ListNode(const T& x = T())
:_prev(nullptr)
, _next(nullptr)
, _data(x)
{
}
};
//迭代器类
template<typename T, typename Ref, typename Ptr>
struct _list_iterator
{
typedef ListNode<T> Node; //为节点类取别名
typedef _list_iterator<T, Ref, Ptr> self; //为正向迭代器类取别名
//成员变量
Node* _node; //指向节点的指针
//正向迭代器构造函数
_list_iterator(Node* node = nullptr) //默认构造函数
:_node(node)
{
}
//重载前置++
//返回迭代器对象自身的引用
//因为对象自身并不是该函数中的局部对象
self& operator++()
{
_node = _node->_next;
return *this;
}
//重载后置++
//此时需要返回temp对象,而不是引用
//因为temp对象是局部的对象,函数结束后就被释放
self operator++(int a)
{
self temp(*this);
_node = _node->_next;
return temp;
}
//重载前置--
self& operator--()
{
_node = _node->_prev;
return *this;
}
//重载后置--
self operator--(int a)
{
self temp = (*this);
_node = _node->_prev;
return temp;
}
//重载!=
bool operator!=(const self& s)const
{
return _node != s._node;
}
//重载==
bool operator==(const self& s)const
{
return _node == s._node;
}
//重载*,返回迭代器指向的节点的值域
//T& operator*()
Ref operator*()
{
return _node->_data;
}
//重载 -> 操作符---实现指针访问元素
//T* operator->()
Ptr operator->()
{
return &_node->_data;
}
};
//带头节点的双向链表
template<class T>
class list
{
private:
typedef ListNode<T> Node;
public:
//正向迭代器
typedef _list_iterator<T, T&, T*>iterator;
typedef _list_iterator<T, const T&, const T*>const_iterator;
iterator begin()
{
//返回使用头结点后一个结点的地址构造出来的普通迭代器,显式调用构造函数,直接用 Node* 构造一个 iterator 对象
return iterator(_head->_next);
}
iterator end()
{
//返回使用头结点的地址构造出来的普通迭代器
return iterator(_head);
}
const_iterator begin()const
{
//返回使用头结点后一个结点的地址构造出来的const迭代器,显式调用构造函数,直接用 Node* 构造一个 const_iterator 对象
return const_iterator(_head->_next);
}
const_iterator end()const
{
//返回使用头结点的地址构造出来的const迭代器
return const_iterator(_head);
}
//构造函数
list()
{
_head = new Node; //new一个节点
_head->_prev = _head;
_head->_next = _head;//_prev和_next同时指向了头结点,形成了双向循环链表
}
//拷贝构造----现代写法 lt2(lt1)
list(const list<T>& lt)
{
_head = new Node;
_head->_prev = _head;
_head->_next = _head;
list<T> tmp(lt.begin(), lt.end());
std::swap(_head, tmp._head);
}
//迭代器区间构造
template<class InputIterator >
//命名InputIterator避免与typedef的iterator冲突
list(InputIterator first, InputIterator last)
{
_head = new Node;
_head->_prev = _head;
_head->_next = _head;
while (first != last)
{
push_back(*first); //尾插数据,会根据不同类型的迭代器进行调用
++first;
}
}
//用n个val进行构造
list(int n, const T& val = T())
{
_head = new Node;
_head->_prev = _head;
_head->_next = _head;
for (int i = 0; i < n; i++)
{
push_back(val);
}
}
//赋值重载
//传统写法
//list<T>& operator=(const list<T>& lt)
//{
// if (this != <)//避免自己给自己赋值
// {
// clear();//清空容器
// for (const auto& e : lt)
// {
// push_back(e);//将容器lt当中的数据一个一个尾插到链表后面
// }
// return *this;//支持连续赋值
// }
//}
void swap(list<T>& lt)
{
std::swap(_head, lt._head); // 交换头指针(哨兵节点指针)
}
// 现代写法(拷贝交换法)
list<T>& operator=(list<T> lt) // 参数是对象,不是引用
{
swap(lt); // 交换当前对象和参数对象的内容
return *this; // 支持连续赋值
}
//析构函数
~list()
{
clear();//清理容器
delete _head;//释放头结点
_head = nullptr;//头指针置空
}
//清空函数
void clear()
{
iterator it = begin();
//逐个删除结点,,只保留头结点
while (it != end())
{
it = erase(it);
}
}
//插入函数
void insert(iterator pos, const T& x)
{
assert(pos._node);//检测pos的合法性
Node* cur = pos._node;//迭代器pos处的结点指针
Node* prev = cur->_prev;//迭代器pos前一个位置的结点指针
Node* newnode = new Node(x);//根据所给数据x构造一个待插入结点
//建立newnode与cur之间的双向关系
newnode->_next = cur;
cur->_prev = newnode;
//建立newnode与prev之间的双向关系
newnode->_prev = prev;
prev->_next = newnode;
}
//删除函数
iterator erase(iterator pos)
{
assert(pos._node);//检测pos的合法性
assert(pos != end());//删除的节点不能是头结点
Node* cur = pos._node;//迭代器pos处的节点指针
Node* prev = cur->_prev;
Node* next = cur->_next;
delete cur;//释放cur节点
//建立prev与next之间的双向关系
prev->_next = next;
next->_prev = prev;
// 函数要求返回 iterator 对象,不是 node* 指针
return iterator(next); //返回所给迭代器pos的下一个迭代器
// 用构造函数把指针包装成迭代器,进行类型转换
}
//尾插,在头结点前插入结点
void push_back(const T& x)
{
insert(end(), x);
}
//尾删,删除头结点的前一个结点
void pop_back()
{
erase(--end());
}
//头插,在第一个有效结点前插入结点
void push_front(const T& x)
{
insert(begin(), x);
}
//头删,删除第一个有效结点
void pop_front()
{
erase(begin());
}
//获取第一个有效数据
T& front()
{
return *begin();//返回第一个有效数据的引用
}
T& back()
{
return*(--end());//返回最后一个有效数据的引用
}
//获取第一个有效数据
const T& front() const
{
return *begin();//返回第一个有效数据的const引用
}
const T& back() const
{
return*(--end());//返回最后一个有效数据的const引用
}
//获取容器的有效数据个数
size_t size() const
{
size_t sz = 0;//统计有效数据个数
const_iterator it = begin();//获取第一个有效数据的迭代器
while (it != end())
{
sz++;
it++;
}
return sz;//返回有效数据的个数
}
//判断容器是否为空
bool empty() const
{
return begin() == end();//判断是否只有头结点
}
//resize函数实现
void resize(size_t n, const T& val = T())
{
iterator i = begin();//获取第一个有效数据的迭代器
size_t len = 0; //记录当前所遍历的数据个数
while (len < n && i != end())
{
len++;
i++;
}
if (len == n) //说明容器当中的有效数据个数大于或是等于n
{
while (i != end()) //只保留前n个有效数据
{
i = erase(i); //每次删除后接收下一个数据的迭代器
}
}
else //说明容器当中的有效数据个数小于n
{
while (len < n) //尾插数据为val的结点,直到容器当中的有效数据个数为n
{
push_back(val);
len++;
}
}
}
private:
Node* _head; //指向头结点的指针
};
}
5.2 list.cpp
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<list>
#include"list.h"
using namespace std;
//struct Data
//{
// Data(int a = int(), double b = double(), char c = char())
// :_a(a)
// , _b(b)
// , _c(c)
// {}
//
// int _a;
// double _b;
// char _c;
//};
//
//void TestList()
//{
// list<Data> lt;
// lt.push_back(Data(1, 2.2, 'A'));
//
// auto it = lt.begin();
// cout << (*it)._a << endl; //不使用 operator->() 比较别扭
// cout << it.operator->()->_b << endl; //这种写法是真实调用情况
// cout << it->_c << endl; //编译器直接优化为 it->
//}
//
//int main()
//{
// TestList();
// return 0;
//}
// 遍历的测试
void test1()
{
xt::list<int> lt(5, 6);
// -------------迭代器测试------------------//
cout << "迭代器的测试" << endl;
xt::list<int>::iterator it = lt.begin();
while (it != lt.end())
{
cout << *it << " ";
++it;
}
cout << endl;
//尾插
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(5);
it = lt.begin();
while (it != lt.end())
{
cout << *it << " ";
++it;
}
cout << endl;
// 头插
lt.push_front(0);
it = lt.begin();
while (it != lt.end())
{
cout << *it << " ";
++it;
}
cout << endl;
// 尾删
lt.pop_back();
it = lt.begin();
while (it != lt.end())
{
cout << *it << " ";
++it;
}
cout << endl;
//头删
lt.pop_front();
it = lt.begin();
while (it != lt.end())
{
cout << *it << " ";
++it;
}
cout << endl;
cout << "范围for的测试" << endl;
// -------------范围 for 测试------------------//
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
cout << "清空数据" << endl;
// 清空数据
lt.clear();
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
}
void test2()
{
xt::list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(5);
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
cout << "拷贝构造" << endl;
xt::list<int> copy(lt);
for (auto e : copy)
{
cout << e << " ";
}
cout << endl;
cout << "赋值重载" << endl;
xt::list<int> lt1;
lt1.push_back(10);
lt1.push_back(20);
lt1.push_back(33);
lt = lt1;
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
}
// const 迭代器
void test3()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
xt::list<int> lt(arr, arr + 10); //迭代器区间构造
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
cout << lt.size() << endl;
cout << lt.empty() << endl;
lt.resize(12, 2);
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
cout << lt.back() << endl;
cout << lt.front() << endl;
}
int main()
{
test3();
return 0;
}