当前位置: 首页 > news >正文

十四、模拟实现 list 类

Ⅰ . list 基本框架的实现

01 结点的建立

为了实现链表,我们首先要做的应该是建立结点

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

代码实现:

namespace yxt
{// 建立结点template<class T>struct ListNode{T _data;ListNode<T>* _next;		// 指向后继节点ListNode<T>* _prev;		// 指向前驱节点};
}

这里为什么 ListNode 要加  <T> 呢?

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

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

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

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

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

02 结点初始化

其实结点初始化就是创建新结点,我们先不考虑开空间的事,完成初始化主要有两个方面:

① 将数据给 _data

② 将 _next 和 _prev 都置成空

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

代码实现:

namespace yxt
{// 建立结点template<class T>struct ListNode{T _data;ListNode<T>* _next;		// 指向后继节点ListNode<T>* _prev;		// 指向前驱节点// 结点初始化ListNode(const T& data = T()):_data(data),_next(nullptr),_prev(nullptr){}};
}

这样,结点就写好了

03 结点的连接

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

因为是带头双向循环链表,我们首先要把头节点设计出来

代码实现:

	/* list */template<class T>class list{typedef ListNode<T> Node;public:/* 构造函数 */list(){_pHead = new Node();_pHead->_next = _pHead;		// 默认指向头结点_pHead->_prev = _pHead;		// 默认指向头结点    }private:Node* _pHead;};

04 push_back

我们先实现一下 push_back,好让 list 先跑起来

步骤一:找到尾结点并创建新结点

虽然我们没有定义 _pTail,但对于双向带头循环链表来说,找到尾结点很容易,尾结点就是头结点的前驱指针,这里创建新结点也很容易,我们直接 new 一个新结点,自动调用我们刚才写的建立结点

步骤二:连接新结点与原链表

这里连接我们主要分两步:一是把新结点与尾结点连接起来 二是把新结点和头结点连接起来

我们直接来看代码

代码实现:

		/* push_back */void push_back(const T& x){// 找尾结点并建立新结点Node* _pTail = _pHead->_prev;Node* new_node = new Node(x);// 新结点和尾结点相连_pTail->_next = new_node;new_node->_prev = _pTail;// 新结点和头结点相连new_node->_next = _pHead;_pHead->_prev = new_node;}

尾插写好之后,我们调试一下:

	void list_test1(){list<int> l;l.push_back(1);l.push_back(2);l.push_back(3);l.push_back(4);}

调试结果如下: 

Ⅱ . list 迭代器的实现

01 引入

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

我们之前实现的 string 和 vector 的迭代器都是一个原生指针,实现很简单

但 list 是一个链表,空间上不连续,又如何实现呢?

在链表中,想要找到下一个结点,通常需要解引用和 ++

而自带的解引用和 ++ ,并不能满足我们的要求,但我们可以对其进行重载

02 迭代器的构造

代码实现:

	/* 定义迭代器 */template<class T>struct _list_iterator{typedef ListNode<T> Node;Node* _node;/* 迭代器的构造 */_list_iterator(Node* x):_node(x){}};

03 operator ++

加加分前置和后置,我们先实现前置

代码实现:

		/* ++it */_list_iterator<T>& operator++(){_node = _node->_next;return *this;}

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

因为出了作用域还在,所以可以返回引用

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

但返回的还是之前的值,这样就实现了后置++

因为前置++后置++都是 operator++,区分方式是后置++用占位符 (int) 占位

代码实现:

		/* it++ */_list_iterator<T>& operator++(int){_list_iterator<T> tmp(*this);_node = _node->_next;return tmp;}

04 operator *

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

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

为了使其支持修改的操作,我们这里用引用返回

代码实现:

		/* 解引用 */T& operator*(){return _node->_data;}

05 测试 实现 begin() 和 end()

有了 operator++ 和 operator* ,我们就可以来测试一下我们的迭代器了

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

而 end 返回的是最后一个数据的下一个位置,即 _pHead

代码实现:

		typedef _list_iterator<T> iterator;iterator begin(){return _pHead->_next;}iterator end(){return _pHead;}

因为迭代器要用到 != ,所以我们要实现操作符的重载

06 operator!=

如何判断是否相等呢?

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

代码实现:

		/* != */bool operator!=(const _list_iterator<T>& it){return _node != it._node;}

测试:

	void list_test1(){list<int> l;l.push_back(1);l.push_back(2);l.push_back(3);l.push_back(4);list<int>::iterator it = l.begin();while (it != l.end()){cout << *it << " ";++it;}cout << endl;}

运行结果如下:

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

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

list 的拷贝构造和赋值不需要自己实现,默认生成的即可

it2(it1)
it2 = it1 浅拷贝

当前迭代器赋值给另一个迭代器使不需要深拷贝的,浅拷贝即可

我们再谈谈析构为什么不需要自己实现

template<class T> 
struct __list_iterator 
{typedef ListNode<T> Node; Node* _node;...
}

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

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

所以它的释放和迭代器没什么关系

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

08 打印链表

我们刚才实现好了迭代器:

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

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

这里为了减少拷贝,使用引用返回,我们没有实现 const 迭代器,会导致没法遍历:

测试:

	void print_list(const list<int>& l){list<int>::iterator it = l.begin();while (it != l.end()){cout << *it << " ";++it;}cout << endl;}void list_test1(){list<int> l;l.push_back(1);l.push_back(2);l.push_back(3);l.push_back(4);print_list(l);}

运行结果:报错

const 迭代器和普通迭代器的区别是什么?

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

所以我们自然是要实现 const 迭代器

09 const 迭代器的实现

传统方法是把 list_iterator 直接 CV,然后改成 const 的

代码实现:

		/* 定义const迭代器 */template<class T>struct _const_list_iterator{typedef ListNode<T> Node;Node* _node;/* 迭代器的构造 */_const_list_iterator(Node* x):_node(x){}/* ++it */_const_list_iterator<T>& operator++(){_node = _node->_next;return *this;}/* it++ */_const_list_iterator<T>& operator++(int){_const_list_iterator<T> tmp(*this);_node = _node->_next;return tmp;}/* 解引用 */const T& operator*(){return _node->_data;}/* != */bool operator!=(const _const_list_iterator<T>& it){return _node != it._node;}

这里我们把 __list_iterator 都修改成 __const_list_iterator,

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

代码:们这里再在 list 中 typedef 一下 const 迭代器

	/* list */template<class T>class list{typedef ListNode<T> Node;public:/* 迭代器 */typedef _list_iterator<T> iterator;typedef _const_list_iterator<T> const_iterator;iterator begin(){return iterator(_pHead->_next);}iterator end(){return iterator(_pHead);}

const 迭代器和普通迭代器不是一个类型

不是迭代器是 const ,而是对象是 const,所以要调用 const 的 begin  和 end才行

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

代码实现:

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

测试:

	void print_list(const list<int>& l){list<int>::const_iterator it = l.begin();while (it != l.end()){cout << *it << " ";++it;}cout << endl;}void list_test1(){list<int> l;l.push_back(1);l.push_back(2);l.push_back(3);l.push_back(4);print_list(l);}

运行结果如下:

10 使用模板实现 const 迭代器

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

在定义 template 模板的时增加一个参数 Ref :

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

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

代码:之后我们在 list 中 typedef 的时候就可以传 T& 或 const T&

	public:/* 迭代器 */typedef _list_iterator<T, T&> iterator;typedef _const_list_iterator<T, const T&> 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& ,可读可写

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

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

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

模板重命名

时去编译,是编译不通过的。

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

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

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

代码实现:

	/* 定义迭代器 */template<class T, class Ref>struct _list_iterator{typedef ListNode<T> Node;typedef _list_iterator<T, Ref> Self;Node* _node;/* 迭代器的构造 */_list_iterator(Node* x):_node(x){}/* ++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;}/* 解引用 */Ref operator*(){return _node->_data;}/* != */bool operator!=(const Self& it){return _node != it._node;}};

箭头操作符

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

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

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

代码:比如是一个日期类,我们没有实现流提取

	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<Date> L;L.push_back(Date(2022, 5, 1));L.push_back(Date(2022, 5, 2));L.push_back(Date(2022, 5, 3));list<Date>::iterator it = L.begin();while (it != L.end()) {// cout << *it << " ";  假设我们没有实现流提取,我们自己访问cout << (*it)._year << "/" << (*it)._month << "/" << (*it)._day << endl;it++;}cout << endl;}

运行结果如下:

我们发现,在没有实现流提取的前提下,想遍历链表L

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

所以我们这里去实现一下 ->:

		/* 解引用 */Ref operator*(){return _node->_data;}T* operator->(){return &_node->_data;}

总结:所以类型重载 operator-> 时都会省略一个箭头

这里还面临一个问题——const 迭代器

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

我们还需要增加一个模板参数:Ptr

此时刚才 typedef 的 Self 就体现出价值了

我们只需要在 Self 中加个 Ptr:

我们直接把 operator-> 的返回值修改成 Ptr 就行了

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

代码:

	/* 定义迭代器 */template<class T, class Ref, class Ptr>struct _list_iterator{typedef ListNode<T> Node;typedef _list_iterator<T, Ref, Ptr> Self;Node* _node;/* 解引用 */Ref operator*(){return _node->_data;}Ptr operator->(){return &_node->_data;}};/* 链表 */template<class T>class list{typedef ListNode<T> Node;public:/* 迭代器 */typedef _list_iterator<T, T&, T*> iterator;typedef _list_iterator<T, const T&, const T*> const_iterator;};

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

11 反向迭代器的实现

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

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

代码:

namespace yxt
{// Iterator是哪个容器的迭代器,reverse_iterator<Iterator>就可以// 适配出哪个容器的反向迭代器。复用的体现template<class Iterator, class Ref, class Ptr>class reverse_iterator{typedef reverse_iterator<Iterator, Ref, Ptr> Self;public:reverse_iterator(Iterator it):_it(it){}Ref operator*(){Iterator prev = _it;return *--prev;}Ptr operator->(){return &operator*();}Self& operator++(){--_it;return *this;}Self operator++(int){Self tmp(*this);--_it;return tmp;}Self& operator--(){++_it;return *this;}Self operator--(int){Self tmp(*this);++_it;return tmp;}bool operator!=(const Self& l) const{return _it != l._it;}private:Iterator _it;};
}

Ⅲ . list 增删查改

01 在 pos 位置前插入 - insert

在 pos 位置插入,我们通过 pos 找到前驱 prev,之后创建新结点,再连接起来

代码实现:

		/* 在pos位置前插入 */void insert(iterator pos, const T& x){// 找到pos位置的结点Node* cur = pos._node;Node* cur_prev = cur->_prev;// 创建新结点Node* new_node = new Node(x);// 连接cur_prev->_next = new_node;new_node->_prev = cur_prev;new_node->_next = cur;cur->_prev = new_node;}

insert 以后,pos 是否失效?不会

优化:

		/* 在pos位置前插入 */iterator insert(iterator pos, const T& x){// 找到pos位置的结点Node* cur = pos._node;Node* cur_prev = cur->_prev;// 创建新结点Node* new_node = new Node(x);// 连接cur_prev->_next = new_node;new_node->_prev = cur_prev;new_node->_next = cur;cur->_prev = new_node;return iterator(new_node);}

有了 insert 之后,我们可以直接复用实现 push_back

代码实现:

		/* push_back */void push_back(const T& x){找尾结点并建立新结点//Node* _pTail = _pHead->_prev;//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);}

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

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

并完成连接,这就做到了尾插

02 push_front

代码实现:

		/* push_front */void push_front(const T& x){insert(begin(), x);}

03 删除 pos 位置的结点 erase

步骤如下:

① 找到 pos 的前驱和后继

② 释放 pos 位置的结点

③ 将已经删除的 pos 结点的前驱和后继连接

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

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

代码实现:

		/* 任意位置删除 */void erase(iterator pos){// 防止头结点被删assert(pos != end());// 找到pos位置的结点Node* cur = pos._node;Node* cur_prev = cur->_prev;Node* cur_next = cur->_next;// 删除pos位置的结点delete[] cur;cur = nullptr;// 连接cur_prev->_next = cur_next;cur_next->_prev = cur_prev;}

erase 以后,pos 是否失效?

一定会失效!因为结点的指针指向的结点被干掉了,这当然会失效

 我们可以学着文档里的处理方式 —— 返回刚刚被删除的元素的下一个元素

		/* 任意位置删除 */iterator erase(iterator pos){// 防止头结点被删assert(pos != end());// 找到pos位置的结点Node* cur = pos._node;Node* cur_prev = cur->_prev;Node* cur_next = cur->_next;// 删除pos位置的结点delete[] cur;cur = nullptr;// 连接cur_prev->_next = cur_next;cur_next->_prev = cur_prev;return iterator(cur_next);}

04 pop_back

代码实现:

		/* 尾删 */void pop_back(){erase(_pHead->_prev);}

当然你也可以这么写:

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

05 pop_front

代码实现:

		/* 头删 */void pop_front(){erase(begin());}

Ⅳ . 拷贝构造和赋值重载

01 list 同样涉及深浅拷贝问题

这里的拷贝构造是深拷贝还是浅拷贝?

	void list_test2(){list<int> l1;l1.push_back(1);l1.push_back(2);l1.push_back(3);l1.push_back(4);list<int> l2(l1);for (auto e : l2)cout << e << " ";cout << endl;}

运行结果如下:

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

浅拷贝导致 l1 和 l2 指向同一块地址,析构的时候导致同一块空间被释放两次

02 clear 清空链表中的所有数据

代码实现:

		/* 清空链表所有数据 */void clear(){iterator cur = begin();while (cur != end()){iterator del = cur++;delete del._node;}_pHead->_next = _pHead;_pHead->_prev = _pHead;}

测试:

	void list_test3(){list<int> l1;l1.push_back(1);l1.push_back(2);l1.push_back(3);l1.push_back(4);cout << "删除前:";print_list(l1);cout << "删除后:";l1.clear();print_list(l1);}

运行结果如下:

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

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

简化:

		/* 清空链表所有数据 */void clear(){iterator cur = begin();while (cur != end()){erase(cur++);}}

 03 析构

代码实现:

		~list(){clear();delete _pHead;_pHead = nullptr;}

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

	void list_test2(){list<int> l1;l1.push_back(1);l1.push_back(2);l1.push_back(3);l1.push_back(4);list<int> l2(l1);for (auto e : l2)cout << e << " ";cout << endl;}

运行结果:崩溃

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

04 拷贝构造

代码实现:传统写法

		/* 拷贝构造 */list(const list<T>& l){_pHead = new Node();_pHead->_next = _pHead;_pHead->_prev = _pHead;for (auto e : l)push_back(e);}

代码实现:现代写法

		list(const list<T>& l){_pHead = new Node();_pHead->_next = _pHead;_pHead->_prev = _pHead;list<T> tmp(l.begin(), l.end());swap(_pHead, tmp._pHead);}

05 赋值重载

代码实现:传统写法

		/* 赋值(传统写法) */list<T>& operator=(list<T> l){if (this != &l){clear();for (auto e : l)push_back(e);}return *this;}

 代码实现:现代写法

		/* 赋值(现代写法) */list<T>& operator=(list<T> l){swap(_pHead, l._pHead);return *this;}

Ⅴ . 完整代码

list.h

#pragma once
#include<iostream>
#include<assert.h>
using namespace std;namespace yxt
{/* 建立结点 */template<class T>struct ListNode{T _data;ListNode<T>* _next;		// 指向后继节点ListNode<T>* _prev;		// 指向前驱节点/* 结点初始化 */ListNode(const T& data = T()):_data(data), _next(nullptr), _prev(nullptr){}};/* 定义迭代器 */template<class T, class Ref, class Ptr>struct _list_iterator{typedef ListNode<T> Node;typedef _list_iterator<T, Ref, Ptr> Self;Node* _node;/* 迭代器的构造 */_list_iterator(Node* x):_node(x){}/* ++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;}/* 解引用 */Ref operator*(){return _node->_data;}Ptr operator->(){return &_node->_data;}/* != */bool operator!=(const Self& it){return _node != it._node;}};///* 定义const迭代器 *///template<class T>//struct _const_list_iterator//{//	typedef ListNode<T> Node;//	Node* _node;//	/* 迭代器的构造 *///	_const_list_iterator(Node* x)//		:_node(x)//	{}//	/* ++it *///	_const_list_iterator<T>& operator++()//	{//		_node = _node->_next;//		return *this;//	}//	/* it++ *///	_const_list_iterator<T>& operator++(int)//	{//		_const_list_iterator<T> tmp(*this);//		_node = _node->_next;//		return tmp;//	}//	/* 解引用 *///	const T& operator*()//	{//		return _node->_data;//	}//	/* != *///	bool operator!=(const _const_list_iterator<T>& it)//	{//		return _node != it._node;//	}//};/* 链表 */template<class T>class list{typedef ListNode<T> Node;public:/* 迭代器 */typedef _list_iterator<T, T&, T*> iterator;typedef _list_iterator<T, const T&, const T*> 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;		// 默认指向头结点    }/* push_back */void push_back(const T& x){找尾结点并建立新结点//Node* _pTail = _pHead->_prev;//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);}/* push_front */void push_front(const T& x){insert(begin(), x);}/* 在pos位置前插入 */iterator insert(iterator pos, const T& x){// 找到pos位置的结点Node* cur = pos._node;Node* cur_prev = cur->_prev;// 创建新结点Node* new_node = new Node(x);// 连接cur_prev->_next = new_node;new_node->_prev = cur_prev;new_node->_next = cur;cur->_prev = new_node;return iterator(new_node);}/* 任意位置删除 */iterator erase(iterator pos){// 防止头结点被删assert(pos != end());// 找到pos位置的结点Node* cur = pos._node;Node* cur_prev = cur->_prev;Node* cur_next = cur->_next;// 删除pos位置的结点delete[] cur;cur = nullptr;// 连接cur_prev->_next = cur_next;cur_next->_prev = cur_prev;return iterator(cur_next);}/* 尾删 */void pop_back(){erase(_pHead->_prev);}/* 头删 */void pop_front(){erase(begin());}/* 清空链表所有数据 */void clear(){iterator cur = begin();while (cur != end()){erase(cur++);}}/* 析构 */~list(){clear();delete _pHead;_pHead = nullptr;}template<class InputInterator>list(InputInterator first, InputInterator last){_pHead = new Node();_pHead->_next = _pHead;_pHead->_prev = _pHead;while (first != last){push_back(*first);++first;}}/* 拷贝构造(现代写法) */list(const list<T>& l){_pHead = new Node();_pHead->_next = _pHead;_pHead->_prev = _pHead;list<T> tmp(l.begin(), l.end());swap(_pHead, tmp._pHead);}/* 拷贝构造(传统写法) */list(const list<T>& l){_pHead = new Node();_pHead->_next = _pHead;_pHead->_prev = _pHead;for (auto e : l)push_back(e);}/* 赋值(传统写法) */list<T>& operator=(list<T> l){if (this != &l){clear();for (auto e : l)push_back(e);}return *this;}/* 赋值(现代写法) */list<T>& operator=(list<T> l){swap(_pHead, l._pHead);return *this;}private:Node* _pHead;};
}

test.cpp

#define _CRT_SECURE_NO_WARNINGS 1
#include"list.h"namespace yxt
{void print_list(const list<int>& l){list<int>::const_iterator it = l.begin();while (it != l.end()){cout << *it << " ";++it;}cout << endl;}void list_test1(){list<int> l;l.push_back(1);l.push_back(2);l.push_back(3);l.push_back(4);print_list(l);}void list_test2(){list<int> l1;l1.push_back(1);l1.push_back(2);l1.push_back(3);l1.push_back(4);list<int> l2(l1);for (auto e : l2)cout << e << " ";cout << endl;}void list_test3(){list<int> l1;l1.push_back(1);l1.push_back(2);l1.push_back(3);l1.push_back(4);cout << "删除前:";print_list(l1);cout << "删除后:";l1.clear();print_list(l1);}
}int main()
{//yxt::list_test1();yxt::list_test2();//yxt::list_test3();return 0;
}

reverse_list.h

#pragma once
#include"list.h"namespace yxt
{// Iterator是哪个容器的迭代器,reverse_iterator<Iterator>就可以// 适配出哪个容器的反向迭代器。复用的体现template<class Iterator, class Ref, class Ptr>class reverse_iterator{typedef reverse_iterator<Iterator, Ref, Ptr> Self;public:reverse_iterator(Iterator it):_it(it){}Ref operator*(){Iterator prev = _it;return *--prev;}Ptr operator->(){return &operator*();}Self& operator++(){--_it;return *this;}Self operator++(int){Self tmp(*this);--_it;return tmp;}Self& operator--(){++_it;return *this;}Self operator--(int){Self tmp(*this);++_it;return tmp;}bool operator!=(const Self& l) const{return _it != l._it;}private:Iterator _it;};
}

相关文章:

十四、模拟实现 list 类

Ⅰ . list 基本框架的实现 01 结点的建立 为了实现链表&#xff0c;我们首先要做的应该是建立结点 为了和真正的 list 进行区分&#xff0c;我们仍然在自己的命名空间内实现 代码实现&#xff1a; namespace yxt {// 建立结点template<class T>struct ListNode{T _d…...

JavaScript简介之引入方式

JavaScript 引入方式 提问&#xff1a;CSS的引入方式&#xff1f;在学习 JavaScript 语法之前&#xff0c;我们首先要知道在哪里写 JavaScript 才行。想要在 HTML 中引入 JavaScript&#xff0c;一般有 3 种方式。 外部 JavaScript 内部 JavaScript 元素事件 JavaScript&#…...

同一台电脑上安装不同版本的nodejs(搭配VSCode)

今天拉取了一个前后端分离的项目&#xff0c;运行前端的时候&#xff0c;出现node版本不匹配的情况。 本文章将从安装node.js开始到VSCode使用进行讲解 1、去官网下载node版本 以16版本为例&#xff0c;需要哪个版本&#xff0c;就在网址上把版本号替换即可 https://nodejs.o…...

python小游戏之摇骰子猜大小

最近学习Python的随机数&#xff0c;逻辑判断&#xff0c;循环的用法&#xff0c;就想找一些练习题&#xff0c;比如小游戏猜大小&#xff0c;程序思路如下&#xff1a; 附上源代码如下&#xff1a; 摇骰子的函数&#xff0c;这个函数其实并不需要传任何参数&#xff0c;调用后…...

C++入门——12继承

1.继承 继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段&#xff0c;它允许程序员在保持原有类特性的基础上进行扩展&#xff0c;增加功能&#xff0c;这样产生新的类&#xff0c;称派生类。继承呈现了面向对象程序设计的层次结构&#xff0c;体现了由简…...

Python做统计图之美

Python数据分析可视化 案例效果图 import pandas as pd import matplotlib.pyplot as plt import matplotlib# 数据 data {"房型": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],"住宅类型": ["普通宅", "普通宅", "普通宅", &q…...

激光雷达点云投影到图像平面

将激光雷达点云投影到图像平面涉及几何变换和相机模型的应用。以下是该过程的基本原理&#xff1a; 1. 坐标系转换 激光雷达生成的点云通常位于激光雷达的坐标系中&#xff0c;而图像则在相机坐标系中。为了将点云投影到图像上&#xff0c;首先需要将点云从激光雷达坐标系转换…...

[python]将anaconda默认创建环境python版本设置为32位的

首先看看gpt怎么回答的 装了Anaconda。如果尚未安装&#xff0c;可以从Anaconda官网下载适合你的操作系统的安装程序&#xff0c;并按照安装向导进行安装。 二、创建32位Python环境 在Anaconda中&#xff0c;你可以通过修改环境变量来尝试切换到32位模式&#xff08;尽管这并…...

Jmeter+Influxdb+Grafana平台监控性能测试过程(三种方式)

一、Jmeter自带插件监控 下载地址&#xff1a;Install :: JMeter-Plugins.org 安装&#xff1a;下载后文件为jmeter-plugins-manager-1.3.jar&#xff0c;将其放入jmeter安装目录下的lib/ext目录&#xff0c;然后重启jmeter&#xff0c;即可。 启动Jmeter&#xff0c;测试计…...

[创业之路-135] :ERP、PDM、EDM、Git各种的用途和区别,硬件型初创公司需要哪些管理工具?

目录 前言&#xff1a; 一、ERP&#xff08;企业资源计划&#xff09; 二、PDM&#xff08;产品数据管理系统&#xff09; 三、EDM&#xff08;文档管理系统&#xff0c;有时也指电子邮件营销&#xff09; 四、Git 总结 五、硬件研发、生产型企业需要哪些管理工具&#…...

通过剪枝与知识蒸馏优化大型语言模型:NVIDIA在Llama 3.1模型上的实践与创新

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…...

DOM型xss靶场实验

xss是什么&#xff1f; XSS是一种经常出现在web应用中的计算机安全漏洞&#xff0c;它允许恶意web用户将代码植入到提供给其它用户使用的页面中。比如这些代码包括HTML代码和客户端脚本。攻击者利用XSS漏洞旁路掉访问控制--例如同源策略(same origin policy)。这种类型的漏洞由…...

华为---端口隔离简介和示例配置

目录 1. 端口隔离概念 2. 端口隔离作用 3. 端口隔离优点 4. 端口隔离缺点 5. 端口隔离的方法和应用场景 6. 端口隔离配置 6.1 端口隔离相关配置命令 6.2 端口隔离配置思路 7. 示例配置 7.1 示例场景 7.2 网络拓扑图 7.3 基本配置 7.4端口隔离配置与验证 7.4.1 双…...

Android 架构模式之 MVC

目录 架构设计的目的对 MVC 的理解Android 中 MVC 的问题试吃个小李子ViewModelController 大家好&#xff01; 作为 Android 程序猿&#xff0c;MVC 应该是我们第一个接触的架构吧&#xff0c;从开始接触 Android 那一刻起&#xff0c;我们就开始接触它&#xff0c;可还记得我…...

节点使用简介:comfyui-photoshop

1、安装comfyui-photoshop 略过 一点要注意的是&#xff1a;在Photoshop上的安装增效工具&#xff0c;要通过Creative Cloud 桌面应用程序进行安装&#xff0c;才能成功在增效工具中显示&#xff0c;直接通过将文件解压到Plug-ins路径行不通&#xff08;至少对我来说行不通&am…...

使用Go语言将PDF文件转换为Base64编码

使用 Go 语言将 Base64 编码转换为 PDF 文件-CSDN博客本文介绍了如何使用 Go 语言将 Base64 编码转换为 PDF 文件&#xff0c;并保存到指定路径。https://blog.csdn.net/qq_45519030/article/details/141225772 在现代编程中&#xff0c;数据转换和编码是常见的需求。本文将介绍…...

XSS Game

关卡网址&#xff1a;XSS Game - Learning XSS Made Simple! | Created by PwnFunction 1.Ma Spaghet! 见源代码分析得&#xff0c;somebody接收参数&#xff0c;输入somebody111查看所在位置 使用input标签 <input onmouseoveralert(1337)> 2.Jefff jeff接收参数,在ev…...

???牛客周赛55:虫洞操纵者

题目描述 \,\,\,\,\,\,\,\,\,\,你需要在一个可以上下左右移动的 nnn\times nnn 棋盘上解开一个迷宫&#xff1a;棋盘四周都是墙&#xff1b;每个方格要么是可以通过的空方格 ′0′\sf 0′0′ &#xff0c;要么是不可通过的墙方格 ′1′\sf 1′1′ &#xff1b;你可以沿着空方格…...

Unity3D开发之OnCollisionXXX触发条件

A和B碰撞触发OnCollision函数条件如下&#xff1a; 1.A和B都要有collider。&#xff08;子物体有也可以&#xff09; 2.A和B至少有一个刚体&#xff08;Rigidbody&#xff09;组件&#xff0c;且刚体的isKinematic为false。如果为true不会触发。 3.挂载脚本的物体必须有刚体…...

spfa()算法(求最短路)

spfa算法是对bellman_ford算法的优化&#xff0c;大部分求最短路问题都可以用spaf算法来求。 注意&#xff1a; &#xff08;1&#xff09;如若图中有负权回路&#xff0c;不能用spfa算法&#xff0c;要用bellman_ford算法&#xff1b;若只有负权边&#xff0c;则可以用 spf…...

聊聊国产数据库的生态系统建设

生态系统是指在自然界中&#xff0c;生物与环境构成统一的整体&#xff0c;之间相互影响相互制约&#xff0c;并在一定时期内处于相对稳定的动态平衡状态。所谓数据库的生态系统&#xff0c;从用户的角度看&#xff0c;就是充分打通产品使用过程中上下游的关联&#xff0c;使其…...

JDK源码解析:LinkedList

1、背景 我们咨询一下腾讯混元大模型&#xff0c;什么是“LinkedList”。 以下是混元大模型的回答&#xff1a; LinkedList 是 Java 集合框架中的一种数据结构&#xff0c;它实现了 List 和 Deque 接口。LinkedList 是一个双向链表&#xff0c;这意味着每个元素都包含对前一个和…...

drawio的问题

drawio的问题 先给出drawio的链接https://app.diagrams.net/ 我在用overleaf写论文的过程中&#xff0c;发现了一个问题&#xff0c;就是使用drawio画好图之后&#xff0c;只能保存以下几个选项&#xff1a; 但是不管是什么类型&#xff0c;在overleaf上面图片都不显示。如果…...

零基础学习Redis(3) -- Redis常用命令

Redis是一个 客户端-服务器 结构的程序&#xff0c;Redis客户端和服务器可以在同一台主机上&#xff0c;也可以在不同主机上&#xff0c;客户端和服务器之间通过网络进行通信。服务器端负责存储和管理数据。客户端则可以通过命名对服务端的数据进行操作。 Redis客户端有多种&a…...

响应式Web设计:纯HTML和CSS的实现技巧-1

响应式Web设计&#xff08;Responsive Web Design, RWD&#xff09;是一种旨在确保网站在不同设备和屏幕尺寸下都能良好运行的网页设计策略。通过纯HTML和CSS实现响应式设计&#xff0c;主要依赖于媒体查询&#xff08;Media Queries&#xff09;、灵活的布局、可伸缩的图片和字…...

FrereRTOS事件组

文章目录 一、事件组概念与操作1、事件组的概念2、事件组的操作 二、事件组函数1、创建2、删除3、设置事件4、等待事件5、同步点 三、示例&#xff1a;广播四、示例&#xff1a;等待一个任意事件五、示例: 等待多个事件都发生 学校组织秋游&#xff0c;组长在等待&#xff1a; …...

【经典算法】BFS_最短路问题

目录 1. 最短路问题介绍2. 算法原理和代码实现(含题目链接)1926.迷宫中离入口最近的出口433.最小基因变化127.单词接龙675.为高尔夫比赛砍树 3. 算法总结 1. 最短路问题介绍 最短路径问题是图论中的一类十分重要的问题。本篇文章只介绍边权为1(或边权相同)的最简单的最短路径问…...

【题目/训练】:双指针

引言 我们已经在这篇博客【算法/学习】双指针-CSDN博客里面讲了双指针、二分等的相关知识。 现在我们来做一些训练吧 经典例题 1. 移动零 思路&#xff1a; 使用 0 当做这个中间点&#xff0c;把不等于 0(注意题目没说不能有负数)的放到中间点的左边&#xff0c;等于 0 的…...

LLVM - 编译器后端-指令选择

一&#xff1a;概述 任何后端的核心都是指令选择。LLVM 实现了几种方法&#xff1b;在本篇文章中&#xff0c;我们将通过选择有向无环图&#xff08;DAG&#xff09;和全局指令选择来实现指令选择。 在本篇文章中&#xff0c;我们将学习以下主题&#xff1a; • 定义调…...

ES+FileBeat+Kibana日志采集搭建体验

1.环境准备 需要linux操作系统&#xff0c;并安装了docker环境 此处使用虚拟机演示。&#xff08;虚拟机和docker看参考我之前写的文章&#xff09; VirtualBox安装Oracle Linux 7.9全流程-CSDN博客 VirtualBox上的Oracle Linux虚拟机安装Docker全流程-CSDN博客 简单演示搭建ES…...

Dockerfile常用指令详解

Dockerfile 是一个用于定义 Docker 镜像构建过程的脚本文件&#xff0c;其中包含了一系列指令&#xff0c;用于指定如何构建和配置镜像。以下是一些常用的 Dockerfile 指令及其示例用法&#xff1a; 1. FROM 指定基础镜像&#xff0c;Dockerfile 必须以该指令开始。 示例&am…...

【vue】浏览器兼容相关

Vue.js 是一个流行的前端 JavaScript 框架&#xff0c;它支持构建单页应用和复杂的用户界面。Vue.js 的核心库本身对浏览器的支持情况如下&#xff1a; Vue.js 2.x 最低支持版本&#xff1a;IE9 及以上版本。特性支持&#xff1a;ES5。兼容性&#xff1a;Vue 2.x 在发布时就考…...

【区块链+金融服务】基于区块链的区域股权金融综合服务平台 | FISCO BCOS应用案例

区域性股权市场是我国资本市场的重要组成部分&#xff0c;是多层次资本市场体系的基石。区块链技术与区域性股权市场 分散特征天然匹配&#xff0c;从新型金融基础设施层面为场外参与各方提供公共的可信服务&#xff0c;以技术手段完善市场基础条 件&#xff0c;弥补区域性短板…...

string字符串和json对象相互转换问题

//响应体String responseStr EntityUtils.toString(response.getEntity());log.debug("下单响应码:{},响应体:{}",statusCode,responseStr);if(statusCode HttpStatus.OK.value()){JSONObject jsonObject JSONObject.parseObject(responseStr);if(jsonObject.cont…...

【生成式人工智能-十一一个不修改模型就能加速语言模型生成的方法】

一个加速语言模型生成的方法 现在语言模型的一个弊端speculative decoding预言家预测的问题 speculative decoding 模块的实现方法NAT Non-autoregressive模型压缩使用搜索引擎 一些更复杂些的speculative decoding 实现方式 speculative decoding 是一个适用于目前生成模型的加…...

Rust 错误处理

Rust 错误处理 Rust 是一种系统编程语言,以其内存安全、高并发和实用性而著称。在 Rust 中,错误处理是一个核心概念,它通过提供 Result 和 Option 类型来鼓励开发者显式地处理可能出现的错误,而不是依赖异常机制。本文将深入探讨 Rust 中的错误处理机制,包括 Result 和 O…...

程序与进程 linux系统

程序与进程 程序 &#xff08; program &#xff09;&#xff1a; 通常为 binary program &#xff0c;放置在储存媒体中&#xff08;如硬盘、光盘、软盘、磁带等&#xff09;&#xff0c; 为实体文件的型态存在&#xff1b;二进制文件&#xff0c;比如静态 /bin/date…...

使用MongoDB构建AI:Story Tools Studio将生成式AI引入Myth Maker AI游戏

Story Tools Studio利用先进的生成式AI技术&#xff0c;打造沉浸式、个性化、无穷尽的情景体验。 Story Tools Studio创始人兼首席执行官Roy Altman表示&#xff1a;“我们的旗舰游戏Myth Maker AI采用的是我们自主研发的、以AI为驱动的专家指导型故事生成器MUSE&#xff0c;它…...

鸿蒙UIAbility组件概述(二)

鸿蒙UIAbility组件概述 UIAbility组件基本用法指定UIAbility的启动页面获取UIAbility的上下文信息 UIAbility组件与UI的数据同步使用EventHub进行数据通信使用AppStorage/LocalStorage进行数据同步 UIAbility组件间交互&#xff08;设备内&#xff09;启动应用内的UIAbility启动…...

Oracle(70)如何优化SQL查询?

优化SQL查询是数据库管理的重要部分&#xff0c;旨在提高查询性能&#xff0c;减少响应时间和资源消耗。以下是一些常见的SQL查询优化技术&#xff0c;结合代码示例详细说明。 1. 使用索引 索引是优化查询性能的最常见方法之一。索引可以显著减少数据检索的时间。 示例 假设…...

深度剖析:Jenkins构建任务无法中断的原因及解决方案

个人名片 &#x1f393;作者简介&#xff1a;java领域优质创作者 &#x1f310;个人主页&#xff1a;码农阿豪 &#x1f4de;工作室&#xff1a;新空间代码工作室&#xff08;提供各种软件服务&#xff09; &#x1f48c;个人邮箱&#xff1a;[2435024119qq.com] &#x1f4f1…...

【YOLO】常用脚本

目录 VOC转YOLO划分训练集、测试集与验证集 VOC转YOLO import os import xml.etree.ElementTree as ETdef convert(size, box):dw 1. / size[0]dh 1. / size[1]x (box[0] box[1]) / 2.0y (box[2] box[3]) / 2.0w box[1] - box[0]h box[3] - box[2]x x * dww w * dwy…...

Springboot IOC DI理解及实现+JUnit的引入+参数配置

一、JavaConfig 我们通常使用 Spring 都会使用 XML 配置&#xff0c;随着功能以及业务逻辑的日益复杂&#xff0c;应用伴随着大量的 XML 配置文件以及复杂的 bean 依赖关系&#xff0c;使用起来很不方便。 在 Spring 3.0 开始&#xff0c;Spring 官方就已经开始推荐使用 Java…...

CeresPCL 最小二乘插值(曲线拟合)

一、简介 在多项式插值时,当数据点个数较多时,插值会导致多项式曲线阶数过高,带来不稳定因素。因此我们可以通过固定幂基函数的最高次数 m(m < n),来对我们要拟合的曲线进行降阶。之前的函数形式就可以变为: 既然是最小二乘问题,那么就仍然可以使用Ceres来进行求解。 …...

【TCP/IP】自定义应用层协议,常见端口号

互联网中&#xff0c;主流的是 TCP/IP 五层协议 5G/4G 上网&#xff0c;是有自己的协议栈&#xff0c;要比 TCP/IP 更复杂&#xff08;能够把 TCP/IP 的一部分内容给包含进去了&#xff09; 应用层 可以代表我们所编写的应用程序&#xff0c;只要应用程序里面用到了网络通信…...

Frida 的下载和安装

首先要安装好 python 环境 安装 frida 和 工具包 pip install frida frida-tools 查看版本&#xff1a; frida --version 16.4.8 然后到 github 上下载对应 server &#xff08; 和frida 的版本一致 16.4.8&#xff09; Releases frida/frida (github.com) 查看手机或…...

后端开发刷题 | 链表内指定区间反转【链表篇】

描述 将一个节点数为 size 链表 m 位置到 n 位置之间的区间反转&#xff0c;要求时间复杂度 O(n)O(n)&#xff0c;空间复杂度 O(1)O(1)。 例如&#xff1a; 给出的链表为 1→2→3→4→5→NULL1→2→3→4→5→NULL, m2,n4 返回 1→4→3→2→5→NULL 数据范围&#xff1a; 链表…...

【NVMe系列-提问页与文章总结页面】

NVMe系列-提问页与文章总结页面 问题汇总NVMe协议是什么&#xff1f;PRP 与 PRP List是做什么的&#xff1f; 已写文章汇总 问题汇总 NVMe协议是什么&#xff1f; PRP 与 PRP List是做什么的&#xff1f; 已写文章汇总...

用生成器函数生成表单各字段

生成器函数生成表单字段是非常合适的用法,避免你要用纯javascript做后台时频繁的制作表单&#xff0c;而不能重复利用 //这里是javascript部分&#xff0c;formfiled.js //生成器函数对字段的处理&#xff0c;让各字段name\className\label\value\placeholder赋值到input的属性…...

【xilinx】O-RAN 无线电接口 - Vivado 2020.1 及更新工具版本的发行说明

描述 记录包含 O-RAN 无线电接口 LogiCORE IP 的发行说明和已知问题&#xff0c;包括以下内容&#xff1a; 一般信息已知和已解决的问题 解决方案 一般信息 可以在以下三个位置找到支持的设备&#xff1a; O-RAN 无线电接口 IP 产品指南&#xff08;需要访问O-RAN 安全站点&…...