<C++> list模拟实现

目录

前言

一、list的使用

1. list的构造函数

2. list iterator的使用

3. list capacity

4. list modifiers 

5. list的算法

1. unique​

2. sort

3. merge

4. remove

5. splice 

二、list模拟实现

1. 设置节点类 && list类

2. push_back 

3. 迭代器 

重载 *

重载前置 ++

重载后置++

重载前置--

重载后置-- 

重载 != 

重载 ==  

begin() && end() 

拷贝构造 && 析构函数

const迭代器

4. insert 

5. erase

6. size

7. push_back && push_front && pop_back && pop_front 

8. clear 

9. list的析构函数 

10. list的拷贝构造函数 

11. empty_init 

12. list的赋值运算符重载

总结


前言

        前面学习了string、vector的模拟实现,其两者相似,但今天要学习的list就又有新的不同

列表是序列容器,允许在序列中的任意位置进行恒定时间插入和擦除操作,以及双向迭代。

列表容器实现为双向链表;双向链表可以将它们包含的每个元素存储在不同且不相关的存储位置。排序通过与每个元素的关联在内部保持,该链接指向它前面的元素,并链接到它后面的元素。

它们与forward_list非常相似:主要区别在于forward_list对象是单链表,因此它们只能向前迭代,以换取更小、更高效。

与其他基本标准序列容器(数组、vector和 deque)相比,列表在已经获得迭代器的容器内任何位置插入、提取和移动元素方面通常表现更好,因此在大量使用这些元素的算法(如排序算法)中也是如此。

与其他序列容器相比,
lists 和 forward_lists 的主要缺点是它们无法通过其位置直接访问元素;例如,要访问列表中的第六个元素,必须从已知位置(如开始或结束)迭代到该位置,这需要它们之间的距离的线性时间。它们还会消耗一些额外的内存,以保持与每个元素关联的链接信息(这可能是大型小元素列表的重要因素)。


一、list的使用

1. list的构造函数

构造函数(constructor)接口说明
list()构造空的list
list (size_type n, const value_type& val = value_type() )构造的list中包含n个值为val的元素
list (const list& x)拷贝构造函数
list (lnputlterator first, inputlterator last)用 [first, last)区间中的元素构造list

2. list iterator的使用

函数声明接口声明
begin + end返回第一个元素的迭代器+返回最后一个元素下一个位置的迭代器
rbegin + rend返回第一个元素的reserve_iterator,即end位置,返回最后一个元素下一个位置的reserve_iterator,即begin位置

3. list capacity

函数声明接口说明
empty检查list是否为空,是返回true,否则返回false
size返回list中有效结点的个数

4. list modifiers 

函数声明接口说明
push_front
在list首元素前插入值为val的元素
pop_front
删除list中第一个元素
push_back
在list尾部插入值为val的元素
pop_back
删除list中最后一个元素
insert
在list position 位置中插入值为val的元素
erase
删除list position位置的元素
swap
交换两个list中的元素
clear
清空list中的有效元素

        insert、erase 与string的 insert 不同,string的insert形参是下标,而vector、list的形参都是迭代器,但是list和vector还是有不同的,因为vector在第i个位置插入是begin() + i,而list就不能使用迭代器加数字,因为链表物理空间不连续,要实现加的代价有点大。

        要实现迭代器的改变,只能循环自加(重载的++)

	std::list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	auto it = lt.begin();
	for (size_t i = 0; i < 4; i++)
	{
		++it;
	}
	lt.insert(it, 100);
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;

        如果我们想在元素3之前插入一个数据,我们应该使用find函数来查找,但是我们可以发现vector、list都没有find函数,这是因为STL将容器和算法分开了,提出了容器公共的算法。

        因为容器数据一般都是私有的,外部不能访问,但是各容器都是通过迭代器访问的,C++通过迭代器提取出了容器公共的算法,迭代器没有暴露原容器的细节,不管是树还是数组...,都是通过同样的方式访问

	it = c.begin();
	while (it != c.end())
	{
		cout << *it << " ";
	}

        循环条件也是关键,并不是 it < c.end( )  而是 it != c.end( ),这是因为容器的存储方式不一定连续存储

        

        【first,last)

        所以公共算法是通过迭代器实现的,例如find函数

