【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器

    ​​​​​​  爆笑教程   《C++要笑着学》  火速订阅  

   本篇博客全站热榜排名:16

写在前面

我们在上一章说过,list 其实就是带哨兵位循环双向链表而已,这种链表虽然结构复杂,但是实现起来反而是最简单的,我们在数据结构专栏中有过详细的讲解。

当时我们是用C语言实现,这里对 list 的实现其实也是大同小异的。当然,我们重点还是倾向于去理解它的底层实现原理,所以我们将对其实现方式进行进一步地简化,并且按照我们自己习惯的命名风格去走。

我们之前已经模拟实现过 string vector 了,这是本专栏 STL 的第三个模拟实现,相信大家已经能够做到 "驾轻就熟" 了,因此在讲解的时,出现重复的知识点我们就一笔带过。我们将重点去讲解迭代器的实现!本章我们要对迭代器有一个新的认知,迭代器不一定就是一个原生指针,也有可能是一个自定义类型。本章我们将通过自定义类型的运算符重载去控制我们的迭代器的 "行为"。

复习链接:【数据结构】从零开始逐步实现带哨兵位循环双向链表


Ⅰ. list 基本框架的实现

0x00 结点的建构

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第1张图片​ 我们还是参考《STL源码剖析》,用 STL3.0 版本去实现一个阉割版的 list

既然是要实现链表,我们首先要做的应该是建构结点。

此外,为了和真正的 list 进行区分,我们这里仍然在自己的命名空间内实现。

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第2张图片​ 回想一下我们的《树锯结构》专栏中,双链表是如何定义的:

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第3张图片

当时我们使用 typedef 的,但我们在讲模板的时候说过,有时候 typedef 做不到 "真正的通用"。

❓ 思考:对于 list 的模拟实现,我们是否可以继续用 typedef 去定义其类型?

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第4张图片​ (范大将军从类模板章节连夜赶到这里怒斥 typedef )

复习:【C++要笑着学】泛型编程 | 函数模板 | 函数模板实例化 | 类模板

而我们即将要实现的 list,需要的肯定是 "通用的 list" ,像这种情况 typedef 就帮不上忙了。

so​,我们这里使用模板去解决:

代码:建构双链表的结点:

namespace chaos {
	 template    // 添加模板参数列表
	 struct ListNode {
		 T _data;				  // 用来存放结点的数据
		 ListNode* _next;      // 指向后继结点的指针
		 ListNode* _prev;	  // 指向前驱结点的指针
	 };
}

思考:为什么这里 ListNode 要加

解读:因为类模板不支持自动推类型。 结构体模板或类模板在定义时可以不加 <T>,但 使用时必须加 <T>。

准备好 _data,放置好前驱 _next 和后继结点 _prev 后,我们的结点就有了 "结构" ——

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第5张图片​ (我们将如此表示双链表)

我们知道,结构体 struct 在 C++ 中升级成了类,因此它也有调用构造函数的权利。

也就是说,在创建结构体对象的时会调用构造函数。

既然如此,结点的初始化工作,我们可以考虑写一个构造函数去初始化,岂不美哉?

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第6张图片​  (我们继续往下看)

0x01 结点初始化

其实结点初始化就是 "创建新结点" ,即我们之前讲数据结构时实现的 CreateNewList() 接口。

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第7张图片

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第8张图片​ 我们先不考虑开空间的事,这里就完成初始化的工作:

① 将数据给给 data

② 将 next prev 都置成空

这些任务我们可以写到 struct  ListNode 的构造函数中,我们还可以设计成全缺省,给一个匿名对象 T() 。如此一来,如果没有指定初识值,它就会按模板类型去给对应的初始值了。

结点初始化:

namespace chaos {
	 template
	 struct ListNode {
		 T _data;				  // 用来存放结点的数据
		 ListNode* _next;      // 指向后继结点的指针
		 ListNode* _prev;	  // 指向前驱结点的指针

		 ListNode(const T& data = T())   // 全缺省构造(初始化)
			 : _data(data)
			 , _next(nullptr)
			 , _prev(nullptr)
		 {}
	 };
}

至此,结点已经写好了。

0x02 结点连接

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第9张图片​ 设计好结点后,我们现在可以开始实现 list 类了。

考虑到我们刚才实现的 "结点" ListNode<T> 类型比较长,为了美观我们将其 typedef Node

现在,我们用 Node 就表示 ListNode<T> 了,这也符合我们之前的使用习惯。

因为是带头(哨兵位)双向循环链表,我们先要带个头儿。

我们先要把头结点 _pHead 给设计出来,而 _prev_next 是默认指向头结点的。

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第10张图片

 代码:pHead

namespace chaos {
	template
	class list {
		typedef ListNode Node;      // 重命名为Node

	public:
		/* 构造函数:初始化头结点 */
		list() {
			_pHead = new Node();      // 开空间,调用ListNode()
			_pHead->_next = _pHead;   // 默认指向头结点
			_pHead->_prev = _pHead;   // 默认指向头结点
		}

	private:
		Node* _pHead;    // 头结点指针
	};
}

0x03 push_back 尾插

还是按老规矩,我们先去实现一下最经典的 push_back 尾插,好让我们的 list 先跑起来。

​ 尾插我们需要做什么呢?我们来冷静分析一下:

 Step1:找到尾结点并创建新节点:

双向带头循环链表,虽然我们没有定义 _pTail,但是找到尾结点真的是轻轻松松,

因为双向带头循环链表真的是太简单了而且全特么是 Fucking O(1)​,

尾结点就是头结点的前驱指针,直接 _pHead->_prev, O(1)​ 的速度去取就完事了!

然后直接 new 一个新结点 new_node,自动调用我们刚才写的 "建构结点" struct ListNode

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第11张图片

 至此,我们就找到了尾结点,并准备好要插入的新节点了。

Step2:拆线重缝:连接 pTail 和 new_node

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第12张图片

pTail 的后继指针 _next 原来是指向 _pHead 的,因为我们插入了新结点,

所以我们改变 pTail 的后继指针的指向,让其指向 new_node

相对的,新结点 new_node 的前驱指针也是要指向 new_node 的,形成一个 "连接" 。

(new_node 的前驱和后继指针默认都是 nullptr,它后继的连接我们继续往下看)

Step3:拆线重缝:连接 new_node 和 _pHead

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第13张图片

