List--运算符重载实现list迭代器

list 基本框架的实现

结点的建构

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

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

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

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

 思考:为什么这里 ListNode 要加 <T> ?

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

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

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

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

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

结点初始化 

其实结点初始化就是 "创建新结点"

这里就完成初始化的工作:

① 将数据给给 data

② 将 next 和 prev 都置成空

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

结点初始化:

 

namespace ayf {
	 template<class T>
	 struct ListNode {
		 T _data;				  // 用来存放结点的数据
		 ListNode<T>* _next;      // 指向后继结点的指针
		 ListNode<T>* _prev;	  // 指向前驱结点的指针
 
		 ListNode(const T& data = T())   // 全缺省构造(初始化)
			 : _data(data)
			 , _next(nullptr)
			 , _prev(nullptr)
		 {}
	 };
}

 至此,结点已经写好了。

结点连接

设计好结点后,我们现在可以开始实现 list 类了。

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

现在,我们用 Node 就表示 ListNode<T> 了,这也符合我们之前的使用习惯。因为是带头(哨兵位)双向循环链表,我们先要带个头儿。我们先要把头结点 _pHead 给设计出来,而 _prev 和 _next 是默认指向头结点的。

代码:pHead:

namespace ayf {
	template<class T>
	class list {
		typedef ListNode<T> Node;      // 重命名为Node
 
	public:
		/* 构造函数:初始化头结点 */
		list() {
			_pHead = new Node();      // 开空间,调用ListNode()
			_pHead->_next = _pHead;   // 默认指向头结点
			_pHead->_prev = _pHead;   // 默认指向头结点
		}
 
	private:
		Node* _pHead;    // 头结点指针
	};
}

 push_back 尾插

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

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

 

双向带头循环链表,虽然我们没有定义 _pTail,但是找到尾结点真的是轻轻松松,因为双向带头循环链表真的是太简单了而且全是 Fucking O(1)​,

尾结点就是头结点的前驱指针,直接 _pHead->_prev, O(1)​ 的速去取就完事了!然后直接 new 一个新结点 new_node,自动调用我们刚才写的 "建构结点" struct ListNode

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

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

 

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

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

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

(new_node 的前驱和后继指针默认都是 nullptr,它后继的连接我们继续往下看)
Step3:拆线重缝:连接 new_node 和 _pHead

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

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

如此一来,我们的 "缝合操作" 就大功告成了,我们可以开始代码实现了。

代码:实现尾插操作

template<class T>
class list {
	typedef ListNode<T> Node;      // 重命名为Node
public:
 
/* 尾插:push_back */
void push_back(const T& x) {
	
	Node* pTail = _pHead->_prev;     // pHead的前驱就是pTail
	Node* new_node = new Node(x);    // 创建新结点(会调用构造,自动创建)
 
 
	//(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;
};

尾插写好了,我们来跑一下看看效果如何。

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

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

list 迭代器的实现

迭代器不一定都是原生指针? 

list 的重点是迭代器,因为这里的迭代器的实现和我们之前讲的实现方式都不同。

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

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

 

而这些所谓的 "链接" 其实都是我们想象出来的,实际上根本就不存在。而这些链接的含义只是 "我存的就是你的地址" ,所以我可以找到你的位置。

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

 

​ 而自带的 解引用* 和 ++ 的功能,是没法在链表中操作的。

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

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

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

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

迭代器的构造

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

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

operator++

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值