template<class InputIterator, class T>
  InputIterator find (InputIterator first, InputIterator last, const T& val)
{
  while (first!=last) {
    if (*first==val) return first;
    ++first;
  }
  return last;
}

        例如:

auto it = find(lt.begin(), lt.end(), 3);
if (it != lt.end())
{
	lt.insert(it, 100);
}

for (auto e : lt)
{
	cout << e << " ";
}

cout << endl;

        因为list不需要扩容,节点的地址不改变,所以list的insert就不存在失效的问题

        同理分析erase,迭代器指向的节点被释放,那么迭代器一定是失效的

5. list的算法

        迭代器从功能上来说,分为:单向、双向、随机 三类迭代器,它是由容器的底层结构决定的

单向:只能++            forward_list / 哈希

双向:可以++ / --      list / map / set

随机:++ / -- / + / -    vector / string / deque

        单向迭代器访问自由度小于双向小于随机。所以,单向迭代器可以使用的情况,双向、随机迭代器都可以访问。

        对于不同的容器,迭代器种类不同,是否使用alrorithm的算法情况也不同,例如sort函数,实现原理是快排,sort的形参只能使用随机迭代器

	std::list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	lt.push_back(5);

	//错误,随机sort需要随即迭代器,而list是双向迭代器
	//sort(lt.begin(), lt.end());
	for (auto e : lt)
		cout << e << " ";
	cout << endl;

	lt.reverse();
	lt.sort();

	for (auto e : lt)
		cout << e << " ";
	cout << endl;

        list的sort实现原理是归并排序,但是一旦数据量增大,list的sort就不合适了,效率还不如转到vector,vector排序后再转回list。

所以,容器所提供的接口都是有讲究的,vector不提供专门的头插头删(只是复用insert),是因为如果它频繁的头插头删那么就不应该使用vector,而是list。

再比如,list的sort,因为迭代器的原因,list需要自己实现sort,但是list它不是一个适合多次排序的容器,vector等随机迭代器才适合sort的快排算法,所以list的sort只适合少量数据的排序,数据量如果很大,那么就考虑更改容器来提高效率。

 1. unique

  • 去重函数,注意要先排序 ,不排序去重效率很低
  • 使用双指针进行去重操作

 2. sort

  • 双向迭代器

3. merge

  • 两个有序链表的合并 
  • 第二个参数是仿函数

4. remove

  •  等效于find + erase
  • 如果没找到,什么也不做,不会报错

5. splice 

  • 转接链表,把 list1 的节点取下来,接到 list2 上 ’
  • 可以转接到自身,但是不能重叠,重叠会陷入死循环

三个重载函数功能:

        1. 将链表x全部转移到position之前位置

        2. 将链表x的 i 迭代器处节点转移到position之前位置,即只转移一个节点

        3. 将链表x从first到last范围的节点全部转移到position之前位置

例:

int main()
{
	std::list<int> mylist1, mylist2;
	std::list<int>::iterator it;

	// set some initial values:
	for (int i = 1; i <= 4; ++i)
		mylist1.push_back(i);      // mylist1: 1 2 3 4

	for (int i = 1; i <= 3; ++i)
		mylist2.push_back(i * 10);   // mylist2: 10 20 30

	it = mylist1.begin();
	++it;                         // points to 2


	//将list2全部接在list1的2之前
	mylist1.splice(it, mylist2);
	//部分转移
	mylist1.splice(it, mylist2, ++mylist2.begin());
	mylist1.splice(it, mylist2, ++mylist2.begin(), mylist2.end());

    //转移到自身
	mylist1.splice(mylist1.begin(), mylist1, ++mylist1.begin(), mylist2.end());

	return 0;
}

二、list模拟实现

1. 设置节点类 && list类

  • list_node采用struct类,因为struct默认是公有,用class也行,但是要加上public,如果不加公有条件,后续使用就会较为麻烦,还要搞友元
