目录
[1. list的私有成员](#1. list的私有成员)
[2. 构造函数](#2. 构造函数)
[2.1 list构造函数](#2.1 list构造函数)
[3. list遍历](#3. list遍历)
[3.1 push_back](#3.1 push_back)
[3.2 ListIterator模拟实现](#3.2 ListIterator模拟实现)
[3.2.1 成员变量_node](#3.2.1 成员变量_node)
[3.2.2 it++ 和 ++it](#3.2.2 it++ 和 ++it)
[3.2.3 it-- 和 --it](#3.2.3 it-- 和 --it)
[3.2.4 *it](#3.2.4 *it)
[3.2.5 operator== 和 operator!=](#3.2.5 operator== 和 operator!=)
[3.2.6 operator->](#3.2.6 operator->)
[3.3 begin 和 end](#3.3 begin 和 end)
[3.4 遍历测试](#3.4 遍历测试)
[4. list 的增加和删除](#4. list 的增加和删除)
[4.1 empty 和 size](#4.1 empty 和 size)
[4.2 insert 和 erase](#4.2 insert 和 erase)
[4.2.1 insert](#4.2.1 insert)
[4.2.2 erase](#4.2.2 erase)
[4.3 push_back(plus)、push_front 和 pop_back、pop_front](#4.3 push_back(plus)、push_front 和 pop_back、pop_front)
[4.4 测试代码](#4.4 测试代码)
[5. const 类型迭代器](#5. const 类型迭代器)
[6. list析构函数 和 拷贝构造](#6. list析构函数 和 拷贝构造)
[6.1 析构函数](#6.1 析构函数)
[6.2 拷贝构造](#6.2 拷贝构造)
[6.3 operator=](#6.3 operator=)
前言
Q: 什么是list?
A: 参考标准库里面的解释std::list
list就是一个序列容器,支持常数级别的时间复杂度的插入和删除。list底层的数据结构是带头双向循环链表,这样每个数据元素就可以在内存中是非相邻的。
如果对带头双向循环链表不熟悉的话,请猛戳这里带头双向循环链表
接下来 list的模拟实现的简单版。
文件准备:
在 vs 2022中创建头文件和测试文件

然后在list.h文件中创建my_list 命名空间,在my_list来模拟实现list类,在test.cpp文件中创建主函数来调用测试接口。

因为list底层的数据结构是带头结点的双向循环链表,这里需要一个节点的数据结构。
cpp
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
namespace my_list
{
//双向链表结构体节点模板
template<typename T>
struct ListNode
{
ListNode<T>* _next;
ListNode<T>* _prev;
T _data;
//节点的构造函数 用来初始化节点数据
ListNode(const T& x = T())
:_next(nullptr)
,_prev(nullptr)
,_data(x)
{}
};
//类似双向链表类模板
template<class T>
class list
{
typedef ListNode<T> Node;
public:
private:
Node* _head;
};
void test_list1()
{
}
}
为什么使用 struct ?
因为里面的数据需要公开的,因为list本质是带头双向循环链表,不公开list就没法使用。
值得注意的是,节点的构造函数中的 T()
表示类型 T
的默认构造值。
当调用不提供参数时,默认使用类型T的默认值。
主要分为内置类型和自定义类型
-
内置类型:
-
int()
→0
-
double()
→0.0
-
bool()
→false
-
char()
→'\0'
-
指针类型 →
nullptr
-
-
自定义类型:
-
调用该类型的默认构造函数
-
如果没有默认构造函数,编译会报错
-
1. list的私有成员
因为list本质是带头双向循环链表,所以需要一个节点指针指向头节点,还需要一个计数器_size来统计节点个数。
cpp
//list 类中的私有成员
private:
Node* _head; //指向头结点
size_t _size; //记录结点个数
2. 构造函数
2.1 list构造函数
在不考虑内存池的情况下的list构造函数,首先创建一个节点,然后头节点的_next指向自己,最后头结点的_prev也指向自己。

cpp
list()
{
_head = new Node; // 1. 创建一个头节点(哨兵节点)
_head->_next = _head; // 2. 头节点的 next 指向自己
_head->_prev = _head; // 3. 头节点的 prev 也指向自己
_size = 0; // 4. 方便计算节点个数
}
3. list遍历
3.1 push_back
在实现遍历之前,首先保证list里面有元素,所以先实现一个尾插元素。这里与链表的尾插相似。
cpp
//插入一个数据
//尾插数据
void push_back(const T& x)
{
//传统写法
//先申请一个节点
Node* newnode = new Node(x);
Node* tail = _head->_prev; //指向最后一个元素节点
tail->_next = newnode;
newnode->_prev = tail;
newnode->_next = _head;
_head->_prev = newnode;
_size++;
}
使用带头节点的优势就是:当只有一个头节点(哨兵卫)的情况 和 其他情况 进行插入节点都是一样的。
3.2 ListIterator模拟实现
list的遍历需要使用迭代器去遍历。使用迭代器的意义就是不管底层是什么,都可以进行访问。
Q:这里的原生指针可以充当迭代器吗?
A:不可以
因为list和顺序表不一样,在顺序表中,原生指针是天然的迭代器(前提是T*指向的物理空间是连续的);
而list中的原生指针Node*指向的物理空间是不连续的(因为节点是new出来的,不能保证每一个节点的地址都是连续的)。

既然list的原生指针不可以的话,所以封装一个类用自定义类型去重载运算符 ,因为C++中的类和运算符重载可以去控制其行为。
迭代器用什么构造? 节点的指针就可以的,只不过是用类进行封装。
所以这要再命名空间my_list中额外写一个类进行控制。
3.2.1 成员变量_node
这里的使用_node来指向链表中的节点,这里要写成公有类,方便外部使用迭代器去调用。
cpp
template<class T>
struct ListIterator
{
//自定义类型封装指针,去控制其行为
typedef ListNode<T> Node; //模板类重命名为Node
typedef ListIterator<T> Self; //模板类重命名为Self
Node* _node; // 指向当前迭代器所代表的链表节点的指针
//构造函数
ListIterator(Node* node)
:_node(node)
};
3.2.2 it++ 和 ++it
因为原生指针不可以充当迭代器,所以这里使用专门封装的类中的运算符重载来进行控制。
3.2.2.1 it++
it++, 这里需要考虑的是先使用后++,返回的是之前的节点,先保存原来的节点,再进行++。
cpp
//后置++,使用(int)来区分前置和后置
Self operator++(int)
{
//后置++ 返回之前的值
Self tmp(*this); //这里调用了拷贝构造,指针内置类型的浅拷贝,因为希望指向同一个空间
//迭代器也不需要写析构,因为节点不属于迭代器,节点属于链表,所以这里不需要析构
_node = _node->_next;
return tmp;
}
注意:
Q :为什么后置++ 返回T 而不是 T& (或者 为什么后置++是返回临时变量) ?
A :这里返回临时变量,因为后置++ 返回的是自增前的旧值,而旧值是一个临时对象(不能返回局部变量的引用)。前置++ 返回的是自增后的对象本身
3.2.2.2 ++it
++it, 前置++,返回++以后的节点,_node 的下一个节点。
cpp
//重载前置++
Self& operator++()
{
_node = _node->_next;
//前置++,返回++以后的值
return *this;
}
注意:
Q :前置++为什么返回引用?
A : 因为前置++ 返回的是自增后的对象本身(*this
),而 *this
的生命周期,不会立即销毁。
值得注意的是,这里使用 operator(int)++ 来进行区分 前置与后置。
3.2.3 it-- 和 --it
这里自减与上述自增类似。
cpp
// it--
Self operator--(int)
{
Self tmp(*this);
_node = _node->_prev;
return tmp;
}
// --it
Self& operator--()
{
_node = _node->_prev;
return *this;
}
3.2.4 *it
这里需要注意的是,解引用来获取data,不能传值返回,传值返回的是data的拷贝,因为*it 有读和写的功能,传引用返回就可以进行读写data。
cpp
T& operator*()
{
return _node->_data;
}
3.2.5 operator== 和 operator!=
这里只需比较节点的指针就可以,两个迭代器如果它们里面的指针是相同的,它们就是相等的,不相同就是不相等的。
cpp
bool operator==(const Self& it)
{
return _node == it._node;
}
//!=
bool operator!=(const Self& it)
{
return _node != it._node;
}
3.2.6 operator->
这里为了提高可读性,数据访问时,由it.operator->()->_a1 直接变为 it->_a1。
cpp
//在C++11中支持多参数构造的隐式类型转换
struct A
{
int _a1;
int _a2;
A(int a1 = 1,int a2 = 1)
:_a1(a1)
,_a2(a2)
{}
};
void test_list3()
{
list<A> lt;
A aa1(2, 2);
A aa2 = {3,3};
lt.push_back(aa1);
lt.push_back(A(2,2));
lt.push_back({3,3}); //C++11多参数的隐式类型转换
list<A>::iterator it = lt.begin();
cout << it->_a1 << endl;
cout << it.operator->()->_a1 << endl;
}
3.3 begin 和 end
使用begin ,因为想使用_head->next 去构造节点,因为_head是私有的,所以使用公有的begin,返回第一个元素的迭代器。
begin返回头结点的下一个节点即可。
普通写法
cpp
iterator begin()
{
//1 普通版
iterator it = _head->_next;
return it;
}
匿名对象写法
cpp
iterator begin()
{
//2 匿名对象版
return iterator(_head->_next);
}
单参数构造写法
cpp
iterator begin()
{
//3 单参数构造函数支持隐式类型转换
//这里的迭代器就是单参数构造函数
return _head->_next;
//构造函数
//ListIterator(Node * node)
// :_node(node)
//{}
}
end返回头结点即可(因为list本质是带头节点的双向循环链表)。
cpp
iterator end()
{
//其他写法同begin类似
return _head;
}
3.4 遍历测试
这里可以在命名空间my_list中进行测试。
cpp
void test_list1()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(5);
//list的遍历需要使用迭代器
list<int>::iterator it = lt.begin();
while (it != lt.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
输出结果 :

4. list 的增加和删除
4.1 empty 和 size
在实现list的增加和删除这里需要先实现,判空和计算节点个数的接口。
cpp
bool empty() const
{
return _size == 0;
}
size_t size() const
{
return _size;
}
4.2 insert 和 erase
4.2.1 insert
这里要实现一个 在pos节点之前插入一个值为val的函数。
先用cur指向pos节点,再创建一个值为val的节点,再用prev指向cur的前一个节点。

cpp
void insert(iterator pos , const T& val)
{
Node* cur = pos._node; //cur指向pos位置的节点
Node* newnode = new Node(val);
Node* prev = cur->_prev;
//在cur前面插入一个节点
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
++_size;
}
4.2.2 erase
删除pos位置的节点,链表只需修改指针域即可。

cpp
iterator erase(iterator pos)
{
//避免空
assert(!empty());
//删除pos位置的节点
//prev cur next
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* next = cur->_next;
prev->_next = next;
next->_prev = prev;
delete cur;
--_size;
//pos 失效 是 释放了pos位置的空间
//为了避免失效,要返回一下节点的迭代器
return iterator(next);
}
值得注意的是,erase返回类型是iterator,避免迭代器失效,要返回下一个节点的迭代器。
4.3 push_back(plus)、push_front 和 pop_back、pop_front
这里借助上方实现的insert 和 erase 来进行实现。
cpp
//push_back 现代写法
void push_back(const T& x)
{
insert(end(),x);
}
//头插
void push_front(const T& x)
{
insert(begin(),x);
}
//尾删
void pop_back()
{
erase(--end()); //注意这里不能end()-1 ,因为这里是使用运算符重载来实现的
}
//头删
void pop_front()
{
erase(begin());
}
值得注意的是,pop_back去调用end()时,不能使用end-1 ,运算符重载只实现了operator--()。
4.4 测试代码
在test.cpp中进行调用,test_list2() 函数在my_list命名空间中进行实现。

cpp
void test_list2()
{
list<int> lt;
lt.push_back(2);
lt.push_front(1);
lt.push_back(3);
lt.push_back(4);
lt.push_back(5);
//整体遍历
list<int>::iterator it = lt.begin();
while (it != lt.end())
{
cout << *it << " ";
++it;
}
cout << endl;
//头删
lt.erase(lt.begin());
it = lt.begin();
while (it != lt.end())
{
cout << *it << " ";
++it;
}
cout << endl;
//头删
lt.pop_front();
//尾删
lt.pop_back();
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
cout << "元素个数:"<<lt.size() << endl;
}
5. const 类型迭代器
我们知道:
cpp
const int* ptr; // const 在*的左边,修饰的是指针指向的数据不能被修改
int* const ptr = nullptr; //const 在*右边,修饰的是指针不能被修改
我们知道权限可以缩小,但是不能放大。这里要实现的是迭代器指向的内容不能被修改。

具体实现:
方式一: 单独实现一个ListConstIterator去封装里面的迭代器指向的内容不能被修改。
cpp
template<class T>
struct ListConstIterator
{
//自定义类型封装指针,去控制其行为
typedef ListNode<T> Node; //模板类重命名为Node
typedef ListConstIterator<T> Self; //模板类重名为Self
Node* _node; // 指向当前迭代器所代表的链表节点
ListConstIterator(Node* node)
:_node(node)
{}
//*
const T& operator*()
{
return _node->_data;
}
//it->
const T* operator->()
{
return &_node->_data;
}
//通过运算符重载控制其行为
//重载前置++
Self& operator++()
{
_node = _node->_next;
//前置++,返回++以后的值
return *this;
}
//后置++,使用(int)来区分前置和后置
Self operator++(int)
{
//后置++ 返回之前的值
Self tmp(*this);
_node = _node->_next;
return tmp;
}
// it--
Self operator--(int)
{
Self tmp(*this);
_node = _node->_prev;
return tmp;
}
// --it
Self& operator--()
{
_node = _node->_prev;
return *this;
}
bool operator==(const Self& it)
{
return _node == it._node;
}
//!=
bool operator!=(const Self& it)
{
return _node != it._node;
}
};
方式二:
发现方式一的写法有一些代码冗余,因为里面具体只是一些函数的返回值类型与Iterator类不同,所以可以考虑使用模板参数来控制。
本质:写一个模板类,然后编译器实例化生成两个类。

cpp
template<class T,class Ref,class Ptr>
struct ListIterator
{
//自定义类型封装指针,去控制其行为
typedef ListNode<T> Node; //模板类重命名为Node
typedef ListIterator<T,Ref,Ptr> Self; //模板类重名为Self
Node* _node; // 指向当前迭代器所代表的链表节点
ListIterator(Node* node)
:_node(node)
{}
//*
Ref operator*()
{
return _node->_data;
}
//it->
Ptr operator->()
{
return &_node->_data;
}
//通过运算符重载控制其行为
//重载前置++
Self& operator++()
{
_node = _node->_next;
//前置++,返回++以后的值
return *this;
}
//后置++,使用(int)来区分前置和后置
Self operator++(int)
{
//后置++ 返回之前的值
Self tmp(*this);
_node = _node->_next;
return tmp;
}
// it--
Self operator--(int)
{
Self tmp(*this);
_node = _node->_prev;
return tmp;
}
// --it
Self& operator--()
{
_node = _node->_prev;
return *this;
}
bool operator==(const Self& it)
{
return _node == it._node;
}
//!=
bool operator!=(const Self& it)
{
return _node != it._node;
}
};
6. list析构函数 和 拷贝构造
6.1 析构函数
先实现一个clear,借助迭代器和erase删除所有数据(不含头节点)。
cpp
void clear()
{
//借助迭代器和erase
iterator it = begin();
while (it != end())
{
it = erase(it); //前提是erase处理了迭代器失效问题
}
}
再去调用clear实现析构。
cpp
~list()
{
clear();
delete _head;
_head = nullptr;
}
6.2 拷贝构造
值得注意的是,list的拷贝构造需要手动去实现,因为:
当不写拷贝构造,list会使用默认的拷贝构造(浅拷贝--指向同一块空间),但是 对同一块空间进行析构两次是错误的,因为第一次析构后对象就已经不在了,第二次可能导致释放了不该释放的内存!
先初始化一个头节点,再复用push_back,把lt的节点拷贝进去。
cpp
void empty_init()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
_size = 0;
}
//list 构造函数
list()
{
empty_init();
}
//lt1(lt2)
list(const list<T>& lt)
{
//先初始化一个头节点
//再复用push_back,把lt的节点拷贝进去
empty_init();
for (auto& e : lt)
{
push_back(e);
}
}
需要析构,一般需要自己写深拷贝。
6.3 operator=
cpp
void swap(list<T>& lt)
{
//借助标准库函数中的swap来实现
std::swap(_head,lt._head);
std::swap(_size,lt._size);
}
//lt2 = lt1
list<T>& operator=(list<T> lt)
{
swap(lt);
return *this;
}