一样的,这里我们要改变的是 new_node 的后继指针和 _pHead 的前驱指针的指向。

new_node _next 指向 _pHead,并将 _pHead _prev 指向 new_node 即可。

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第14张图片​ 如此一来,我们的 "缝合操作" 就大功告成了,我们可以开始代码实现了。

代码:实现尾插操作(注释为思路草图)

template
class list {
	typedef ListNode Node;      // 重命名为Node
public:

/* 尾插:push_back */
void push_back(const T& x) {
	/*
	*	  我们可以通过 pHead->prev,找到 pTail:
	*
	*     phead <-> ... <-> ptail
	*       ↑_________________↓
	* 
	*/
	Node* pTail = _pHead->_prev;     // pHead的前驱就是pTail
	Node* new_node = new Node(x);    // 创建新结点(会调用构造,自动创建)

	/*
	* 
	*  因为插入了新结点(new_node),所以我们需要把链表 “重新连接” 一下:
	* 
	*                             (A)
	*     pHead <-> ... <-> pTail <-> new_node 
	*       ↑_____________________________↓
	*                    (B)
	*/

	//(A) pTail 与 new_node 的链接
	pTail->_next = new_node;
	new_node->_prev = pTail;

	// (B) new_node 与 pHead 的链接
	new_node->_next = _pHead;
	_pHead->_prev = new_node;
}

private:
	Node* _pHead;
};

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第15张图片​ 尾插写好了,我们来跑一下看看效果如何。

 我们随便插入一些数据,然后打开监视窗口看看 push_back 的效果如何。

	 void test_list1() {
		 list L;
		 L.push_back(1);
		 L.push_back(2);
		 L.push_back(3);
		 L.push_back(4);
	 }

调试结果如下:

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第16张图片

即使我们链表为空,也是可以进行尾插操作的,这就是结构的优势。

Ⅱ. list 迭代器的实现

0x00 引入:什么!迭代器不一定都是原生指针?

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第17张图片​ list 的重点是迭代器,因为这里的迭代器的实现和我们之前讲的实现方式都不同。

我们之前讲的 string vector 的迭代器都是一个原生指针,实现起来是非常简单的。

但是 list 是一个链表,你的迭代器还能这样去实现吗?在空间上不是连续的,如何往后走?

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第18张图片

而这些所谓的 "链接" 其实都是我们想象出来的,实际上根本就不存在。

而这些链接的含义只是 "我存的就是你的地址" ,所以我可以找到你的位置。

而我要到下一个位置的重点是 —— 解引用能取到数据,++ 移动到下一位:

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第19张图片

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第20张图片​ 而自带的 解引用* ++ 的功能,是没法在链表中操作的。

但是,得益于C++有运算符重载的功能,我们可以用一个类型去对结点的指针进行封装!

然后重载运算符 operator++ operator* ,是不是就可以控制其解引用并 ++ 到下一个位置了?

回想:运算符重载就是能让自定义类型像内置类型一样使用,回想一下我们当时讲解日期类的实现,是如何 ++ 到下一天的?当时是我们自己对 operator++ 进行重载,去实现 "进位" 操作的,之后我们使用 ++ 就可以调用那个我们实现的函数。

所以,我们首先要做的是对这两个运算符进行重载!

0x01 迭代器的构造

代码:只需要用一个结点的指针就可以构造了:

template
struct __list_iterator {
	typedef ListNode Node;   // 重命名
	Node* _node;
    
    /* 迭代器的构造 */
	__list_iterator(Node* x)
		: _node(x) 
	{}
};

0x02 operator++

​ 加加分为前置和后置,我们这里先实现以下前置++

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第21张图片

代码:前置++的实现:

/* ++it */
__list_iterator& operator++() {
	_node = _node->_next;  // 让 _node 指向下一个结点
	return *this;  // 返回加加后的值
}

因为前置是直接改变本体,我们直接 return *this 即可。

因为除了作用域还在,所以可以用引用返回, __list_iterator<T>

对应的,后置++ 我们可以拷贝构造出一个 tmp 存储原来的值,这样虽然改变本体了,

但是返回的还是之前的值,这就实现了后置++。此外,因为前置++后置++都是 operator++

区分方式是后置++用占位符 (int) 占位,这些知识点我们在之前讲解日期类的时候都说过。

代码:后置++的实现:

/* it++ */
__list_iterator operator++(int) {
	__list_iterator tmp(*this);   // 拷贝构造一个tmp存储原来的值
	_node = _node->_next;  // 让自己++
	return tmp;   // 返回原来的值
}

0x03 operator*

​ 解引用就是取结点 _node 里的数据,

并且 operator* 和指针一样,不仅仅能读数据,还能写数据。

为了使 operator* 能支持修改的操作,我们这里用引用返回 & (返回 _node 中 _data 的别名)

代码:解引用的重载:

/* 解引用 */
T& operator*() {
	return _node->_data;  // 返回结点的数据
}

这样,你用 *it 也就支持修改的操作了:

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第22张图片

0x04 测试效果(实现一下 begin 和 end)

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第23张图片​ 有了 operator++operator* ,我们就可以来测试一下我们的迭代器了。

begin end 的位置,我们画个图去看:

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第24张图片

begin 是第一个存有效数据的结点,即 _pHead 的下一个位置的结点。

end 返回的是最后一个数据的下一个位置,即 _pHead(循环链表,懂得都懂)

代码:list 类中设计 beginend

template
class list {
	typedef ListNode Node;
public:
	typedef __list_iterator iterator;  // 重命名成iterator
	iterator begin() {
		return iterator(_pHead->_next);
	}
	iterator end() {
		return iterator(_pHead);
	}

	list() {...}
    void push_back(const T& x) {...}

private:
	Node* _pHead;
};

​ 因为判断迭代器要用到 != ,所以我们还要实现以下这两个操作符的重载。

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第25张图片

0x05 operator!=

❓ 思考:如何判断是否相等呢?

如果两个迭代器结点的指针指向的是同一个结点,那就说明是相等的迭代器。

​ 反之,如果不是就说明不是相等的迭代器!

代码:这里我们利用 bool 的性质直接 return 返回要判断的条件即可:

/* != */
bool operator!=(const __list_iterator& it) {
	return _node != it._node;  // 它们结点的指针不一样吗?T or F
}