template<class T>
struct list_node
{
    list_node<T>* _next;
    list_node<T>* _prev;
    T _val;
};
  • C++的struct已经成为类,并且还有模板,所以list_node不是类型,list_node<T>才是类型,为了避免书写时忘记<T>,我们重命名
  • list成员变量即为头节点
	private:
		Node* _head;

namespace my_list
{
	template<class T>
	struct list_node
	{
		list_node<T>* _next;
		list_node<T>* _prev;
		T _val;
	};

	template<class T>
	class list
	{
		typedef list_node<T> Node;
	private:
		Node* _head;
        size_t _size;
	};

2. push_back 

  • 开辟新节点(new Node ),新节点类型是一个类,new的时候会自动调用Node的默认构造,所以我们在list_node类中定义默认构造函数来构造新节点
namespace my_list
{
	template<class T>
	struct list_node
	{
		list_node<T>* _next;
		list_node<T>* _prev;
		T _val;

		//默认构造
		list_node(const T& val = T())
			:_next(nullptr)
			,_prev(nullptr)
			,_val(val)
		{}

	};

	template<class T>
	class list
	{
		typedef list_node<T> Node;
	public:
		list()
		{
			_head = new Node;
			_head->_prev = _head;
			_head->_next = _head;
            _size = 0;
		}

		void push_back(const T& x)
		{
			Node* tail = _head->_prev;
			Node* newnode = new Node(x);
			
			tail->_next = newnode;
			newnode->_prev = tail;

			newnode->_next = _head;
			_head->_prev = newnode;
		}
	private:
		Node* _head;
	};
}

3. 迭代器类 

思考:直接将 Node* 作为 iterator 可以吗?

答案:不能!

原因

        1. 首先,Node*作为迭代器,它解引用得到的是结构体,不是数据!

        2. 其次,节点的地址不连续,迭代器++不能使迭代器移动到下一个节点!

        之前的vector、string的元素指针天生就有两点优势,所以它们的迭代器就用指针实现

解决:

        使用运算符重载!将iterator封装为一个类!

        重载运算符 ++ 和 *

        iterator是一个类,它的成员是一个节点的指针,iterator类就是封装了结点指针、和获取节点val值、使迭代器移动等方法的类

list<int>::iterator it = lt.begin();

        属于类域的是在类内部定义的,一种为typedef的内嵌类型,另一种是内部类

	template<class T>
	struct __list_iterator
	{
		typedef list_node<T> Node;
		//成员
		Node* _node;

		//默认构造
		__list_iterator(Node* _node)
			:_node(node)
		{}

	};
  • 在 list 内部,我们将__list_iterator<T> 重定义为 iterator,切记用public修饰,因为我们需要在类外部使用迭代器
  • 之所以将迭代器命名为__list_iterator<T>是为了避免重名,因为后面的容器都是需要iterator的

重载 *

  • 迭代器解引用应为元素值
		//引用返回,因为节点生命周期不在该函数内
		//可读可写版本
		T& operator*()
		{
			return _node->_val;
		}

重载前置 ++

  • 返回this解引用,*this是 iterator 类对象,返回*this可以之后再次解引用、++等操作
		__list_iterator<T>& operator++()
		{
			_node = _node->_next;
			return *this;
		}

重载后置++

        *this是__list_iterator<T> 类实例化的对象,使用*this拷贝构造tmp

        此时我们还没写拷贝构造,所以调用的是编译器默认生成的拷贝构造,对内置类型逐字节拷贝,也就是tmp的_node同this->_node指向的节点相同,不需要深拷贝,只是借用结构体来获取val

        意思就是拷贝构造一个新的类对象tmp,tmp的_node指向原节点,而this指向的节点往后移动,就这么个关系

		//后置++
		__list_iterator<T> operator++(int)
		{
			__list_iterator<T> tmp(*this);

			_node = _node->_next;
			return tmp;
		}

重载前置--

        这里将__list_iterator<T>   typedef为self(自己),方便后续修改iterator类模板,不需要再在各个函数处手动修改返回值

		//前置--
		self& operator--()
		{
			_node = _node->_prev;
			return *this;
		} 

重载后置-- 

		//后置--
		self operator--(int)
		{
			self tmp(*this);

			_node = _node->_prev;
			return tmp;
		}

重载 != 

