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++
加加分为前置和后置,我们这里先实现以下前置++。