🍕 阿i索 个人主页
《C语言专栏》 《C++专栏》
《数据结构专栏》 《LaTeX专栏》
《软件配置问题》 《Linux 专栏》
待更新...

前言.

本篇介绍C++ 标准库(STL)中的list容器


一、list

list 是 C++ STL 序列容器,头文件 <list>,模板 list<T>,用于存储同类型数据,底层为双向循环链表。内存不连续,每个元素是独立节点,节点包含三块内容:当前元素、前驱节点指针、后继节点指针;节点分散在堆中,靠指针串联,不存在整块连续内存。

list学习文档

二、list的使用

1. 迭代器

迭代器分类:

  1. 单向迭代器:仅支持自增 ++,只能从前向后遍历,不能反向走;
  2. 双向迭代器:支持 ++(向后)、--(向前),正反都能遍历;
  3. 随机访问迭代器:除了++/--,还支持 +、-、+=、-=、下标偏移(it+n),可以任意跳转,直接跳到指定位置。

list 底层是双向循环链表,每个节点只存前后指针,只能一步一步前后移动,无法直接跳跃偏移,因此它的迭代器是双向迭代器,只支持++--,不支持it + 5这种随机跳转操作,不支持下标[]访问。

2. 基础遍历

list<int> lt2 = { 1,2,3,4,5 };
// 1. 迭代器while遍历
list<int>::iterator it2 = lt2.begin();
while (it2 != lt2.end())
{
	cout << *it2 << " ";
	++it2;
}

// 2. C++11范围for遍历(简洁常用)
for (auto e : lt2)
{
	cout << e << " ";
}

要点:

  • begin():首元素迭代器;end():末尾无效位置,循环终止条件
  • list 无随机访问,不能用 lt2[i] 下标访问

3. 插入 insert & 删除 erase 与迭代器失效规则

auto pos = find(lt2.begin(), lt2.end(), 3);
if (pos != lt2.end())
{
	lt2.insert(pos, 30); // 在pos前插入元素,pos迭代器不失效
	lt2.erase(pos);      // 删除pos指向节点,pos立刻失效,后续不能解引用
}

失效:

  1. insert 插入:所有原有迭代器、指针全部有效
  2. erase 删除:仅被删除节点对应的迭代器失效,其余迭代器不受影响

4. 排序 sort

  1. 全局算法 sort(lt.begin(), lt.end()) 不可用:全局 sort 要求随机访问迭代器,list 只有双向迭代器
  2. 必须使用 list内置成员排序函数
lt2.sort(); // 默认升序,底层链表归并排序

5. 排序 + 去重 unique

unique() 仅能去除相邻重复元素,使用前必须先排序

list<int> lt3 = { 1,2,2,2,3,3,2,3,4,5 };
lt3.sort();   // 1,2,2,2,2,3,3,3,4,5
lt3.unique(); // 清除相邻重复,结果:1,2,3,4,5

5. splice 链表迁移

作用:把另一个 list(或自身)的节点移动到目标位置,无拷贝,仅修改指针,效率极高

三参数格式:目标链表.splice(插入位置, 源链表, 待移动元素迭代器)

list<int> lt4 = { 1,2,3,4,5 };
auto pos2 = find(lt4.begin(), lt4.end(), 4);
// 将lt4中pos2指向的元素,移动到lt4头部
lt4.splice(lt4.begin(),lt4,pos2);
// 执行后结果:4 1 2 3 5

关键特点:

  • 移动节点,不拷贝数据,性能远高于 insert+erase
  • 移动后原链表对应节点消失,迭代器跟随节点转移依然有效

三、list 优缺点总结

优点

  1. 头部、中间、尾部增删元素均为 O (1),仅修改指针
  2. 插入操作不会导致迭代器失效
  3. splice 实现链表节点无损迁移

缺点

  1. 无随机访问,查找、遍历效率低,O (n)
  2. 每个节点额外存储前后指针,内存开销大
  3. 不支持全局 sort,缓存命中率低,大批量遍历慢

适用场景

频繁在任意位置插入、删除数据,很少随机读取;不适合大量遍历查询场景。

容器 适用场景
vector

1. 需要下标随机访问、频繁读取元素

2. 仅在尾部做插入删除操作

3. 需要使用全局 sort 整体排序