  • 这里注意const,因为比较!=时,右边是end(), end() 函数返回的是匿名对象的拷贝,临时对象具有常性,所以形参需要const修饰,不然会传参失败(const引用权限变大)
		//这里注意const,因为比较!=时,右边时end(),该函数返回的是临时对象,临时对象具有常性
		bool operator!=(const __list_iterator<T>& it)
		{
			return _node != it._node;
		}

重载 ==  

		bool operator==(const __list_iterator<T>& it)
		{
			return _node == it._node;
		}

begin() && end() (list类)

  • 直接返回指针类型会隐式类型转换为iterator类,我们也可以直接构造一个匿名对象,返回匿名对象
  • 遵循左闭右开原则,_head为头节点
		typedef __list_iterator<T> iterator;

        iterator begin()
		{
			//匿名对象,临时构造一个iterator类,因为_head是一个Node指针类型
			return iterator(_head->next);
		}

		iterator end()
		{
			return iterator(_head);
		}

 拷贝构造 && 析构函数(iteator类)

list<int>::iterator it = lt.begin();

1. 这里就是拷贝构造 it ,但是成员是内置类型Node*,浅拷贝正确吗?

        答:我们所需要的就是那个节点的指针,就是需要浅拷贝来复制并访问该节点,所以对于iterator的拷贝构造我们仅仅就是需要浅拷贝即可,不需要深拷贝,所以编译器默认生成的就足够使用。

2. 但是,我们要思考,多个迭代器对象指向同一个节点,会导致程序崩溃吗?

        浅拷贝导致程序崩溃的原因就是多次析构,但是每次使用完迭代器,就需要析构该节点吗?

        答:不可以!因为每个节点并不属于迭代器,迭代器无权释放节点,迭代器仅仅是访问修改功能,有权限释放节点的是 list 的析构函数

.

所以,在实际的工作中,对于内置类型指针,需要浅拷贝or深拷贝是根据目标来决定的

 const迭代器(iterator类)

如何设计const迭代器?

        

typedef const __list_iterator<T> const_iterator;

这样设计可以吗?

        答:不可以!因为如果这样设计,那么迭代器本身被const修饰,就不能++、--进行修改了。因为被const修饰的对象,只能调用被const修饰的函数。

        我们想实现的是迭代器指向的内容不能被修改,而迭代器本身可以++或--修改迭代器指向,如果使用上面的设计,那么我们就不能修改迭代器了,因为const迭代器对象就不能调用++函数,因为++函数没有const修饰,但是,我们也不能因此对operator++函数加上const修饰,因为该函数内必须要进行修改

正确设计:只需将解引用重载函数的返回值设为const T&

        但是又来了一个问题,如果只是返回值类型不同无法构成函数重载,因为前面说过了,iterator类型不能被const修饰,也就表明了成员函数形参处即使有const也不能与实参最好的匹配,从而实现重载,那么我们需要再写一个除了解引用重载函数外一模一样的const_iterator类吗?

        不可行,代码冗余

我们来看stl的解决方案:

采用多模板来解决

  • 因为只有解引用的返回值不同,所以在这里我们可以使用模板参数控制返回值类型
  • 对 __list_iterator<T,  Ref> 重命名为self(自己),方便日后再次修改模板时,不用再去修改各成员函数的返回值
  • Ref表示引用返回的权限,T& 或 const T&
	//typedef __list_iterator<T, T&> iterator;
	//typedef __list_iterator<T, const T&> const_iterator;
	//对于const迭代器和普通迭代器,这属于两个类,根据模板生成相应的类
	template<class T, class Ref>
	struct __list_iterator
	{
		typedef list_node<T> Node;
		typedef __list_iterator<T, Ref> self;

		//成员变量
		Node* _node;

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

		//引用返回,因为节点生命周期不在该函数内
		//可读可写版本
		//Ref根据不同的参数,返回相应权限的引用
		Ref operator*()
		{
			return _node->_val;
		}

		//前置++
		self& operator++()
		{
			_node = _node->_next;
			return *this;
		}

		//后置++
		self operator++(int)
		{
			self tmp(*this);

			_node = _node->_next;
			return tmp;
		}