OK,现在我们可以来测试一下我们的迭代器了:

	void test_list1() {
		list L;
		L.push_back(1);
		L.push_back(2);
		L.push_back(3);
		L.push_back(4);	

		list::iterator it = L.begin();
		while (it != L.end()) {
		 cout << *it << " ";
		 it++;
		}
		cout << endl;
	}

运行结果如下:

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第26张图片​ (大功告成)

总结:

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第27张图片

0x06 感受类型的意义

​ 经此一役,我们知道了 迭代器不一定是原生指针,也有可能是自定义类型的指针。

void f() {
    Node* pNode = _pHead->_next;
    iterator it = _pHead->_next;

    *pNode;
    *it;

    ++pNode;
    ++it;
}

Node* 原生指针和一个迭代器对象,它们占用的空间是一样大的,都是 4byte​,

并且存的值也是一样的,但是对它们使用运算符的意义和结果是不一样的。

这就是类型的意义,类型的力量!C++ 自定义类型、运算符重载,这一切都是有意义的。

0x07 迭代器的拷贝构造、赋值和析构

思考:拷贝构造和赋值重载是否需要自己实现?析构呢?

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第28张图片​ 先说结论 —— list 的拷贝构造和赋值不需要自己实现,默认生成的即可。

it2(it1)
it2 = it1 浅拷贝

当前迭代器赋值给另一个迭代器是不需要深拷贝的,浅拷贝就可以。

我们再来谈谈析构函数,为什么不需要自己实现。

template struct __list_iterator {
    typedef ListNode Node; 
    Node* _node;
    ...
}

迭代器这里虽然有一个结点的指针,但是它并不是迭代器管的,是链表 list 管的,

链表 list 的析构函数会把这个结点 _node 给释放掉的。

所以它的释放和迭代器没什么关系,所以我们不需要关心它的析构。

举个比较形象的例子:

这就好比你在家吃饭和在饭店吃饭,你在家吃饭,自己吃饭自己洗碗。

而你在饭店吃饭就不一样了,吃就行了,吃完是不需要自己洗碗的。(霸王餐除外啊)

你在这里管就是越俎代庖了,东厂管得了的我西厂要管,东厂管不了的我西厂更要管!

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第29张图片 (雨化田是吧,纯纯的 "多管闲事" )

你去饭店吃饭吃完还帮人家把碗洗了,什么绝世大好人……

当然你去写也没人拦你,写 STL3.0 的大哥就自己实现了拷贝构造,并且是浅拷贝,

也就是你不写,自动生成出来的那个浅拷贝,其实默认生成的就可以了。

大哥可能考虑到可能会要构造一个无参的迭代器的情况,所以自己去实现了一下:

(具体可以去看看的TL3.0的源代码,反正我是没找到有要构造无参迭代器的场景)

总结:迭代器是借助结点的指针访问修改链表的,结点是属于链表的,而不属于迭代器,所以不用去管它的释放问题。 因此,拷贝构造、赋值重载和析构函数,这些都不需要自己实现,默认生成的可以。

0x08 引入:print_list 链表打印函数

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第30张图片 我们刚才实现好了迭代器,我们是这么去用的:

		list::iterator it = L.begin();
		while (it != L.end()) {
			cout << *it << " ";
			it++;
		}
		cout << endl;

不用范围 for 的前提下去用迭代器似乎挺麻烦的,我们可以把它放到一个函数里:

这里考虑到减少拷贝,我们使用引用返回,我们之前也说过这种情况能用 const 就用 const

所以这里就成 const_iterator 了,而我们刚才实现的是普通迭代器,会导致没法遍历。

测试:我们可以来试试

	void print_list(const list& L) {
		list::const_iterator it = L.begin();
		while (it != L.end()) {
			cout << *it << " ";
			it++;
		}
		cout << endl;
	}

	void test_list2() {
		list L;
		L.push_back(2);
		L.push_back(4);
		L.push_back(6);
		L.push_back(8);

		print_list(L);
	}

运行结果:报错  (而且聪明的编译器都给你画波浪线提醒了)

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第31张图片

思考:想想本质 —— const 迭代器和普通迭代器的区别是什么?

解答:普通迭代器访问普通对象,可读可写;const 迭代器访问 const 对象,可读但不可写。

所以我们这里自然是 需要实现 const 迭代器,即实现一个 "可读但不可写" 的迭代器。

(可以 ++ 可以解引用,但解引用的时候不能修改)

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第32张图片

所以直接在 __list_iterator 里面重载一个 const 类型的 operator* 解决不了问题,

我们得重新实现一个 __const_list_iterator 出来。(当然,更好的方法我们放到后面讲)

0x09 const 迭代器的实现

传统的方法是把  list_iterator 这个类 CV 一下,然后把名称改成 __const_list_iterator

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第33张图片 (话不多说我们直接CV大法用起来)

 代码:定义 const 迭代器

/* 定义const迭代器 */
template
struct __const_list_iterator {
	typedef ListNode Node;
	Node* _node;

	__const_list_iterator(Node* x)
		: _node(x)
	{}
	/* 解引用 */
	const T& operator*() {
		return _node->_data;       // 返回结点的数据
	}
	/* ++it */
	__const_list_iterator& operator++() {
		_node = _node->_next;  // 让 _node 指向下一个结点
		return *this;  // 返回加加后的值
	}
	/* it++ */
	__const_list_iterator operator++(int) {
		__const_list_iterator tmp(*this);   // 拷贝构造一个tmp存储原来的值
		_node = _node->_next;
		return tmp;
	}
	/* != */
	bool operator!=(const __const_list_iterator& it) {
		return _node != it._node;  // 它们结点的指针不一样吗?T or F
	}
}

这里我们把 __list_iterator 都修改成 __const_list_iterator

并且对于解引用 operator* 的重载,我们将其改成 const 引用返回,这样就只能读不能写了。

 代码:然后我们这里再在 list typedef 一下 const 迭代器:

/* 定义链表 */
template
class list {
	typedef ListNode Node;      // 重命名为Node
public:
	/* 迭代器 */
	typedef __list_iterator iterator;
	typedef __const_list_iterator const_iterator;  //  重命名const迭代器

	iterator begin() {   
		return iterator(_pHead->_next);  // 调用 __list_iterator
	}
	iterator end() {
		return iterator(_pHead);
	}
 
    ...
}

解读:const 迭代器和普通迭代器就不是一个类型了,是另外一个类型。

我们说了,不是迭代器是 const,而是对象是 const,所以要调用 const begin end 才行,