4. 大批量顺序遍历,追求读取速度

list

1. 频繁头插、头删,或中间任意位置增删元素

2. 需要链表拆分合并(splice)

3. 需要长期保存迭代器,避免频繁失效

四、list的模拟实现

模块 1 list_node 节点结构体(存储单元)

1.1 为什么用 struct 不用 class

template<class T>
struct list_node
{
	list_node<T>* _next;
	list_node<T>* _prev;
	T _data;
	list_node(const T& x)
		:_next(nullptr), _prev(nullptr), _data(x)
	{}
};

class 成员默认私有,链表、迭代器没法直接读写前后指针和数据;要访问就得额外写一堆 get/set 函数,代码冗余。

struct 成员默认公有,节点只用来存数据和指针,不需要封装保护,直接访问更省事。

1.2 构造无缺省参数编译报错问题

问题代码

list_node(const T& x)
	:_next(nullptr)
	, _prev(nullptr)
	, _data(x)
{}
// 报错调用
_head = new Node;

问题原因:写了带参构造,编译器不会自动生成无参构造,创建节点必须传参数。

哨兵头只是占位空节点,没有业务数据可传,无参创建参数不匹配,编译报错。

方案 1:构造加匿名对象缺省参数
    //节点类:双向循环链表存储单元
	template<class T>
	struct list_node
	{
		list_node<T>* _next;  // 后继节点指针
		list_node<T>* _prev;  // 前驱节点指针
		T _data;              // 存储的数据

		//节点构造函数
		//list_node(const T& x=T())//调用时有参数,解决1:匿名对象做缺省参数
		list_node(const T& x)
			:_next(nullptr)
			, _prev(nullptr)
			, _data(x)
		{}
	};
方案 2:构造不设缺省,创建哨兵手动传匿名对象
		// 初始化空链表:创建哨兵头节点,完成双向循环闭环
		void empty_init()
		{
			// //调用时有参数,解决2:调用时手动传匿名对象做参数
			_head = new Node(T());
			_head->_next = _head; // 空链表:头节点后继指向自己
			_head->_prev = _head; // 空链表:头节点前驱指向自己
		}

1.3 节点单独写在外部,不嵌套进 list 类

  1. 迭代器代码要提前识别 list_node 类型,如果节点写在 list 内部,编译器读到迭代器时找不到节点定义,直接报错。
  2. 分工分开:节点只存指针和数据,list 只管理整条链表增删、内存,改一块代码不会误改另一块,bug 更好找。
  3. 插入元素需要随时new Node(x)创建节点,独立结构体不受 list 类作用域限制,创建更方便。

模块 2 list_iterator 迭代器(遍历游标)

 2.1 必须实现 const 迭代器的原因

被 const 修饰的 list 对象,只能调用带 const 的成员函数。

  1. 没有 const 迭代器,const list<int> lt无法遍历;
  2. 普通迭代器返回T&,能修改常量容器数据,破坏只读规则;const 迭代器返回const T&,语法上禁止修改元素。