		//这里注意const,因为比较!=时,右边时end(),该函数返回的是临时对象,临时对象具有常性
		bool operator!=(const self& it)
		{
			return _node != it._node;
		}

		bool operator==(const self& it)
		{
			return _node == it._node;
		}
	};

我们再来升级:

        可以看到STL的list模板有三个类型,并且list的重载函数重载了->

问:为什么要重载 ->呢?*解引用得不到数据吗?

        答:确实是的,因为对于内置类型,它单一的只有一个数据,解引用就得到了该数据;而对于其他的自定义类型,例如结构体或class类,operator*确实得不到其中数据,只能得到这一整个结构体变量

struct A
{
	A(int a1 = 0, int a2 = 0)
		:_a1(a1)
		,_a2(a2)
	{}

	int _a1;
	int _a2;
};

void test()
{
	list<A> lt;
	lt.push_back(A(1, 1));
	lt.push_back(A(2, 2));
	lt.push_back(A(3, 3));
	lt.push_back(A(4, 4));

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

        出现编译错误,对于*it,解引用的结果是结构体,如果想要输出,要自己在结构体内部重载流插入和流提取函数,对于私有的变量,是需要自己重载的,但是对于公有的变量,我们可以用 ->(*it)._a1来访问数据。用 -> 是因为迭代器模拟的就是指针 

        但是,it -> _a1本质其实是it ->() -> _a1,因为第一个->是运算符重载,返回的是一个指针,如果只有一个->,后面没有->运算符,是访问不了数据的,这里能访问数据是因为迭代器进行了特殊处理,省略了一个->

        由于operator->返回值有两种类型,所以我们又加了一个模板参数Ptr,表示operator->返回值类型,T* 或 const T*

template<class T, class Ref, class Ptr>

        所以对于复杂的代码,很难一步看懂,先省略细节,往后看

	//迭代器类
	template<class T, class Ref, class Ptr>
	class __list_iterator
	{
		typedef list_node<T> Node;
		typedef __list_iterator<T, Ref, Ptr> self;

	public:
		Node* _node;
	public:
		//迭代器构造函数
		__list_iterator(Node* node)
			:_node(node)
		{}

		//我们采用编译器默认生成的拷贝构造即可
		//__list_iterator(__list_iterator& x)
		//	:_node(x._node)
		//{}

		//重载*,解引用得到数据
		Ref operator*()
		{
			return _node->_val;
		}

		Ptr operator->()
		{
			return &(_node->val);
		}

		//前++
		self& operator++()
		{
			_node = _node->_next;
			return *this;
		}

		//后++
		self operator++(int)
		{
			__list_iterator<T> 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& it)
		{
			return _node != it._node;
		}

		bool operator==(const self& it)
		{
			return _node == it._node;
		}


	};

4. insert (list类)

  • 在pos之前插入,由于双向链表特性,pos在哪里都可以,即使是头节点,那就是尾插
  • 根据stl设计,返回插入后的迭代器,防止迭代器失效(外部的pos迭代器根据返回值来更新,但是禁止再在外部使用pos来操作list容器)
  • 返回的newnode通过隐式类型转换为iterator
		//在pos之前插入,由于双向链表特性,pos哪里都可以,即使是头节点
		iterator insert(iterator pos, const T& x)
		{
			//这里就体现了struct比class优点,class是需要封装为私有的,后面还要搞友元
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(x);

			prev->_next = newnode;
			newnode->_next = cur;

			cur->_prev = newnode;
			newnode->_prev = prev;

			++_size;

			return newnode;
		}

5. erase

  • erase就需要判断pos是否合法
  • 返回删除后的下一个迭代器
  • 返回的next通过隐式类型转换为iterator
		iterator erase(iterator pos)
		{
			assert(pos != end());

			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* next = cur->_next;

			prev->_next = next;
			next->_prev = prev;
			delete cur;

			--_size;

			return next;
		}

6. size

  • 可以遍历一遍,时间复杂度是O(N)
  • 也可以加一个_size成员变量,记录size
		size_t size()
		{
			/*size_t sz = 0;
			iterator it = begin();
			while (it != end())
			{
				++sz;
				++it;
			}

			return sz;*/

			//也可增加一个_size的成员变量
			return _size;
		}

7. push_back && push_front && pop_back && pop_front 

  • 全部复用insert、erase,非常方便
  • _size统一的都由insert、erase函数处理
		void push_back(const T& x)
		{
			/*Node* tail = _head->_prev;
			Node* newnode = new Node(x);
			
			tail->_next = newnode;
			newnode->_prev = tail;

			newnode->_next = _head;
			_head->_prev = newnode;*/
			insert(end(), x);
		}

		void push_front(const T& x)
		{
			insert(beign(), x);
		}

		void pop_back()
		{
			erase(--end());
		}

		void pop_front()
		{
			erase(begin());
		}

8. clear 

  • 清除数据,除了头节点
		//清数据,不需要清除头节点
		void clear()
		{
			iterator it = beign();
			while (it != end())
			{
				//不要++it,迭代器失效
				it = erase(it);
			}
			_size = 0;
		}

9. list的析构函数 

  • 复用clear函数
		~list()
		{
			//复用clear
			clear();
			delete _head;
			_head = nullptr;
		}

10. list的拷贝构造函数 

  • 逐个push_back,push_back内部再调用insert函数,在insert函数内部 new Node() ,不管是内置类型还是自定义类型,最终都会传递数据到 list_node 类里构造一个新的node节点(因为在 insert 函数内使用了new,先开空间再调用构造函数),对于内置类型在构造时逐字节拷贝,对于自定义类型在构造时调用其自身的构造函数,在初始化列表知识点那里
		//拷贝构造
		list(const list<T>& lt)
		{
            //初始化工作
			_head = new Node;
			_head->_prev = _head;
			_head->_next = _head;
			_size = 0;

			for (auto& e : lt)	//引用是为了防止数组类型很大
			{
				push_back(e);
			}
		}

11. empty_init 

  • 因为构造和拷贝构造都需要四行初始化,所以我们封装为empty_init函数
		void empty_init()
		{
			_head = new Node;
			_head->_prev = _head;
			_head->_next = _head;

			_size = 0;
		}

12. list的赋值运算符重载

  • 现代写法,先拷贝构造,再交换
		void swap(list<T>& lt)
		{
			std::swap(_head, lt._head);
			std::swap(_size, lt._size);
		}
		
		list<T>& operator=(list<T> lt)	//赋值运算符重载的现代写法
		{
			swap(lt);
			return *this;
		}

        注意:对于拷贝构造和赋值运算符重载,STL的list中这两个函数声明时,返回值是list&,而我们写的是list<T>&list<T>是类型,list是类名,写类型是必然正确的,但是这里写类名也对,编译器也可以识别。 

        类模板在类里既可以写类名又可以写类型但是不推荐缩写类型名

整体代码

#pragma once
#include<assert.h>
#include<iostream>
#include<algorithm>
#include"ReverseIterator.h"
using namespace my_ReverseIterator;

namespace my_list
{

	//list数据类型
	template<class T>
	struct list_node
	{
		//成员
		list_node<T>* _next;
		list_node<T>* _prev;
		T _val;

		//默认构造
		list_node(const T& val = T())
			:_next(nullptr)
			,_prev(nullptr)
			,_val(val)
		{}

	};

	//迭代器类
	//typedef __list_iterator<T, T&> iterator;
	//typedef __list_iterator<T, const T&> const_iterator;
	//对于const迭代器和普通迭代器,这属于两个类,根据模板生成相应的类
	template<class T, class Ref, class Ptr>
	struct __list_iterator
	{
		typedef list_node<T> Node;
		typedef __list_iterator<T, Ref, Ptr> self;

		//成员变量
		Node* _node;

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

		//引用返回,因为节点生命周期不在该函数内
		//可读可写版本
		//Ref根据不同的参数,返回相应权限的引用
		//你要明白,_node->val它不仅仅可能是int类型,还可能是string、vector等等类型
		//所以解引用得到的可能是结构体,那么返回结构体显然是错误的,得不到里面的数据
		Ref operator*()
		{
			return _node->_val;
		}

		//第三个模板参数,T* 和 const T*
		Ptr operator->()
		{
			return &(_node->_val);
		}

		//前置++
		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;
		}

		//这里注意const,因为比较!=时,右边是end(),该函数返回的是临时对象,临时对象具有常性
		bool operator!=(const self& it)
		{
			return _node != it._node;
		}

		bool operator==(const self& it)
		{
			return _node == it._node;
		}
	};

	template<class T>
	class list
	{
		typedef list_node<T> Node;
	public:
		typedef __list_iterator<T, T&, T*> iterator;	//迭代器要公有,让外面可以使用
		typedef __list_iterator<T, const T&, const T*> const_iterator;
		typedef ReverseIterator<iterator, T&, T*> reverse_iterator;
		typedef ReverseIterator<const_iterator, const T&, const T*> const_reverse_iterator;

		//用 end 适配 rbegin
		reverse_iterator rbegin()
		{
			return reverse_iterator(end());
		}

		//用 begin 适配 rend
		reverse_iterator rend()
		{
			return reverse_iterator(begin());
		}

		iterator begin()
		{
			//由指针类型隐式转换为iterator类
			//return _head->_next;

			//也可以用匿名对象
			return iterator(_head->_next);
		}

		iterator end()
		{
			return iterator(_head);
		}

		const_iterator begin() const
		{
			return const_iterator(_head->next);
		}

		const_iterator end() const
		{
			return const_iterator(_head);
		}

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

			_size = 0;
		}

		list()
		{
			/*_head = new Node;
			_head->_prev = _head;
			_head->_next = _head;

			_size = 0;*/
			//因为构造和拷贝构造都需要这四行初始化,所以我们封装为empty_init
			empty_init();
		}

		//拷贝构造
		list(const list<T>& lt)
		{
			/*_head = new Node;
			_head->_prev = _head;
			_head->_next = _head;
			_size = 0;*/
			empty_init();

			for (auto& e : lt)	//引用是为了防止数组类型很大
			{
				push_back(e);
			}
		}

		void swap(list<T>& lt)
		{
			std::swap(_head, lt._head);
			std::swap(_size, lt._size);
		}
		
		//list<T>是类型
		list<T>& operator=(list<T> lt)	//赋值运算符重载的现代写法
		{
			swap(lt);
			return *this;
		}

		~list()
		{
			//复用clear
			clear();
			delete _head;
			_head = nullptr;
		}

		//清数据,不需要清除头节点
		void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				//不要++it,迭代器失效
				it = erase(it);
			}
			_size = 0;
		}

		void push_back(const T& x)
		{
			/*Node* tail = _head->_prev;
			Node* newnode = new Node(x);
			
			tail->_next = newnode;
			newnode->_prev = tail;

			newnode->_next = _head;
			_head->_prev = newnode;*/
			insert(end(), x);
		}

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

		void pop_back()
		{
			erase(--end());
		}

		void pop_front()
		{
			erase(begin());
		}

		//在pos之前插入,由于双向链表特性,pos哪里都可以,即使是头节点
		iterator insert(iterator pos, const T& x)
		{
			//这里就体现了struct比class优点,class是需要封装为私有的,后面还要搞友元
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(x);

			prev->_next = newnode;
			newnode->_next = cur;

			cur->_prev = newnode;
			newnode->_prev = prev;

			++_size;

			return newnode;
		}

		iterator erase(iterator pos)
		{
			assert(pos != end());

			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* next = cur->_next;

			prev->_next = next;
			next->_prev = prev;
			delete cur;

			--_size;

			return next;
		}

		size_t size()
		{
			/*size_t sz = 0;
			iterator it = begin();
			while (it != end())
			{
				++sz;
				++it;
			}

			return sz;*/

			//也可增加一个_size的成员变量
			return _size;
		}

	private:
		Node* _head;
		size_t _size;
	};

}