所以还要写 __const_list_iterator 类型的 begin end,我们用 const 去修饰,限制它写的权限。

 代码:实现 const 类型的 begin end

//  const 迭代器的 begin 和 end
const_iterator begin() const {
	return const_iterator(_pHead->_next);  
}
const_iterator end() const {
	return const_iterator(_pHead);
}

搞定了,这种 CV 重新实现一个 __const_list_iterator 的方式确实是可以的。

测试代码:我们再来调用一下 print_list 函数:

	void print_list(const list& L) {
		list::const_iterator it = L.begin();
		while (it != L.end()) {
			cout << *it << " ";
			it++;
		}
		cout << endl;
	}

	void test_list2() {
		list L;
		L.push_back(2);
		L.push_back(4);
		L.push_back(6);
		L.push_back(8);

		print_list(L);
	}

运行结果:(运行成功)

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第34张图片

我们再来看看我们的 const 迭代器效果如何:

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第35张图片

这里 *it 调用的是 __const_list_iterator  operator*,它返回的是 const T&,限制了写了功能。

大功告成了,我们成功实现了 list const 迭代器,

​ 这种实现方式可以是可以,但是这么实现好像有点搓啊!代码是完全冗余的有木有!

这个 const 迭代器和普通迭代器也就是类型名称和返回值不一样而已……

有没有办法可以优化一下呢?我们继续往下看!

0x0A 妙用模板实现 const 迭代器

​ 通过加一个额外的模板参数去控制 operator 的返回值,你能想到吗?

!太妙了,我们来看看大哥是怎么做的 —— 在定义 template 模板的时增加一个参数 Ref 

这样的话,我们 operator* 的返回值我们不要用 T&了,我们改成 Ref

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第36张图片

也就是说,让 operator* 的返回值变成 Ref 这个模板参数!

代码:之后我们就可以在 list 中 typedef 的时候就可以做到 "分流" ,传 T&const T&

/* 定义链表 */
template
class list {
	typedef ListNode Node; 
public:
	/* 迭代器 */
	typedef __list_iterator iterator;
	typedef __list_iterator const_iterator;

	iterator begin() {   
		return iterator(_pHead->_next);
	}
	iterator end() {
		return iterator(_pHead);
	}
	const_iterator begin() const {
		return const_iterator(_pHead->_next);  
	}
	const_iterator end() const {
		return const_iterator(_pHead);
	}

情况一:Ref 是 T&,可读可写 

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第37张图片

解读:这里 test_list1 是一个普通对象,调用的自然是普通的 begin begin 返回的是普通迭代器 __list_iterator<T, T&>,第二个模板参数是 T&Ref 就是 T& 了。operator* 的返回值 Ref T& 了,这样就做到了可读可写了。

情况二:Ref 是 const T&,可读但不可写

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第38张图片

解读:比如这里的 print_list 就是一个 const 对象,它调用的就是 const beginconst begin 返回的是 const 迭代器 __list_iterator<T, const T&>,第一个值传的都是 T,第二个值 const T& 传给 Ref。那么 operator* 的返回值 Ref 就是 const T& 了,这样就做到了可读但不可写的。

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第39张图片​ 我们本来是要实现两个类的,但是这里就额外加一个模板参数就搞定了,

这就是高手写的代码,而我自己去写可能就是写一个重复的类去实现了。 

0x0B 将模板重命名

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第40张图片​ 此时去编译,是编译不通过的。

因为我们多定义了一个 Ref,所以  __list_iterator 中的所有类模板都得加上它,比如:

 【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第41张图片

这太烦了,马上我们甚至还要再加个模板参数 Ptr(剧透一下)

这样加来加去是不是太不方便了?我们来看看设计 STL 的大佬是怎么做的:

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第42张图片

把这些都 typedef 一下,这样我们就可以把 __list_iterator<T, Ref> 写成 self 了:

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第43张图片

代码:真的是非常方便,我们来替换一下:

/* 定义迭代器 */
template
struct __list_iterator {
	typedef ListNode Node;
	typedef __list_iterator self;    // 为了方便我们重命名为self

	Node* _node;

	__list_iterator(Node* x)
		: _node(x) 
	{}

	/* 解引用 */
	Ref operator*() {
		return _node->_data;       // 返回结点的数据
	}
	const T& operator*() const {
		return _node->_data;      // 返回结点的数据
	}
	/* ++it */
	self& operator++() {
		_node = _node->_next;  // 让 _node 指向下一个结点
		return *this;  // 返回加加后的值
	}
	/* it++ */
	self operator++(int) {
		self tmp(*this);   // 拷贝构造一个tmp存储原来的值
		_node = _node->_next;
		return tmp;
	}
	/* != */
	bool operator!=(const self& it) {
		return _node != it._node;  // 它们结点的指针不一样吗?T or F
	}
    
    // 顺手再把 -- 写了
	/* --it */
	self& operator--() {
		_node = _node->_prev;  
		return *this;
	}
	/* it-- */
	self operator--(int) {
		self tmp(*this); 
		_node = _node->_prev;
		return tmp;
	}
};

运行结果如下:(运行成功)

 【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第44张图片

0x0C 箭头操作符

迭代器是像指针一样的,所以要重载两个解引用。

为什么?指针如果指向的类型是原生的普通类型,要取对象是可以用解引用,

但是如果指向而是一个结构,并且我们又要取它的每一个成员变量,就像这样:

示例:比如是一个日期类,假设我们没有实现其流插入,我们自己访问

	struct Date {
		int _year;
		int _month;
		int _day;

		Date(int year = 1, int month = 1, int day = 1) 
			: _year(year)
			, _month(month)
			, _day(day) 
		{}
	};