Ref operator*()
{
	return _node->_data;
}
// const对象专属遍历接口
const_iterator begin()const
{
	return const_iterator(_head->_next);
}
const_iterator end()const
{
	return const_iterator(_head);

    2.2如何实现普通迭代器和const迭代器

    方案一: 一个模板生成普通 /const 两种迭代器
    // 两套类绝大部分代码完全一样,仅返回值不同
    // 普通迭代器
    template<class T>
    struct list_iterator
    {
    	using Node = list_node<T>;
    	Node* _node;
    
    	list_iterator(Node* node) :_node(node) {}
    	T& operator*() { return _node->_data; }
    	T* operator->() { return &_node->_data; }
    	// 前置++、后置++、--、==、!= 一堆重载
    };
    
    // const迭代器,95%代码完全重复,只有*、->返回值不同
    template<class T>
    struct list_const_iterator
    {
    	using Node = list_node<T>;
    	Node* _node;
    
    	list_const_iterator(Node* node) :_node(node) {}
    	const T& operator*() { return _node->_data; }
    	const T* operator->() { return &_node->_data; }
    	// 和上面一模一样的 ++ -- == != 重载,复制粘贴一遍
    };
    
    //list双向循环链表容器类
    template<class T>
    class list
    {
    	using Node = list_node<T>; // 节点类型别名
    public:
    	//迭代器
    	// 方案一:手写两个迭代器类:
    	using iterator = list_iterator<T>;
    	using const_iterator = list_const_iterator<T>;//重新定义一个const迭代器类
        ...
    }
    方案二:一套模板同时实现普通迭代器、const 迭代器

    两套迭代器只有*itit->返回值不一样,其余遍历逻辑完全重复。 增加 Ref、Ptr 模板参数控制返回类型,编译器自动生成两份迭代器代码,不用手动复制粘贴,修改遍历逻辑只改一处。

    //一个模板类由编译器自动实例化出普通迭代器、const迭代器两种类型
    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控制
    	Ref operator*()
    	{
    		return _node->_data;
    	}
    	// 箭头返回类型由模板参数Ptr控制
    	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;
    	}
    };
    // list内部定义两种迭代器
    using iterator = list_iterator<T, T&,T*>;
    using const_iterator = list_iterator<T,const T&,const T*>;
    

    (1)模板参数作用:

    • T:链表存储的数据类型;
    • Ref:控制 *it 返回的引用类型;
    • Ptr:控制 it-> 返回的指针类型。

    (2)编译器会根据传入的模板参数,自动生成两份独立代码:

    • list_iterator<int, int&, int*> → 普通 iterator;
    • list_iterator<int, const int&, const int*> → const_iterator;

    (3)遍历移动(++/--/ 判断相等)共用同一套逻辑,只写一次;

    (4)解引用区分读写:

    • 普通迭代器 *it 返回 T&,可以修改元素 *it = 100
    • const 迭代器 *it 返回 const T&,只能读取,不能赋值修改;

      2.3 后置 ++ 不能返回引用,否则野引用崩溃

      错误代码

      Self& operator++(int)
      {
      	Self tmp(*this);
      	_node = _node->_next;
      	return tmp;
      }
      

      正确代码

      Self operator++(int)
      {
      	Self tmp(*this);
      	_node = _node->_next;
      	return tmp;
      }
      

      后置 ++ 逻辑为先返回原值再移动指针,必须创建局部临时 tmp 保存初始状态。 tmp 是函数内局部变量,函数结束内存销毁,如果返回 tmp 的引用,后续使用这个引用会访问已销毁内存,程序崩溃;值拷贝返回临时对象无风险。

      2.4 迭代器不需要自定义析构

      迭代器里只有节点指针,仅记录节点地址,不管理堆内存;所有节点都是 list 统一 new、统一 delete。

      如果给迭代器写析构delete _node,多个迭代器指向同一个节点会多次释放内存,循环临时迭代器还会误删链表有效节点,程序崩溃,默认析构足够使用。

      2.5 迭代器单独封装,不写进 list 内部

      我们代码的书写顺序:先写节点,再写迭代器,最后写链表。迭代器里面要用到节点,链表里面要用到迭代器。

      情况 1:迭代器写在 list 里面编译器,开始读 list 代码,刚读到里面嵌套的迭代器,list 都还没完整读完。后面 list 的 begin、end 函数要用到这个迭代器,但此时这个迭代器还没被编译器完整识别,两边互相等着对方定义,编译器识别不出类型,直接报错。

      情况 2:迭代器写在外面编译器,先读完节点,再完整读完迭代器,最后才读链表。链表使用的是已经提前定义好的迭代器,不存在互相等对方的情况,编译器能正常识别,不会报错。

      模块 3 list 初始化、构造相关

      3.1 empty_init 初始化空双向循环带头链表

      void empty_init()
      {
      	_head = new Node(T());
      	_head->_next = _head;
      	_head->_prev = _head;
      }
      

      链表带哨兵头节点,不存有效数据;空链表头节点前后指针都指向自己形成闭环。

      好处:多处代码都需要创建空链表,重复写相同初始化代码会冗余。拷贝构造、普通构造、区间构造、初始化列表构造,全都需要先把链表初始为空。如果不抽成 empty_init,每个构造函数都要重复写创建哨兵头、头尾自指向、初始化 size 的代码,改逻辑要多处同步修改。

      3.2 initializer_list 构造,支持花括号初始化

      list(std::initializer_list<T> il)
      {
      	empty_init();
      	for (auto& e : il)
      	{
      		push_back(e);
      	}
      }
      

      支持list<int> lt{1,2,3,4}写法,编译器自动把大括号元素打包传入函数,循环尾插完成初始化,贴合标准容器用法。使用前需要包含#include <initializer_list>。

      3.3 迭代器区间模板构造

      template <class InputIterator>
      list(InputIterator first, InputIterator last)
      {
      	empty_init();
      	while(first!=last)
      	{
      		push_back(*first);
      		++first;
      	}
      }
      
      作用

      接收一段数据的开头、结尾游标,把这段数据全部复制,生成新链表。

      InputIterator 通用输入迭代器,不限制只能使用 list 自身迭代器,只要支持 * 取值、++ 向后移动、!= 判断结束的游标都能传入。

      能接收哪些数据
      1. 另一个 list 的首尾迭代器,复制整条链表;
      2. 其他 STL 容器的迭代器:所有标准容器的 begin()end() 都能传入: vector、string、deque、set、map、unordered_set、unordered_map
      3. 普通数组首尾指针:数组名是首元素指针,指针支持*++!=,满足输入迭代器要求。
      4. 自定义自定义容器 / 自定义迭代器:只要自己写的游标满足三个操作:解引用*it、自增++it、判等it != end,就能传入这个区间构造。

      模块 4 拷贝构造、赋值重载(深浅拷贝)

      传统写法

      // 拷贝构造
      list(const list<T>& lt)
      {
      	empty_init();
      	for (auto& e : lt)
      		push_back(e);
      }
      // 赋值重载
      list<T>& operator=(const list<T>& lt)
      {
      	if(this != &lt)
      	{
      		clear();
      		for(auto& e : lt)
      			push_back(e);
      	}
      	return *this;
      }
      

      现代写法

      // 拷贝构造
      list(const list<T>& lt)
      {
      	empty_init();
      	list<T> tmp(lt.begin(), lt.end());
      	swap(tmp);
      }
      // 赋值重载
      list<T>& operator=(list<T> tmp)
      {
      	swap(tmp);
      	return *this;
      }
      void swap(list<T>& lt)
      {
      	std::swap(_head, lt._head);
      	std::swap(_size, lt._size);
      }
      

      解释

      1. 编译器默认拷贝是浅拷贝,只复制头指针,两个对象共用节点,析构时重复释放内存崩溃,必须手动深拷贝新建节点;
      2. 传统写法拷贝、赋值重复循环代码,赋值需要手动判断自赋值;
      3. swap 优化:临时对象自动完成深拷贝,仅交换头指针和 size,交换操作 O (1) 极快;临时对象出作用域自动释放旧节点,不用手动清理。

      模块 5 增删接口 insert erase push pop

      5.1 push/pop 全部复用 insert/erase,减少重复代码

      写法1(不复用):

      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;
      }
      

      写法2:

      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()); }
      

      头尾增删本质都是节点指针重链接,只是位置不同。单独写四套指针逻辑,修改时要同步改四处,容易漏写出 bug;复用核心 insert、erase,只维护一套链接代码。

      5.2 insert 插入,所有迭代器不失效

      	void insert(iterator pos, const T& x)
      	{
      		Node* cur = pos._node;    // pos指向的节点
      		Node* prev = cur->_prev;  // pos节点的前驱节点
      		Node* newnode = new Node(x); // 新建待插入节点
      
      		// 双向指针重新链接:prev <-> newnode <-> cur
      		prev->_next = newnode;
      		newnode->_prev = prev;
      		newnode->_next = cur;
      		cur->_prev = newnode;
      
      		++_size; // 有效节点数量+1
      	}

      insert 只在两个原有节点中间新增节点,原有节点内存地址不变,所有指向旧节点的迭代器依然有效。对比 vector 插入会移动元素,全部迭代器失效。

      5.3 erase 删除,仅被删位置迭代器失效,必须接收返回值

      错误循环写法

      while(it != end())
      {
          erase(it); // it指向已释放节点,变成野指针,循环崩溃
      }
      

      正确代码

      		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;    // 有效节点数量-1
      
      			return iterator(next); // 返回下一个节点的迭代器,解决迭代器失效问题
      		}
      		void clear()
      		{
      			iterator it = begin();
      			while (it != end())
      			{
      				it = erase(it);
      			}
      		}
      

      erase 会 delete 当前节点,原迭代器变成野指针;函数提前记录下一个有效节点并返回,循环用 it 接收返回值,保证每次迭代器合法。只有被删除位置迭代器失效,其余不受影响。

      模块 6 clear、析构、size 效率优化

      6.1 不手动释放堆节点会内存泄漏

      void clear()
      {
      	iterator it = begin();
      	while (it!= end())
      		it = erase(it);
      }
      ~list()
      {
      	clear();
      	delete _head;
      	_head = nullptr;
      	_size = 0;
      }
      

      有效节点、哨兵头节点都在堆上开辟,堆内存不会自动回收。clear 循环释放全部数据节点,析构额外释放哨兵头,完整释放所有堆内存,无内存泄漏。

      6.2 维护_size 成员,O (1) 获取链表长度

      写法1(低效):

      size_t size()const
      {
      	size_t n = 0;
      	for (auto& e : *this)
      		++n;
      	return n;
      }
      

      写法2:

      size_t size()const
      {
      	return _size;
      }
      

      写法1每次获取长度都要遍历整条链表,数据量大速度慢;

      优化:每次插入++_size、删除--_size实时记录数量,直接返回变量无需遍历,查询效率极高。

      模块七:list的模拟实现

      list.h

      #pragma once
      #include <initializer_list>
      #include <iostream>
      namespace asuo
      {
      	//节点类:双向循环链表存储单元
      	template<class T>
      	//使用struct,struct所有成员默认public,不需要封装访问接口
      	struct list_node
      	{
      		list_node<T>* _next;  // 后继节点指针
      		list_node<T>* _prev;  // 前驱节点指针
      		T _data;              // 存储的数据
      
      		//节点构造函数
      		//list_node(const T& x=T())//调用时有参数,解决1:匿名对象做缺省参数
      		list_node(const T& x)
      			:_next(nullptr)
      			, _prev(nullptr)
      			, _data(x)
      		{}
      	};
      
      	//迭代器模板类
      	//分开写普通迭代器、const迭代器两个独立类,运算符重载代码大部分重复,维护成本极高
      	//解决:增加两个模板参数Ref、Ptr,一个模板类由编译器自动实例化出普通迭代器、const迭代器两种类型
      	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)
      		{
      		}
      
      		//迭代器为什么不需要手动写析构函数?
      		//底层原理:迭代器只保存节点地址,只是借用指针,不拥有堆内存所有权;所有节点都是list容器通过new创建、delete释放
      		//如果手动写析构执行delete _node,会出现两种致命bug:
      		//1、多个迭代器同时指向同一个节点,迭代器销毁时重复delete,双重释放内存,程序直接崩溃
      		//2、for循环内临时迭代器出作用域自动销毁,误删除链表有效节点,链表结构彻底损坏
      		//结论:编译器默认生成的析构足够使用,无需自定义析构
      
      		//运算符重载
      		// *it=1,解引用运算符 *it
      		//模板参数Ref作用区分读写:普通迭代器传T&,const迭代器传const T&
      		//const迭代器解引用返回只读常引用,语法层面禁止修改容器元素,保证const对象只读语义
      		Ref operator*()
      		{
      			return _node->_data; // 返回节点数据的引用
      		}
      
      		//箭头运算符 it->
      		//模板参数Ptr区分指针类型:普通迭代器T*,const迭代器const T*
      		Ptr operator->()
      		{
      			return &_node->_data; // 返回数据的地址,支持结构体成员访问
      		}
      
      		//前置++ ++it
      		Self& operator++()
      		{
      			_node = _node->_next; // 游标移动到下一个节点
      			return *this;         // 返回自身,支持连续++操作
      		}
      		//后置++ it++
      		//如果错误写成返回值Self&,返回局部临时对象引用,函数结束临时对象销毁,产生野引用,访问直接崩溃
      		//解决:后置自增以值拷贝返回临时对象,前置返回自身引用
      		Self operator++(int)
      		{
      			Self tmp(*this); // 先保存当前迭代器快照
      			_node = _node->_next; // 游标后移
      			return tmp; // 返回未移动前的旧迭代器副本
      		}
      		//前置-- --it
      		Self& operator--()
      		{
      			_node = _node->_prev;
      			return *this;
      		}
      		//后置-- it--
      		//和后置++一样,不能返回引用,只能返回值
      		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;
      		}
      	};
      
      	////const迭代器类
      	//template<class T>
      	//struct list_const_iterator
      	//{
      	//	using Self = list_const_iterator<T>;
      	//	using Node = list_node<T>;
      	//	Node* _node;
      
      	//	//构造
      	//	list_const_iterator(Node* node)
      	//		:_node(node)
      	//	{
      	//	}
      
      	//	//迭代器不需要写析构
      
      	//	//运算符重载
      	//	// *it=1
      	//	const T& operator*()//返回const别名,能读不能修改
      	//	{
      	//		return _node->_data;
      	//	}
      
      	//	// ++it
      	//	Self& operator++()
      	//	{
      	//		_node = _node->_next;
      	//		return *this;
      	//	}
      	//	// it++
      	//	Self& operator++(int)
      	//	{
      	//		Self tmp(*this);
      	//		_node = _node->_next;
      	//		return tmp;
      	//	}
      	//	// --it
      	//	Self& operator--()
      	//	{
      	//		_node = _node->_prev;
      	//		return *this;
      	//	}
      	//	// it--
      	//	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;
      	//	}
      	//};
      
      	//list双向循环链表容器类
      	template<class T>
      	class list
      	{
      		using Node = list_node<T>; // 节点类型别名
      	public:
      		////迭代器
      		// 方案一:手写两个迭代器类:
      		//using iterator = list_iterator<T>;
      		//using const_iterator = list_const_iterator<T>;//重新定义一个const迭代器类
      		// 方案二:利用同一个迭代器模板,定义两种迭代器
      		// 普通迭代器:可读可写元素
      		using iterator = list_iterator<T, T&, T*>;
      		// const迭代器:仅读取,禁止修改元素
      		using const_iterator = list_iterator<T, const T&, const T*>;
      
      		// 普通list对象调用begin(),返回可修改迭代器
      		iterator begin()
      		{
      			return iterator(_head->_next); // 第一个有效节点是哨兵头的下一个
      		}
      		iterator end()
      		{
      			return iterator(_head); // end代表哨兵头节点,遍历结束标志
      		}
      		// 带const修饰的list对象只能调用本版本begin/end
      		// 如果不写const重载的begin/end,const list无法遍历;或者拿到可修改迭代器,篡改常量容器数据,违背C++ const语法规则
      		const_iterator begin()const
      		{
      			return const_iterator(_head->_next);
      		}
      		const_iterator end()const
      		{
      			return const_iterator(_head);
      		}
      
      		// 初始化空链表:创建哨兵头节点,完成双向循环闭环
      		void empty_init()
      		{
      			// //调用时有参数,解决2:这里用匿名对象做参数
      			_head = new Node(T());
      			_head->_next = _head; // 空链表:头节点后继指向自己
      			_head->_prev = _head; // 空链表:头节点前驱指向自己
      		}
      
      		// 无参构造:创建空链表
      		list()
      		{
      			empty_init();
      		}
      
      		// initializer_list构造:支持花括号直接初始化 list<int> lt{1,2,3,4}
      		list(std::initializer_list<T> i1)
      		{
      			empty_init();
      			for (auto& e : i1)
      			{
      				push_back(e);
      			}
      		}
      
      		//迭代器区间构造:把其他容器 / 数组一段区间的数据拷贝到当前 list 中
      		template <class InputIterator>
      		list(InputIterator first, InputIterator last)
      		{
      			empty_init();
      			while (first != last)
      			{
      				push_back(*first);
      				++first;
      			}
      		}
      
      		// n个相同值构造:创建n个val元素的链表
      		list(size_t n, T val = T())
      		{
      			empty_init();
      			for (size_t i = 0; i < n; i++)
      			{
      				push_back(val);
      			}
      		}
      
      		// 析构函数
      		// 所有节点、哨兵头节点都是new在堆上开辟,不手动释放会永久占用内存,造成内存泄漏
      		// 解决方案:先clear释放全部有效数据节点,再单独释放哨兵头节点_head
      		~list()
      		{
      			clear();
      			delete _head;
      			_head = nullptr;
      			_size = 0;
      		}
      
      		////传统拷贝构造
      		////缺陷1:代码重复,赋值重载需要再写一遍循环插入逻辑
      		////缺陷2:没有利用临时对象自动析构,内存管理繁琐
      		//list(const list<T>& lt)
      		//{
      		//	empty_init();
      		//	for (auto& e : lt)
      		//	{
      		//		push_back(e);
      		//	}
      		//}
      		////传统赋值重载
      		////缺陷1:必须手动判断自赋值 if(this != &lt),忘记写会内存出错
      		////缺陷2:逻辑和拷贝构造高度重复,维护麻烦
      		//list<T>& operator=(const list<T>& lt)
      		//{
      		//	if (this != &lt)
      		//	{
      		//		clear();
      		//		for (auto& e : lt)
      		//		{
      		//			push_back(e);
      		//		}
      		//	}
      		//	return *this;
      		//}
      
      		//现代写法:拷贝构造
      		//编译器默认生成的拷贝构造是浅拷贝,只复制_head指针,两个list共用同一批堆节点,析构时重复delete,程序崩溃
      		//解决:创建临时对象tmp完成深拷贝,swap交换当前对象与tmp底层资源,tmp出作用域自动释放旧数据
      		list(const list<T>& lt)
      		{
      			empty_init();
      			list<T> tmp(lt.begin(), lt.end()); // tmp深拷贝原链表所有元素
      			swap(tmp);						   // 交换头节点指针、size变量
      		}
      
      		//现代赋值重载lt1=lt3:参数传值自动生成临时对象,无需手动判断自赋值
      		list<T>& operator=(list<T> tmp)
      		{
      			swap(tmp);
      			return *this;
      		}
      
      		//交换两个list底层资源,只交换指针和size,O(1)时间复杂度,效率高
      		void swap(list<T>& lt)
      		{
      			std::swap(_head, lt._head);
      			std::swap(_size, lt._size);
      		}
      
      		//清空所有有效数据节点,保留哨兵头节点
      		void clear()
      		{
      			iterator it = begin();
      			while (it != end())
      			{
      				//erase删除节点后原it指向已释放内存,变成野指针,如果直接写erase(it),下一轮循环访问it程序崩溃
      				//解决:erase会返回下一个合法迭代器,循环必须用it接收返回值 it = erase(it);
      				it = erase(it);
      			}
      		}
      
      	
      		////缺陷:push_back/push_front/pop_back/pop_front都要手写一套双向指针链接代码,大量重复代码
      		//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;
      		//}
      
      		//尾插,复用insert核心逻辑,消除重复指针操作代码
      		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());
      		}
      
      		//在pos迭代器指向节点的前面插入新元素
      		//list插入不会造成任何迭代器失效,只会新增节点,原有所有节点内存地址不变
      		void insert(iterator pos, const T& x)
      		{
      			Node* cur = pos._node;    // pos指向的节点
      			Node* prev = cur->_prev;  // pos节点的前驱节点
      			Node* newnode = new Node(x); // 新建待插入节点
      
      			// 双向指针重新链接:prev <-> newnode <-> cur
      			prev->_next = newnode;
      			newnode->_prev = prev;
      			newnode->_next = cur;
      			cur->_prev = newnode;
      
      			++_size; // 有效节点数量+1
      		}
      
      		//删除pos迭代器指向的节点,返回下一个有效迭代器
      		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;    // 有效节点数量-1
      
      			return iterator(next); // 返回下一个节点的迭代器,解决迭代器失效问题
      		}
      
      		//获取链表有效元素个数
      		size_t size()const
      		{
      			////低效写法:每次调用size都要遍历整条链表统计数量,时间复杂度O(n)
      			//size_t n = 0;
      			//for (auto& e : *this)
      			//{
      			//	++n;
      			//}
      			//return n;
      
      			return _size; // 维护成员变量,O(1)直接返回长度
      		}
      	private:
      		Node* _head;    // 双向循环链表哨兵头节点,不存储有效数据
      		size_t _size = 0;// 记录当前有效节点总数
      	};
      }

      test.cpp测试文件

      #define _CRT_SECURE_NO_WARNINGS
      #include"list.h"
      #include<iostream>
      using namespace std;
      
      // 测试1:尾插、普通迭代器遍历、范围for循环
      void test_list1()
      {
      	cout << "========== test_list1 尾插+迭代器+范围for ==========\n";
      	asuo::list<int> lt;
      	lt.push_back(1);
      	lt.push_back(2);
      	lt.push_back(3);
      	lt.push_back(4);
      
      	// 1、普通while迭代器遍历(可读可修改)
      	asuo::list<int>::iterator it = lt.begin();
      	while (it != lt.end())
      	{
      		cout << *it << " ";
      		++it;
      	}
      	cout << endl;
      
      	// 2、支持迭代器就自动支持范围for循环(编译器底层替换成迭代器遍历)
      	for (auto e : lt)
      	{
      		cout << e << " ";
      	}
      	cout << "\n\n";
      }
      
      // 测试2:头插、头删、尾删、size()获取长度
      void test_list2()
      {
      	cout << "========== test_list2 头插/头删/尾删/size ==========\n";
      	asuo::list<int> lt;
      	lt.push_back(1);
      	lt.push_back(2);
      	lt.push_back(3);
      	lt.push_back(4);
      	lt.push_front(-1);
      	lt.push_front(-2);
      
      	cout << "头插后链表:";
      	for (auto e : lt)
      	{
      		cout << e << " ";
      	}
      	cout << endl;
      
      	lt.pop_back();
      	lt.pop_back();
      	lt.pop_front();
      	lt.pop_front();
      
      	cout << "两次尾删+两次头删后链表:";
      	for (auto e : lt)
      	{
      		cout << e << " ";
      	}
      	cout << endl;
      
      	cout << "当前有效元素个数size:" << lt.size() << "\n\n";
      }
      
      // 工具函数:接收const list,测试const_iterator只读迭代器
      void Print(const asuo::list<int>& lt)
      {
      	// 传入const引用,只能使用const_iterator,解引用无法修改数据
      	asuo::list<int>::const_iterator it = lt.begin();
      	while (it != lt.end())
      	{
      		cout << *it << " ";
      		++it;
      	}
      	cout << endl;
      }
      
      // 测试3:initializer_list花括号初始化、拷贝构造、赋值重载、const迭代器
      void test_list3()
      {
      	cout << "========== test_list3 花括号初始化/拷贝构造/赋值重载 ==========\n";
      	// 花括号初始化,调用initializer_list构造函数
      	asuo::list<int> lt1 = { 1,2,3,4,5,6 };
      	cout << "lt1 初始化结果:";
      	for (auto e : lt1)
      	{
      		cout << e << " ";
      	}
      	cout << endl;
      
      	// 拷贝构造(现代swap深拷贝写法)
      	asuo::list<int> lt2(lt1);
      	cout << "lt2 拷贝lt1结果:";
      	for (auto e : lt2)
      	{
      		cout << e << " ";
      	}
      	cout << endl;
      
      	// 赋值运算符重载
      	asuo::list<int> lt3 = { 10,20,30 };
      	lt1 = lt3;
      	cout << "lt1 = lt3赋值后lt1:";
      	for (auto e : lt1)
      	{
      		cout << e << " ";
      	}
      	cout << endl;
      
      	// 调用Print,测试const_iterator只读遍历
      	cout << "Print函数const迭代器遍历lt1:";
      	Print(lt1);
      	cout << "\n";
      }
      
      // 自定义结构体,测试operator->箭头运算符重载
      struct A
      {
      	int _a1;
      	int _a2;
      	A(int a1 = 0, int a2 = 0)
      		:_a1(a1)
      		, _a2(a2)
      	{
      	}
      };
      void test_list4()
      {
      	cout << "========== test_list4 自定义结构体 + operator->箭头重载 ==========\n";
      	asuo::list<A> lt;
      	// 花括号隐式构造A对象插入链表
      	lt.push_back({ 1,1 });
      	lt.push_back({ 2,2 });
      	lt.push_back({ 3,3 });
      
      	asuo::list<A>::iterator it = lt.begin();
      	while (it != lt.end())
      	{
      		// 两种写法等价:(*it)._a1 / it->_a1
      		//cout << (*it)._a1 << ":" << (*it)._a2 << endl;
      		cout << it->_a1 << ":" << it->_a2 << endl;
      		++it;
      	}
      	cout << "\n";
      }
      
      int main()
      {
      	// 依次放开对应函数测试不同功能
      	test_list1();
      	test_list2();
      	test_list3();
      	test_list4();
      	return 0;
      }

      更多推荐