总结

        本文没有写反向迭代器,关于反向迭代器的构建和编写在这篇文章

<C++> 反向迭代器-CSDN博客

        list相较于vector、string最重要的是迭代器的设计,list因为数据类型、储存方式而需要独特的迭代器——类来实现,除此之外,list无更多新的内容,对于反向迭代器,会在之后的适配器同意讲解。下节,我们来学习stack、queue。

        最后,如果小帅的本文哪里有错误,还请大家指出,请在评论区留言(ps:抱大佬的腿),新手创作,实属不易,如果满意,还请给个免费的赞,三连也不是不可以(流口水幻想)嘿!那我们下期再见喽,拜拜!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
这是一个比较复杂的程序,需要考虑很多细节,包括文件读写、目录遍历等,需要仔细设计。下面我将给出一个简单的设计思路。 首先,我们需要定义一个目录结构体和一个文件结构体,用来表示文件系统中的目录和文件。目录结构体应该包含目录名、子目录列表和文件列表,文件结构体应该包含文件名和文件内容。 ```C++ struct Directory { string name; vector<Directory> subdirs; vector<File> files; }; struct File { string name; string content; }; ``` 接下来,我们需要定义一个文件系统类,这个类应该包含一个根目录指针,以及一些方法来实现各种操作。其中,改变目录、显示目录和创建目录操作都比较简单,只需要在目录结构体中进行遍历即可。删除目录则需要注意递归删除子目录和文件。 ```C++ class FileSystem { public: FileSystem() { root = new Directory{"", {}, {}}; current_dir = root; } void change_dir(const string& dirname) { // 在当前目录中查找子目录 for (auto& subdir : current_dir->subdirs) { if (subdir.name == dirname) { current_dir = &subdir; return; } } cout << "Directory not found!\n"; } void list_dir() { // 显示当前目录的子目录和文件 cout << "Directories:\n"; for (auto& subdir : current_dir->subdirs) { cout << subdir.name << "\n"; } cout << "Files:\n"; for (auto& file : current_dir->files) { cout << file.name << "\n"; } } void create_dir(const string& dirname) { // 创建子目录 current_dir->subdirs.push_back(Directory{dirname, {}, {}}); } void delete_dir(const string& dirname) { // 在当前目录中查找子目录并删除 for (auto it = current_dir->subdirs.begin(); it != current_dir->subdirs.end(); ++it) { if (it->name == dirname) { current_dir->subdirs.erase(it); return; } } cout << "Directory not found!\n"; } private: Directory* root; Directory* current_dir; }; ``` 新建文件和打开文件操作也比较简单,只需要在当前目录中查找文件即可。写入文件内容则需要注意将内容保存到文件结构体中。 ```C++ class FileSystem { public: // ... void create_file(const string& filename) { // 创建文件 current_dir->files.push_back(File{filename, ""}); } void open_file(const string& filename) { // 在当前目录中查找文件并打开 for (auto& file : current_dir->files) { if (file.name == filename) { current_file = &file; return; } } cout << "File not found!\n"; } void write_file(const string& content) { // 写入文件内容 if (current_file) { current_file->content += content; } } void delete_file(const string& filename) { // 在当前目录中查找文件并删除 for (auto it = current_dir->files.begin(); it != current_dir->files.end(); ++it) { if (it->name == filename) { current_dir->files.erase(it); return; } } cout << "File not found!\n"; } private: // ... File* current_file = nullptr; }; ``` 最后,我们需要在主函数中处理用户输入。这里我使用了一个简单的循环来获取用户输入并调用相应的方法。 ```C++ int main() { FileSystem fs; while (true) { string command, arg; cout << "> "; cin >> command; if (command == "cd") { cin >> arg; fs.change_dir(arg); } else if (command == "dir") { fs.list_dir(); } else if (command == "md") { cin >> arg; fs.create_dir(arg); } else if (command == "rd") { cin >> arg; fs.delete_dir(arg); } else if (command == "edit") { cin >> arg; fs.create_file(arg); fs.open_file(arg); } else if (command == "open") { cin >> arg; fs.open_file(arg); } else if (command == "write") { getline(cin, arg); fs.write_file(arg); } else if (command == "del") { cin >> arg; fs.delete_file(arg); } else if (command == "exit") { break; } else { cout << "Invalid command!\n"; } } return 0; } ``` 这个程序还有很多可以改进的地方,比如添加错误处理、支持文件重命名等。不过基本功能已经实现了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值