	void test_list3() {
		list L;
		L.push_back(Date(2022, 5, 1));
		L.push_back(Date(2022, 5, 2));
		L.push_back(Date(2022, 5, 3));

		list::iterator it = L.begin();
		while (it != L.end()) {
			// cout << *it << " ";  假设我们没有实现流插入,我们自己访问
			cout << (*it)._year << "/" << (*it)._month << "/" << (*it)._day << endl;
			it++;
		}
		cout << endl;
	}

运行结果如下:

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第45张图片

我们发现,在我们没有实现日期类的流提取运算符的前提下,想去迭代链表 L,

我们就需要 *(it)._xxx 去访问,而大多数主流习惯应该是用 -> 去访问的:

(诚然,用 *. 去访问完全没有问题,这个在本人《维生素C语言》专栏中提到过)

所以我们这里可以去实现一下箭头操作符 ->

代码:其实现方式似乎有些出乎意料,思考下原理是什么?

/* 解引用 */
Ref operator*() {
	return _node->_data;       // 返回结点的数据
}
T* operator->() {
	return &_node->_data;     
}

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第46张图片

总结:所有类型重载 operator-> 时都会省略一个箭头。(后期讲智能指针还会再提)

​ 还没完,这里也面临一个问题  —— 就是我们刚才提到的 const 迭代器。

如果你是一个 const 迭代器你用箭头也是可以去修改数据的,基于这样的一个原因,

我们还要增加一个模板参数:Ptr   (刚才剧透的)

此时我们刚才 typedefself 是不是就体现出方便的价值了?

我们这里增加一个新的模板参数,其他地方都不用改,只需要 self 里加个 Ptr

太方便了!我们直接把 operator-> 的返回值修改成 Ptr 就行了,

到时候我们传一个 T* const T*Ptr 就做到适配普通迭代器和 const 迭代器的 operator-> 了。

​ (和 operator* 一样的原理,我们下面传的时候增加一个参数)

代码:

/* 定义迭代器 */
template
struct __list_iterator {
	typedef ListNode Node;
	typedef __list_iterator self;    // 为了方便我们重命名为self

	Node* _node;

	__list_iterator(Node* x)
		: _node(x) 
	{}

	/* 解引用 */
	Ref operator*() {
		return _node->_data;       // 返回结点的数据
	}
	Ptr operator->() {
		return &_node->_data;     
	}
    ...
};

/* 定义链表 */
template
class list {
	typedef ListNode Node;      // 重命名为Node
public:
	/* 迭代器 */
	typedef __list_iterator iterator;
	typedef __list_iterator const_iterator;
    
    ...
}

这里我们把传递的值增加一个 T*const T* ,如此一来就做到了完美的适配。

0x0D 反向迭代器的实现

​ 我们来看一下源代码是如何实现的:

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第47张图片

反向迭代器其实就是对正向迭代器的一种封装 —— 适配器模式(配接器模式)。

(这里我们先做一个简单的了解,我们将在下一章进行详细的探讨)

代码:反向迭代器的实现:

namespace chaos
{
	// Iterator是哪个容器的迭代器,reverse_iterator就可以
	// 适配出哪个容器的反向迭代器。复用的体现
	template 
	class reverse_iterator {
		typedef reverse_iterator self;
	public:
		reverse_iterator(Iterator it)
			:_it(it)
		{}

		Ref operator*() {
			//return *_it;
			Iterator prev = _it;
			return *--prev;
		}

		Ptr operator->() {
			return &operator*();
		}

		self& operator++() {
			--_it;
			return *this;
		}

		self& operator--() {
			++_it;
			return *this;
		}

		bool operator!= (const self& rit) const {
			return _it != rit._it;
		}

	private:
		Iterator _it;
	};
}

Iterator 是那个容器的迭代器,reverse_iterator<Iterator> 就可以适配出哪个容器的反向迭代器。复用的体现。

Ⅲ. list 增删查改的实现

0x00 在 pos 位置前插入 - insert

pos 位置插入,我们通过 pos 去找到前驱 prev,之后创建新结点,再进行 "缝合" 操作,

这个我们在前面讲 push_back 的时候详细说过了,这里不在细说。具体看图:

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第48张图片

代码: pos 位置前插入

/* 在pos位置前插入 */
void insert(iterator pos, const T& x) {
	Node* cur = pos._node;     // 找到pos位置的结点
	Node* cur_prev = cur->_prev;     // 因为要在pos位置前插入,所以要找到当前pos位置的前一个结点
	Node* new_node = new Node(x);  // 创建新节点

	// 缝合: cur_prev <-> new_node <-> cur
	cur_prev->_next = new_node;
	new_node->_prev = cur_prev;
	new_node->_next = cur;
	cur->_prev = new_node;
}

 ❓ 思考:erase 以后,pos 是否失效?不会。

⚡ 优化:

/* 在pos位置前插入 */
iterator insert(iterator pos, const T& x) {
	Node* cur = pos._node;     // 找到pos位置的结点
	Node* cur_prev = cur->_prev;     // 因为要在pos位置前插入,所以要找到当前pos位置的前一个结点
	Node* new_node = new Node(x);  // 创建新节点

	// 缝合: cur_prev <-> new_node <-> cur
	cur_prev->_next = new_node;
	new_node->_prev = cur_prev;
	new_node->_next = cur;
	cur->_prev = new_node;

    return iterator(new_node);
}

有了 insert,我们就可以让之前为了快速把 list 跑起来而实现的 push_back 用 insert 复用一下。

⚡ 代码复用:push_back


/* 尾插:push_back */
void push_back(const T& x) {
	//Node* pTail = _pHead->_prev;     // pHead的前驱就是pTail
	//Node* new_node = new Node(x);    // 创建新结点(会调用构造,自动创建)
	//
	//pTail->_next = new_node;
	//new_node->_prev = pTail;
	//new_node->_next = _pHead;
	//_pHead->_prev = new_node;

	insert(end(), x);   // 在end(pHead)前插入,即尾插
}

push_back 复用 insertpos 我们给 end() 。因为 end() 是头结点 _pHead

在头结点前面插入,insertcur_prev 就会代表尾结点,会在 cur_prev 后面插入 new_node

并完成 "缝合",这就做到了尾插。如果不明白这里为啥要传 end 作为 pos,可以看图理解:

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第49张图片

0x01 push_front 头插

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第50张图片 push_back 有了,我们顺手再把 push_front 写了。 

代码:push_front

/* 头插:push_front */
void push_back(const T& x) {
	insert(begin(), x);  // 在begin(头结点的下一个结点)前插入,即头插
}

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第51张图片

0x02 删除 pos 位置的结点 erase

​ 删除 pos 位置结点,步骤如下:

① 找到 pos 的前驱和后继

② 释放 pos 位置结点

③ 将已删除的 pos 结点的前驱和后继 "缝合" 

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第52张图片

注意:当然我们还要防止哨兵位头结点 _pHead 被删的情况,头不小心卸了就没法玩了。

这里我还是习惯用暴力的方式去解决,用 assert 断言处理。

代码:删除 pos 位置结点

/* 任意位置删除 */
void erase(iterator pos) {
	assert(pos != end());   // 防止头结点被删除

	Node* cur = pos._node;   // 找到pos位置的结点
	Node* cur_prev = cur->_prev;   // 找到pos的前驱
	Node* cur_next = cur->_next;   // 找到pos的后继

	// 删除cur
	delete[] cur;
    cur = nullptr;

	// 缝合:  cur_prev <-> cur(删) <-> cur_next
	cur_prev->_next = cur_next;
	cur_next->_prev = cur_prev;
}

❓ 思考:erase 以后,pos 是否失效?

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第53张图片​ 一定会失效!因为结点的指针指向的结点被干掉了,这当然会失效。

为了救迭代器,我们可以学着文档里的处理方式 —— 返回刚刚被删除的元素的下一个元素。

改进:erase

/* 任意位置删除 */
iterator erase(iterator pos) {
	assert(pos != end());   // 防止头结点被删除

	Node* cur = pos._node;   // 找到pos位置的结点
	Node* cur_prev = cur->_prev;   // 找到pos的前驱
	Node* cur_next = cur->_next;   // 找到pos的后继

	// 删除cur
	delete[] cur;

	// 缝合:  cur_prev <-> cur(删) <-> cur_next
	cur_prev->_next = cur_next;
	cur_next->_prev = cur_prev;

	return iterator(cur_next);
}

0x03 pop_back 尾删

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第54张图片​ 删除 pos 位置结点的 erase 写好了,我们实现 pop_back 也是易如反掌的。 

代码:直接复用就完事了:

/* 尾删 */
void pop_back() {
	erase(_pHead->_prev);  // 删除最后一个元素,即尾结点
}

因为是删除 pos 位置,我们传过去的得是要删的结点。

而尾删删除的是最后一个结点,即尾结点。_pHead->_prev 就是尾结点了,传过去即可。

当然你也可以这么写,效果是一样的:

/* 尾删 */
void pop_back() {
	erase(--end());  // 删除最后一个元素,即尾结点
}

我们知道 end() 是头结点 _pHead 的位置,我们对 end() 减减,就跑到尾节点的位置了:

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第55张图片

0x04 pop_front 头删

pop_front 头删,即删除头结点的下一个结点,即 begin() 位置的结点。

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第56张图片

代码:仍然是复用

/* 头删 */
void pop_front() {
	erase(begin());  // 删除头结点的下一个结点(即begin位置的结点)
}

Ⅳ. list 的拷贝构造和赋值重载

0x00 引入:list 的同样涉及深浅拷贝问题

❓ 思考:这里的拷贝构造是深拷贝还是浅拷贝?

	void test_list4() {
		list L1;
		L1.push_back(1);
		L1.push_back(2);
		L1.push_back(3);

		list L2(L1);
		for (auto e : L2) cout << e << " ";
	}

运行结果:

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第57张图片

这里默认生成的拷贝构造是浅拷贝:

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第58张图片

比如这里 浅拷贝导致 L1L2 指向同一块地址,析构的时候会导致一块空间被释放两次。

这些知识我们在之前讲解深浅拷贝的时候就说过,这里没崩仅仅是因为我们还没设计析构。

 【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第59张图片​ 这里只是勉强 "逃过一杰" 。

综上所述,我们这里依然要自己去实现一个拷贝构造,去完成 "深拷贝" 。

我们下面实现一下 ~list,再回来看看。

0x01 clear 清空链表中所有数据

在实现链表 list 的析构函数前,为了方便清空,我们先实现一下 clear() 

代码:清空链表数据

/* 清空链表所有数据 */
void clear() {
	// 利用迭代器去遍历整个链表
	iterator cur = begin();
	while (cur != end()) {
		iterator del = cur++;  // 巧妙利用后置++的特性
		delete del._node;      // 删除当前结点,后置++生效
	}

	// 删完之后我们还需要将其恢复到初始状态
	_pHead->_next = _pHead;
	_pHead->_prev = _pHead;
}

测试代码:

	void test_list5() {
		list L;
		L.push_back(1);
		L.push_back(2);
		L.push_back(3);
		cout << "删除前:";
		print_list(L);

		cout << "删除后:";
		L.clear();
		print_list(L);
	}

运行结果:

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第60张图片

当然,这里的删除结点我们也可以用 erase 去完成。

erase 可以省去我们将其恢复 _pHead 的前驱和后继指向自己的操作。

删除掉最后一个有效元素后, erase 的 "缝合操作" 自然会做到:【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第61张图片

代码:用迭代器 + erase 的方式去逐个访问并删除每个节点

void clear() {
	iterator cur = begin();
	while (cur != end()) {
		iterator del = cur++;
		erase(del);           // 直接反复调用erase删除结点
	}
}

⚡ 简化:还可以进一步简化:

/* 清空链表所有数据 */
void clear() {
	iterator cur = begin();
	while (cur != end()) {
		// iterator del = cur++;
		erase(cur++);   // 直接反复调用erase删除结点
	}
}

0x02 ~list 析构

实现了 clear 后,我们再去实现 list 的析构函数就很简单了。

我们只需要把哨兵位头结点 _pHead 给干掉就行了,并且记得要置成空指针。

代码:析构函数的实现

~list() {
	// 清空链表有效数据
	clear();
	// 干掉头结点
	delete _pHead;
	_pHead = nullptr;
}

实现好了析构函数,我们再回过头来测刚才 "逃过一劫" 的浅拷贝导致的两个结点指向同一地址的问题,现在问题就变得严重起来了,因为它会被析构两次:

		void test_list4() {
			list L1;
			L1.push_back(1);
			L1.push_back(2);
			L1.push_back(3);

			list L2(L1);
			for (auto e : L2) cout << e << " ";
		}

运行结果:(崩溃)

刚才得益于我们还没有去实现析构函数,让浅拷贝带来的问题 "逃过一杰" 。

但是现在可谓是在劫难逃了 ——

【C++要笑着学】list 核心框架接口的模拟实现 | 运算符重载实现list迭代器 | 妙用模板实现const迭代器_第62张图片

自动生成的拷贝构造是浅拷贝,为了解决这个问题,我们需要手动实现一个深拷贝的拷贝构造!

0x03 拷贝构造的实现

代码:传统写法

/* 拷贝构造:L2(L1) */
list(const list& L) {
	_pHead = new Node();
	_pHead->_next = _pHead;
	_pHead->_prev = _pHead;

	for (auto e : L) {
		push_back(e);
	}
}

代码:现代写法

template 
list(InputIterator first, InputIterator last) {
	_pHead = new Node();
	_pHead->_next = _pHead;
	_pHead->_prev = _pHead;

	while (first != last) {
		push_back(*first);
		first++;
	}
}
/* 拷贝构造(现代写法):L2(L1) */
list(const list& L) {
    _pHead = new Node();
	_pHead->_next = _pHead;
	_pHead->_prev = _pHead;

	list tmp(L.begin(), L.end());
	swap(_pHead, tmp._pHead);  // 交换两个结点的指针
}

0x04 赋值重载

代码:传统写法

/* 赋值:L2 = L1 */
list& operator=(list L) {
	if (this != &L) {
		clear();
		for (auto e : L) {
			push_back(e);
		}
	}
	return *this;
}

代码:现代写法

/* 赋值(现代写法):L2 = L1 */
list& operator=(list L) {
	swap(_pHead, L._pHead);
	return *this;
}

Ⅴ. 完整代码

0x00 list.h

(注:未采用申明与定义分离的形式)

#define _CRT_SECURE_NO_WARNINGS 1
#include 
#include 
using namespace std;

namespace chaos
{
	/* 定义结点 */
	template
	struct ListNode {
		T _data;				  // 用来存放结点的数据
		ListNode* _next;       // 指向后继结点的指针
		ListNode* _prev;	      // 指向前驱结点的指针

		ListNode(const T& data = T())   // 全缺省构造(初始化)
			: _data(data)
			, _next(nullptr)
			, _prev(nullptr)
		{}
	};

	/* 定义迭代器 */
	template
	struct __list_iterator {
		typedef ListNode Node;
		typedef __list_iterator self;    // 为了方便我们重命名为self
		
		typedef Ref reference;
		typedef Ptr pointer;

		Node* _node;

		__list_iterator(Node* x)
			: _node(x) 
		{}

		/* 解引用 */
		Ref operator*() {
			return _node->_data;       // 返回结点的数据
		}
		Ptr operator->() {
			return &_node->_data;     
		}

		/* ++it */
		self& operator++() {
			_node = _node->_next;  // 让 _node 指向下一个结点
			return *this;  // 返回加加后的值
		}
		/* it++ */
		self operator++(int) {
			self tmp(*this);   // 拷贝构造一个tmp存储原来的值
			_node = _node->_next;
			return tmp;
		}
		/* != */
		bool operator!=(const self& it) {
			return _node != it._node;  // 它们结点的指针不一样吗?T or F
		}

		/* --it */
		self& operator--() {
			_node = _node->_prev;  
			return *this;
		}
		/* it-- */
		self operator--(int) {
			self tmp(*this); 
			_node = _node->_prev;
			return tmp;
		}
	};

	///* 定义const迭代器 */
	//template
	//struct __const_list_iterator {
	//	typedef ListNode Node;
	//	Node* _node;

	//	__const_list_iterator(Node* x)
	//		: _node(x)
	//	{}
	//	/* 解引用 */
	//	const T& operator*() {
	//		return _node->_data;       // 返回结点的数据
	//	}
	//	/* ++it */
	//	__const_list_iterator& operator++() {
	//		_node = _node->_next;  // 让 _node 指向下一个结点
	//		return *this;  // 返回加加后的值
	//	}
	//	/* it++ */
	//	__const_list_iterator operator++(int) {
	//		__const_list_iterator tmp(*this);   // 拷贝构造一个tmp存储原来的值
	//		_node = _node->_next;
	//		return tmp;
	//	}
	//	/* != */
	//	bool operator!=(const __const_list_iterator& it) {
	//		return _node != it._node;  // 它们结点的指针不一样吗?T or F
	//	}
	//	/* --it */
	//	__const_list_iterator& operator--() {
	//		_node = _node->_prev;
	//		return *this;
	//	}
	//	/* it-- */
	//	__const_list_iterator operator--(int) {
	//		__list_iterator tmp(*this);
	//		_node = _node->_prev;
	//		return tmp;
	//	}
	//};

	/* 定义链表 */
	template
	class list {
		typedef ListNode Node;      // 重命名为Node
	public:
		/* 迭代器 */
		typedef __list_iterator iterator;
		typedef __list_iterator const_iterator;
		iterator begin() { 
			return iterator(_pHead->_next); 
		}
		iterator end() { 
			return iterator(_pHead); 
		}
		const_iterator begin() const {
			return const_iterator(_pHead->_next);  
		}
		const_iterator end() const {
			return const_iterator(_pHead);
		}

		/* 构造函数:初始化头结点 */
		list() {
			_pHead = new Node();
			_pHead->_next = _pHead;
			_pHead->_prev = _pHead;
		}
		list(size_t n, const T& val = T()) {   // 初始化n个结点
			_pHead = new Node();
			_pHead->_next = _pHead;
			_pHead->_prev = _pHead;
			for (size_t i = 0; i < n; i++) {
				push_back(val);
			}
		}
		list(int n, const T& val = T()) {   // 初始化n个结点
			_pHead = new Node();
			_pHead->_next = _pHead;
			_pHead->_prev = _pHead;
			for (int i = 0; i < n; i++) {
				push_back(val);
			}
		}

		template 
		list(InputIterator first, InputIterator last) {
			_pHead = new Node();
			_pHead->_next = _pHead;
			_pHead->_prev = _pHead;

			while (first != last) {
				push_back(*first);
				first++;
			}
		}
		/* 拷贝构造(现代写法):L2(L1) */
		list(const list& L) {
			_pHead = new Node();
			_pHead->_next = _pHead;
			_pHead->_prev = _pHead;

			list tmp(L.begin(), L.end());
			swap(_pHead, tmp._pHead);
		}

		/* 拷贝构造:L2(L1) */
		/*list(const list& L) {
			_pHead = new Node();
			_pHead->_next = _pHead;
			_pHead->_prev = _pHead;

			for (auto e : L) {
				push_back(e);
			}
		}*/

		///* 赋值:L2 = L1 */
		//list& operator=(list L) {
		//	if (this != &L) {
		//		clear();
		//		for (auto e : L) {
		//			push_back(e);
		//		}
		//	}
		//	return *this;
		//}


		/* 赋值(现代写法):L2 = L1 */
		list& operator=(list L) {
			swap(_pHead, L._pHead);
			return *this;
		}


		/* 在pos位置前插入 */
		void insert(iterator pos, const T& x) {
			Node* cur = pos._node;     // 找到pos位置的结点
			Node* cur_prev = cur->_prev;     // 因为要在pos位置前插入,所以要找到当前pos位置的前一个结点
			Node* new_node = new Node(x);  // 创建新节点

			// 缝合: cur_prev <-> new_node <-> cur
			cur_prev->_next = new_node;
			new_node->_prev = cur_prev;
			new_node->_next = cur;
			cur->_prev = new_node;
		}

		/* 尾插:push_back */
		void push_back(const T& x) {
			//Node* pTail = _pHead->_prev;     // pHead的前驱就是pTail
			//Node* new_node = new Node(x);    // 创建新结点(会调用构造,自动创建)
			//
			//pTail->_next = new_node;
			//new_node->_prev = pTail;
			//new_node->_next = _pHead;
			//_pHead->_prev = new_node;

			insert(end(), x);   // 在end(头结点)前插入,即尾插
		}

		/* 头插:push_front */
		void push_front(const T& x) {
			insert(begin(), x);  // 在begin(头结点的下一个结点)前插入,即头插
		}


		/* 任意位置删除 */
		iterator erase(iterator pos) {
			assert(pos != end());   // 防止头结点被删除

			Node* cur = pos._node;   // 找到pos位置的结点
			Node* cur_prev = cur->_prev;   // 找到pos的前驱
			Node* cur_next = cur->_next;   // 找到pos的后继

			// 删除cur
			delete[] cur;

			// 缝合:  cur_prev <-> cur(删) <-> cur_next
			cur_prev->_next = cur_next;
			cur_next->_prev = cur_prev;

			return iterator(cur_next);
		}

		/* 尾删 */
		void pop_back() {
			erase(--end());  // 删除最后一个元素,即尾结点
		}

		/* 头删 */
		void pop_front() {
			erase(begin());  // 删除头结点的下一个结点(即begin位置的结点)
		}

		/* 清空链表所有数据 */
		void clear() {
			 利用迭代器去遍历整个链表
			//iterator it = begin();
			//while (it != end()) {
			//	iterator del = it++;  // 巧妙利用后置++的特性
			//	delete del._node;  // 删除当前结点,后置++生效
			//}

			 删完之后我们还需要将其恢复到初始状态
			//_pHead->_next = _pHead;
			//_pHead->_prev = _pHead;

			iterator it = begin();
			while (it != end()) {
				// iterator del = it++;
				erase(it++);   // 直接反复调用erase删除结点
			}
		}

		~list() {
			// 清空链表有效数据
			clear();
			// 干掉头结点
			delete _pHead;
			_pHead = nullptr;
		}

	private:
		Node* _pHead;
	};

		void print_list(const list& L) {
			list::const_iterator it = L.begin();
			while (it != L.end()) {
				// *it = 100;
				cout << *it << " ";
				it++;
			}
			cout << endl;
		}


		void test_list1() {
			list L;
			L.push_back(1);
			L.push_back(2);
			L.push_back(3);
			L.push_back(4);	

			list::iterator it = L.begin();
			while (it != L.end()) {
				*it *= 2;
				cout << *it << " ";
				it++;
			}
			cout << endl;
		}

		void test_list2() {
			list L;
			L.push_back(2);
			L.push_back(4);
			L.push_back(6);
			L.push_back(8);

			print_list(L);
		}


		struct Date {
			int _year;
			int _month;
			int _day;

			Date(int year = 1, int month = 1, int day = 1) 
				: _year(year)
				, _month(month)
				, _day(day) 
			{}
		};

		void test_list3() {
			list L;
			L.push_back(Date(2022, 5, 1));
			L.push_back(Date(2022, 5, 2));
			L.push_back(Date(2022, 5, 3));

			list::iterator it = L.begin();
			while (it != L.end()) {
				cout << it->_year << "/" << it->_month << "/" << it->_day << endl;
				it++;
			}
			cout << endl;
		}

		void test_list4() {
			list L1;
			L1.push_back(1);
			L1.push_back(2);
			L1.push_back(3);

			list L2(L1);
			for (auto e : L2) cout << e << " ";
		}

		void test_list5() {
			list L;
			L.push_back(1);
			L.push_back(2);
			L.push_back(3);
			cout << "删除前:";
			print_list(L);

			cout << "删除后:";
			L.clear();
			print_list(L);
		}
}

0x01 test.cpp

#define _CRT_SECURE_NO_WARNINGS 1
#include "list.h"

int main(void)
{
	chaos::test_list1();

	return 0;
}

0x02 reverse_iterator.h

#define _CRT_SECURE_NO_WARNINGS 1
#include "list.h"

namespace chaos
{
	// Iterator是哪个容器的迭代器,reverse_iterator就可以
	// 适配出哪个容器的反向迭代器。复用的体现
	//template 

	/* 定义反向迭代器 */
	template 
	class reverse_iterator {
		typedef reverse_iterator self;
		typedef typename Iterator::Ref reference;  
		typedef typename Iterator::Ptr pointer;
	public:
		reverse_iterator(Iterator it)
			:_it(it)
		{}

		Ref operator*() {
			//return *_it;
			Iterator prev = _it;
			return *--prev;
		}

		Ptr operator->() {
			return &operator*();
		}

		self& operator++() {
			--_it;
			return *this;
		}

		self& operator--() {
			++_it;
			return *this;
		}

		bool operator!= (const self& rit) const {
			return _it != rit._it;
		}

	private:
		Iterator _it;
	};
}


 [ 笔者 ]   王亦优
 [ 更新 ]   2022.3.
❌ [ 勘误 ]   /* 暂无 */
 [ 声明 ]   由于作者水平有限,本文有错误和不准确之处在所难免,
              本人也很想知道这些错误,恳望读者批评指正!

参考资料 

C++reference[EB/OL]. []. http://www.cplusplus.com/reference/.

Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .

百度百科[EB/OL]. []. https://baike.baidu.com/.

比特科技. C++[EB/OL]. 2021[2021.8.31]. 

list容器模拟实现(一)—— push_back添加数据.[]. _abs(ln(1+NaN))

你可能感兴趣的:(《C++要笑着学》,c++,链表,list)