从C语言到C++_17(list的模拟实现)list不是原生指针的迭代器

news2024/11/27 4:00:07

目录

1. list 的基本框架

1.1 list 的结点

1.2 list 构造函数

1.3 push_back 

2. list 迭代器的实现

2.1 迭代器的构造

2.2 begin() 和 end()

2.3  重载 != 和 * 和 ++ 

2.4 遍历测试:

2.6 operator->

2.7 operator--

2.8 const 迭代器

3. list 的增删查改

3.1 insert和头插尾插

3.2 erase和头删尾删

4.  list 的深浅拷贝

4.1 clear 和析构 

4.2 迭代器区间构造和交换

4.3 拷贝构造和赋值重载

5. list相关选择题

答案:

6. 完整代码

list.h

Test.c

本篇完。


上一篇说到,list 其实就是带哨兵位循环双向链表而已,这种链表虽然结构复杂,

但是实现起来反而是最简单的,我们在数据结构与算法专栏中有过详细的讲解:

数据结构与算法⑦(第二章收尾)带头双向循环链表的实现_GR C的博客-CSDN博客

当时我们是用C语言实现,这里对 list 的实现其实也是大同小异的。

当然,我们重点还是倾向于去理解它的底层实现原理,

所以我们将对其实现方式进行进一步地简化,并且按照我们自己习惯的命名风格去走。

我们之前已经模拟实现过 string 和 vector 了,这是本专栏 STL 的第三个模拟实现,

因此在讲解的时,出现重复的知识点我们就一笔带过。我们将重点去讲解迭代器的实现!

本章我们要对迭代器有一个新的认知,迭代器不一定就是一个原生指针,

也有可能是一个自定义类型。

本章我们将通过自定义类型的运算符重载去控制我们的迭代器的 "行为"。

1. list 的基本框架

我们还是参考《STL源码剖析》,既然是要实现链表,我们首先要做的应该是建构结点。

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

1.1 list 的结点

C语言写的:

 C++的代码:

	template<class T>
	struct list_node
	{
		T _data;
		list_node* _prev;
		list_node* _next;
	};

C++中对象里的成员如果全是共有的还是比较习惯用 struct 的

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

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

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

	template<class T>
	struct list_node
	{
		T _data;
		list_node* _prev;
		list_node* _next;

		list_node(const T& x = T())
			:_data(x)
			,_prev(nullptr)
			,_next(nullptr)
		{}
	};

设计成全缺省,给一个匿名对象 T() 。如此一来,如果没有指定初识值,

它就会按模板类型去给对应的初始值了。

1.2 list 构造函数

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

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

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

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

到这里 list.h 就是这样:

#pragma once

#include<iostream>
#include<assert.h>
using namespace std;

namespace rtx
{
	template<class T>
	struct list_node // 定义结点
	{
		T _data;
		list_node* _prev;
		list_node* _next;

		list_node(const T& x = T())
			:_data(x)
			,_prev(nullptr)
			,_next(nullptr)
		{}
	};

	template<class T>
	class list // 定义list类
	{
		typedef list_node<T> Node;
	public:
		list()
		{
			_head = new Node;
			_head->_prev = _head;
			_head->_next = _head;
		}

	private:
		Node* _head; // 哨兵位头结点
	};
}

1.3 push_back 

我们先去实现一下最经典的 push_back 尾插,好让我们的 list 先跑起来。

尾插思路和以前写过的思路一样,后面很多接口也是,不懂的回去看啊,别逼我求你,

数据结构与算法⑦(第二章收尾)带头双向循环链表的实现_GR C的博客-CSDN博客

直接放代码了:

		void push_back(const T& x)
		{
			Node* tail = _head->_prev;
			Node* NewNode = new Node(x);
			// 思路图:head        tail  NewNode
			tail->_next = NewNode;
			NewNode->_prev = tail;
			_head->_prev = NewNode;
			NewNode->_next = _head;
		}

我们想要打印的话就只能自己实现迭代器了:

2. list 迭代器的实现

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

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

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

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

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

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

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

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

然后重载运算符 operator++ 和 operator* ,

是不是就可以控制其解引用并 ++ 到下一个位置了?

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

2.1 迭代器的构造

代码:只需要用一个结点的指针

	template<class T>
	struct __list_iterator // 定义迭代器
	{
		typedef list_node<T> Node;
        typedef __list_iterator<T> iterator; // STL规定的命名,且公有
		Node* _node;

		iterator(Node* node)
			:_node(node)
		{}
	};

这里命名是参考源码的,__list_iterator 前面是两个下划线。

我们想要打印的话应该是这样的:

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

所以我们来实现这几个操作

2.2 begin() 和 end()

代码:在 list 类中设计 begin 和 end

	template<class T>
	class list // 定义list类
	{
		typedef list_node<T> Node;
	public:
		typedef __list_iterator<T> iterator; // STL规定的命名,且公有

		iterator begin()// begin是实际数据的第一个,_head 是哨兵位头结点
		{
			return iterator(_head->_next);
		}
		iterator end()// end是实际数据的下一个
		{
			return iterator(_head);
		}

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

		void push_back(const T& x)
		{
			Node* tail = _head->_prev;
			Node* NewNode = new Node(x);
			// 思路图:head        tail  NewNode
			tail->_next = NewNode;
			NewNode->_prev = tail;
			_head->_prev = NewNode;
			NewNode->_next = _head;
		}

	private:
		Node* _head; // 哨兵位头结点
	};

2.3  重载 != 和 * 和 ++ 

operator!=

如何判断是否相等呢?

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

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

operator*

解引用就是取结点 _node 里的数据,并且 operator* 和指针一样,不仅仅能读数据,还能写数据。

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

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

operator++

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

		iterator& operator++()
		{
			_node = _node->_next;
			return *this; // 返回加加后的值
		}

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

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

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

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

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

后置++的实现:(注意后置++不能用引用返回)

		iterator operator++(int)
		{
			__list_iterator<T> tmp(*this); // 拷贝构造一个tmp存储原来的值
			_node = _node->next;
			return tmp; // 返回加加前的值
		}

2.4 遍历测试:

至此,我们可以遍历打印我们的代码了,而且范围for也能用了:

list.h

#pragma once

#include<iostream>
#include<assert.h>
using namespace std;

namespace rtx
{
	template<class T>
	struct list_node // 定义结点
	{
		T _data;
		list_node* _prev;
		list_node* _next;

		list_node(const T& x = T())
			:_data(x)
			,_prev(nullptr)
			,_next(nullptr)
		{}
	};

	template<class T>
	struct __list_iterator // 定义迭代器
	{
		typedef list_node<T> Node;
        typedef __list_iterator<T> iterator; // STL规定的命名,且公有
		Node* _node;

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

		bool operator!=(const iterator& it) 
		{
			return _node != it._node;
		}
		T& operator*()
		{
			return _node->_data;  // 返回结点的数据
		}
		iterator& operator++()
		{
			_node = _node->_next;
			return *this; // 返回加加后的值
		}
		iterator& operator++(int)
		{
			iterator tmp(*this); // 拷贝构造一个tmp存储原来的值
			_node = _node->next;
			return tmp; // 返回加加后的值
		}
	};

	template<class T>
	class list // 定义list类
	{
		typedef list_node<T> Node;
	public:
		typedef __list_iterator<T> iterator; // STL规定的命名,且公有

		iterator begin()// begin是实际数据的第一个,_head 是哨兵位头结点
		{
			return iterator(_head->_next);
		}
		iterator end()// end是实际数据的下一个
		{
			return iterator(_head);
		}

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

		void push_back(const T& x)
		{
			Node* tail = _head->_prev;
			Node* NewNode = new Node(x);
			// 思路图:head        tail  NewNode
			tail->_next = NewNode;
			NewNode->_prev = tail;
			_head->_prev = NewNode;
			NewNode->_next = _head;
		}

	private:
		Node* _head; // 哨兵位头结点
	};
}

Test.c

#define _CRT_SECURE_NO_WARNINGS 1

#include "list.h"

namespace rtx
{
	void Test_push_back()
	{
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);
		lt.push_back(5);

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

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

int main()
{
	rtx::Test_push_back();

	return 0;
}

2.6 operator->

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

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

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

比如我们想打印坐标:

	void Test_arrow()
	{
		struct Pos
		{
			int _a1;
			int _a2;

			Pos(int a1 = 0, int a2 = 0)
				:_a1(a1)
				, _a2(a2)
			{}
		};
		Pos aa;
		Pos* p2 = &aa;
		p2->_a1;
		p2->_a2;

		list<Pos> lt;
		lt.push_back(Pos(10, 20));
		lt.push_back(Pos(10, 21));

		list<Pos>::iterator it = lt.begin();
		while (it != lt.end())
		{
			cout << (*it)._a1 << "," << (*it)._a2 << endl;
			//cout << it->_a1 << "," << it->_a2 << endl;
			++it;
		}
		cout << endl;
	}

虽然能用解引用+点,但用箭头还是方便的,而且你模拟实现总不能不给别人用吧,

所以我们这里可以去实现一下箭头操作符 operator->,如果不是很熟练应该是不会的。

我们直接看一下源代码是怎么实现的,抄下来用用然后思考下:

		T& operator*()
		{
			return _node->_data;  // 返回结点的数据
		}
		T* operator->()
		{
			return &(operator*());
			//即 return &(_node->_data);
		}
	void Test_arrow()
	{
		struct Pos
		{
			int _a1;
			int _a2;

			Pos(int a1 = 0, int a2 = 0)
				:_a1(a1)
				, _a2(a2)
			{}
		};
		Pos aa;
		Pos* p2 = &aa;
		p2->_a1;
		p2->_a2;

		list<Pos> lt;
		lt.push_back(Pos(10, 20));
		lt.push_back(Pos(10, 21));

		list<Pos>::iterator it = lt.begin();
		while (it != lt.end())
		{
			//cout << (*it)._a1 << "," << (*it)._a2 << endl;
			cout << it->_a1 << "," << it->_a2 << endl;
		    //实际要写,it->->_a1,但是编译器优化了一个箭头
			++it;
		}
		cout << endl;
	}

第一个指针是operator->,第二个指针是原生指针的箭头,但是编译器为了可读性:

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

2.7 operator--

前面实现了operator++,现在实现下operator--,把++的_next换成_prev就行:

		iterator& operator--()
		{
			_node = _node->_prev;
			return *this; // 返回减减后的值
		}
		iterator operator--(int)
		{
			iterator tmp(*this); // 拷贝构造一个tmp存储原来的值
			_node = _node->prev;
			return tmp; // 返回减减前的值
		}

2.8 const 迭代器

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

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

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

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

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

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

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

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

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

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

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

有没有办法可以优化一下呢?

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

我们来看看巨佬是怎么做的 —— 在定义 template 模板的时增加两个参数:

	template<class T, class Ref, class Ptr>
	struct __list_iterator // 定义迭代器
	{
		typedef list_node<T> Node;
		typedef __list_iterator<T, Ref, Ptr> iterator;
		// 在list类里面:
		// typedef __list_iterator<T, T&, T*>             iterator;
        // typedef __list_iterator<T, const T&, const T*> const_iterator;

再加上const begin和const end我们的遍历打印函数就能跑出来了:

list.h

#pragma once

#include<iostream>
#include<assert.h>
using namespace std;

namespace rtx
{
	template<class T>
	struct list_node // 定义结点
	{
		T _data;
		list_node* _prev;
		list_node* _next;

		list_node(const T& x = T())
			:_data(x)
			,_prev(nullptr)
			,_next(nullptr)
		{}
	};

	template<class T, class Ref, class Ptr>
	struct __list_iterator // 定义迭代器
	{
		typedef list_node<T> Node;
		typedef __list_iterator<T, Ref, Ptr> iterator;
		// 在list类里面:
		// typedef __list_iterator<T, T&, T*>             iterator;
        // typedef __list_iterator<T, const T&, const T*> const_iterator;
		Node* _node;

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

		bool operator!=(const iterator& it)
		{
			return _node != it._node;
		}
		//T& operator*()
		Ref operator*()
		{
			return _node->_data;  // 返回结点的数据
		}
		//T* operator->()
		Ptr operator->()
		{
			return &(operator*());
			//即 return &(_node->_data);
		}
		iterator& operator++()
		{
			_node = _node->_next;
			return *this; // 返回加加后的值
		}
		iterator operator++(int)
		{
			iterator tmp(*this); // 拷贝构造一个tmp存储原来的值
			_node = _node->_next;
			return tmp; // 返回加加前的值
		}
        iterator& operator--()
		{
			_node = _node->_prev;
			return *this; // 返回减减后的值
		}
		iterator operator--(int)
		{
			iterator tmp(*this); // 拷贝构造一个tmp存储原来的值
			_node = _node->prev;
			return tmp; // 返回减减前的值
		}
	};

	template<class T>
	class list // 定义list类
	{
		typedef list_node<T> Node;
	public:
		typedef __list_iterator<T, T&, T*> iterator;
		typedef __list_iterator<T, const T&, const T*> const_iterator;

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

		iterator begin()// begin是实际数据的第一个,_head 是哨兵位头结点
		{
			return iterator(_head->_next);
		}
		iterator end()// end是实际数据的下一个
		{
			return iterator(_head);
		}

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

		void push_back(const T& x)
		{
			Node* tail = _head->_prev;
			Node* NewNode = new Node(x);
			// 思路图:head        tail  NewNode
			tail->_next = NewNode;
			NewNode->_prev = tail;
			_head->_prev = NewNode;
			NewNode->_next = _head;
		}

	private:
		Node* _head; // 哨兵位头结点
	};
}

Test.c

#define _CRT_SECURE_NO_WARNINGS 1

#include "list.h"

namespace rtx
{
	void Test_arrow()
	{
		struct Pos
		{
			int _a1;
			int _a2;

			Pos(int a1 = 0, int a2 = 0)
				:_a1(a1)
				, _a2(a2)
			{}
		};
		Pos aa;
		Pos* p2 = &aa;
		p2->_a1;
		p2->_a2;

		list<Pos> lt;
		lt.push_back(Pos(10, 20));
		lt.push_back(Pos(10, 21));

		list<Pos>::iterator it = lt.begin();
		while (it != lt.end())
		{
			//cout << (*it)._a1 << "," << (*it)._a2 << endl;
			cout << it->_a1 << "," << it->_a2 << endl;
		    //实际要写,it->->_a1,但是编译器优化了一个箭头
			++it;
		}
		cout << endl;
	}

	//cout << it->_a1 << ":" << it->_a2 << endl;

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

	void Test_push_back()
	{
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);
		lt.push_back(5);

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

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

int main()
{
	rtx::Test_push_back();
	//rtx::Test_arrow();

	return 0;
}

前面实现的迭代器都是原生指针。,写到 list 这才是涉及到了迭代器的精华。

3. list 的增删查改

在以前数据结构实现的时候说过,双向带头循环链表,

这个结构的优势就是只要实现insert和erase其它大多函数都能复用了

3.1 insert和头插尾插

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

这个我们也学过了,这里不在细说

		iterator insert(iterator pos, const T& x)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* NewNode = new Node(x);

			prev->_next = NewNode;
			NewNode->_prev = prev;
			NewNode->_next = cur;
			cur->_prev = NewNode;

			return iterator(NewNode);
		}
		void push_back(const T& x)
		{
			//Node* tail = _head->_prev;
			//Node* NewNode = new Node(x);
			 思路图:head        tail  NewNode
			//tail->_next = NewNode;
			//NewNode->_prev = tail;
			//_head->_prev = NewNode;
			//NewNode->_next = _head;
			insert(end(), x);
		}
		void push_front(const T& x)
		{
			insert(begin(), x);
		}

测试:

	void Test_insert()
	{
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);
		lt.push_back(5);
		//list<int>::iterator pos = find(lt.begin(), lt.end(), 2);// 涉及其它问题,先不这样写
		//if (pos != lt.end())
		//{
		//	lt.insert(pos, 20);
		//}
		lt.insert(++lt.begin(), 20);
		lt.push_front(0);
		lt.push_front(-1);
		print_list(lt);
	}

3.2 erase和头删尾删

只需注意别删掉哨兵位头结点:

		iterator erase(iterator pos)
		{
			assert(pos != end());
			Node* cur = pos._node; // 删cur
			Node* prev = cur->_prev;

			prev->_next = cur->_next; // cur前一个指向cur后一个
			cur->_next->_prev = prev; // cur后一个指回cur前一个

			delete cur;
			return iterator(prev->_next); // 返回删除位置下一个
		}
		void pop_back()
		{
			erase(begin());
		}
		void pop_front()
		{
			erase(--end());
		}

测试:

	void Test_erase()
	{
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);
		lt.push_back(5);

		lt.erase(++++lt.begin());//发现个好玩的,(删除3)
		lt.pop_back();
		lt.pop_front();
		print_list(lt);
	}

4.  list 的深浅拷贝

list 的同样涉及深浅拷贝问题,下面的拷贝构造是深拷贝还是浅拷贝?

	void Test_copy() 
	{
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);
		lt.push_back(5);

		list<int> lt2(lt);
		print_list(lt);
		print_list(lt2);
	}

 程序没有崩,是深拷贝吗?不是的,这里默认生成的拷贝构造还是浅拷贝,

有童鞋就会问了:难道是没有写数据?也不是,这里没崩仅仅是因为我们还没设计析构。

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

我们下面先实现一下析构:

4.1 clear 和析构 

写析构之前为了方便清空,我们先实现一下 clear ,然后复用一下,clear又可以复用erase,

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

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

		~list()
		{
			clear();
			delete _head;
			_head = nullptr;
		}
		void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				it = erase(it); // 返回删除位置的下一个结点
			}
		}

再运行一下程序就崩溃了,虽然没报错,但是调试一下就报错了:

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

4.2 迭代器区间构造和交换

我们直接写现代写法,因为list本来就是提供迭代器区间初始化和交换函数的,

现在我们实现一下,并且拷贝构造的话至少保证有个头结点把,

所以我们把构造函数拎出来复用一下,写成一个empty_init 函数 (源码里也是这样写的),

这几个函数我们很熟了,直接放代码:

		void empty_init()// 创建并初始化哨兵位头结点(即构造函数)
		{
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;
		}
		template <class InputIterator>
		list(InputIterator first, InputIterator last)
		{
			empty_init();
			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}
		list()
		{
			empty_init();
		}
		void swap(list<T>& x)//提一嘴:void swap(list& x)也行(类里面可以省略<T>,类外不行>
		{
			std::swap(_head, x._head); // 换哨兵位头结点就行
		}

4.3 拷贝构造和赋值重载

在上面的基础上我们直接实现现代写法:

		list(const list<T>& lt)//lt2(lt1)
		{
			empty_init();
			list<T> tmp(lt.begin(), lt.end()); // 迭代器区间构造一个(lt1)
			swap(tmp); // 直接和lt2换哨兵位头结点
		}
		list<T>& operator=(list<T> lt)//lt3 = lt1 这里lt1直接深拷贝给lt,lt是局部对象,出来作用域直接调析构
		{
			swap(lt);// 把深拷贝出来的lt和lt3交换
			return *this; // 把lt3返回
		}

测试一下:

	void Test_copy() 
	{
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);
		lt.push_back(5);

		list<int> lt2(lt);
		for (auto& e : lt2)
		{
			e *= 10;
		}
		print_list(lt);
		print_list(lt2);
	}

5. list相关选择题

1. 以下程序输出结果为( )

int main()
{
	int ar[] = { 0,1, 2, 3, 4, 5, 6, 7, 8, 9 };
	int n = sizeof(ar) / sizeof(int);
	list<int> mylist(ar, ar + n);
	list<int>::iterator pos = find(mylist.begin(), mylist.end(), 5);
	reverse(mylist.begin(), pos);
	reverse(pos, mylist.end());
	list<int>::const_reverse_iterator crit = mylist.crbegin();
	while (crit != mylist.crend())
	{
		cout << *crit << " ";
		++crit;
	}
	cout << endl;
}

A.4 3 2 1 0 5 6 7 8 9

B.0 1 2 3 4 9 8 7 6 5

C.5 6 7 8 9 0 1 2 3 4

D.5 6 7 8 9 4 3 2 1 0

2. 下面程序的输出结果正确的是( )

int main()
{
	int array[] = { 1, 2, 3, 4, 0, 5, 6, 7, 8, 9 };
	int n = sizeof(array) / sizeof(int);
	list<int> mylist(array, array + n);
	auto it = mylist.begin();
	while (it != mylist.end())
	{
		if (*it != 0)
			cout << *it << " ";
		else
			it = mylist.erase(it);
		++it;
	}
	return 0;
}

A.1 2 3 4 5 6 7 8 9

B. 1 2 3 4 6 7 8 9

C.程序运行崩溃

D.1 2 3 4 0 5 6 7 8 9

3. 对于list有迭代器it, 当erase(it)后,说法错误的是( )

A.当前迭代器it失效

B.it前面的迭代器仍然有效

C.it后面的迭代器失效

D.it后面的迭代器仍然有效

4. 下面有关vector和list的区别,描述错误的是( )

A.vector拥有一段连续的内存空间,因此支持随机存取,如果需要高效的随机读取,应该使用vector

B.list拥有一段不连续的内存空间,如果需要大量的插入和删除,应该使用list

C.vector<int>::iterator支持“+”、“+=”、“<”等操作符

D.list<int>::iterator则不支持“+”、“+=”、“<”等操作符运算,但是支持了[]运算符

5. 下面有关vector和list的区别,描述正确的是( )

A.两者在尾部插入的效率一样高

B.两者在头部插入的效率一样高

C.两者都提供了push_back和push_front方法

D.两者都提供了迭代器,且迭代器都支持随机访问

6. 以下代码实现了从表中删除重复项的功能,请选择其中空白行应填入的正确代码( )

template<typename T>
void removeDuplicates(list<T>& aList)
{
	T curValue;
	list<T>::iterator cur, p;
	cur = aList.begin();
	while (cur != aList.end())
	{
		curValue = *cur;
		//空白行 1
		while (p != aList.end())
		{
			if (*p == curValue)
			{
				//空白行 2
			}
			else
			{
				p++;
			}
		}
	}
}

A. p=cur+1;aList.erase(p++);

B.p=++cur; p == cur ? cur = p = aList.erase(p) : p = aList.erase(p);

C.p=cur+1;aList.erase(p);

D.p=++cur;aList.erase(p);

答案:

1. C

分析:list<int>::iterator pos = find(mylist.begin(), mylist.end(), 5); //找到5的位置

reverse(mylist.begin(), pos);//逆置0 1 2 3 4 为 4 3 2 1 0

reverse(pos, mylist.end()); //逆置5 6 7 8 9 为 9 8 7 6 5

逆置完成之后其数据为:4 3 2 1 0 9 8 7 6 5

list<int>::const_reverse_iterator crit = mylist.crbegin(); //反向迭代器进行反向访问

while(crit != mylist.crend()){}

所以答案为:5 6 7 8 9 0 1 2 3 4

2. B

分析:程序在使用迭代器取值时,如果不等于0就进行打印,为0时不打印并删除当前节点,

3. C

分析:删除节点后,只有指向当前节点的迭代器失效了,其前后的迭代器仍然有效,因为底层为不连续空间,只有被删除的   节点才会失效。

4. D

A.如果想大量随机读取数据操作,vector是首选的容器

B.如果想大量的插入和删除数据,list效率较高,是首选

C.由于vector底层是连续空间,其迭代器就是相应类型的指针,所以支持对应的操作

D.list迭代器不支持[]运算符

5. A

A.vector在尾部插入数据不需要移动数据,list为双向循环链表也很容易找到尾部,因此两者在尾部插入数据效率相同

B.vector头部插入效率极其低,需要移动大量数据

C.vector由于在头部插入数据效率很低,所以没有提供push_front方法

D.list不支持随机访问

6. B

分析:迭代p需要迭代不重复节点的下一节点,重要的是cur迭代器需要往下迭代,因此cur需要往前移动,二答案A C的cur都不会改变,空白行2是当需要找到重复值时进行节点删除,当删除时当前迭代器会失效,因此需要将迭代器p往后迭代。

6. 完整代码

list.h

#pragma once

#include<iostream>
#include<assert.h>
using namespace std;

namespace rtx
{
	template<class T>
	struct list_node // 定义结点
	{
		T _data;
		list_node* _prev;
		list_node* _next;

		list_node(const T& x = T())
			:_data(x)
			,_prev(nullptr)
			,_next(nullptr)
		{}
	};

	template<class T, class Ref, class Ptr>
	struct __list_iterator // 定义迭代器
	{
		typedef list_node<T> Node;
		typedef __list_iterator<T, Ref, Ptr> iterator;
		// 在list类里面:
		// typedef __list_iterator<T, T&, T*>             iterator;
        // typedef __list_iterator<T, const T&, const T*> const_iterator;
		Node* _node;

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

		bool operator!=(const iterator& it)
		{
			return _node != it._node;
		}
		//T& operator*()
		Ref operator*()
		{
			return _node->_data;  // 返回结点的数据
		}
		//T* operator->()
		Ptr operator->()
		{
			return &(operator*());
			//即 return &(_node->_data);
		}
		iterator& operator++()
		{
			_node = _node->_next;
			return *this; // 返回加加后的值
		}
		iterator operator++(int)
		{
			iterator tmp(*this); // 拷贝构造一个tmp存储原来的值
			_node = _node->_next;
			return tmp; // 返回加加前的值
		}
		iterator& operator--()
		{
			_node = _node->_prev;
			return *this; // 返回减减后的值
		}
		iterator operator--(int)
		{
			iterator tmp(*this); // 拷贝构造一个tmp存储原来的值
			_node = _node->prev;
			return tmp; // 返回减减前的值
		}
	};

	template<class T>
	class list // 定义list类
	{
		typedef list_node<T> Node;
	public:
		typedef __list_iterator<T, T&, T*> iterator;
		typedef __list_iterator<T, const T&, const T*> const_iterator;

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

		iterator begin()// begin是实际数据的第一个,_head 是哨兵位头结点
		{
			return iterator(_head->_next);
		}
		iterator end()// end是实际数据的下一个
		{
			return iterator(_head);
		}

		void empty_init()// 创建并初始化哨兵位头结点(即构造函数)
		{
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;
		}
		template <class InputIterator>
		list(InputIterator first, InputIterator last)
		{
			empty_init();
			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}
		list()
		{
			empty_init();
		}
		void swap(list<T>& x)//提一嘴:void swap(list& x)也行(类里面可以省略<T>,类外不行>
		{
			std::swap(_head, x._head); // 换哨兵位头结点就行
		}

		list(const list<T>& lt)//lt2(lt1)
		{
			empty_init();
			list<T> tmp(lt.begin(), lt.end()); // 迭代器区间构造一个(lt1)
			swap(tmp); // 直接和lt2换哨兵位头结点
		}
		list<T>& operator=(list<T> lt)//lt3 = lt1 这里lt1直接深拷贝给lt,lt是局部对象,出来作用域直接调析构
		{
			swap(lt);// 把深拷贝出来的lt和lt3交换
			return *this; // 把lt3返回
		}

		~list()
		{
			clear();
			delete _head;
			_head = nullptr;
		}
		void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				it = erase(it); // 返回删除位置的下一个结点
			}
		}

		iterator insert(iterator pos, const T& x)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* NewNode = new Node(x);

			prev->_next = NewNode;
			NewNode->_prev = prev;
			NewNode->_next = cur;
			cur->_prev = NewNode;

			return iterator(NewNode);
		}
		void push_back(const T& x)
		{
			//Node* tail = _head->_prev;
			//Node* NewNode = new Node(x);
			 思路图:head        tail  NewNode
			//tail->_next = NewNode;
			//NewNode->_prev = tail;
			//_head->_prev = NewNode;
			//NewNode->_next = _head;
			insert(end(), x);
		}
		void push_front(const T& x)
		{
			insert(begin(), x);
		}

		iterator erase(iterator pos)
		{
			assert(pos != end());
			Node* cur = pos._node; // 删cur
			Node* prev = cur->_prev;

			prev->_next = cur->_next; // cur前一个指向cur后一个
			cur->_next->_prev = prev; // cur后一个指回cur前一个

			delete cur;
			return iterator(prev->_next); // 返回删除位置下一个
		}
		void pop_back()
		{
			erase(begin());
		}
		void pop_front()
		{
			erase(--end());
		}

	private:
		Node* _head; // 哨兵位头结点
	};
}

Test.c

#define _CRT_SECURE_NO_WARNINGS 1

#include "list.h"

namespace rtx
{
	void Test_arrow()
	{
		struct Pos
		{
			int _a1;
			int _a2;

			Pos(int a1 = 0, int a2 = 0)
				:_a1(a1)
				, _a2(a2)
			{}
		};
		Pos aa;
		Pos* p2 = &aa;
		p2->_a1;
		p2->_a2;

		list<Pos> lt;
		lt.push_back(Pos(10, 20));
		lt.push_back(Pos(10, 21));

		list<Pos>::iterator it = lt.begin();
		while (it != lt.end())
		{
			//cout << (*it)._a1 << "," << (*it)._a2 << endl;
			cout << it->_a1 << "," << it->_a2 << endl;
		    //实际要写,it->->_a1,但是编译器优化了一个箭头
			++it;
		}
		cout << endl;
	}

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

	void Test_push_back()
	{
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);
		lt.push_back(5);

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

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

	void Test_insert()
	{
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);
		lt.push_back(5);
		//list<int>::iterator pos = find(lt.begin(), lt.end(), 2);// 涉及其它问题,先不这样写
		//if (pos != lt.end())
		//{
		//	lt.insert(pos, 20);
		//}
		lt.insert(++lt.begin(), 20);
		lt.push_front(0);
		lt.push_front(-1);
		print_list(lt);
	}

	void Test_erase()
	{
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);
		lt.push_back(5);

		lt.erase(++++lt.begin());//发现个好玩的,(删除3)
		lt.pop_back();
		lt.pop_front();
		print_list(lt);
	}

	void Test_copy() 
	{
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);
		lt.push_back(5);

		list<int> lt2(lt);
		for (auto& e : lt2)
		{
			e *= 10;
		}
		print_list(lt);
		print_list(lt2);
	}
}

int main()
{
	//rtx::Test_push_back();
	//rtx::Test_arrow();
	//rtx::Test_insert();
	//rtx::Test_erase();
	rtx::Test_copy();

	return 0;
}

本篇完。

list 的反向迭代器放在后面栈和队列期间讲,下一部分:栈和队列:使用,OJ,模拟实现。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/620993.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Vulnhub 靶机渗透:SICKOS: 1.2

SICKOS: 1.2 一级目录二级目录三级目录 nmap 扫描端口扫描详细扫描漏洞扫描 web渗透gobuster扫描nikto漏洞扫描思考继续 获得立足点提权总结 一级目录 二级目录 三级目录 https://www.vulnhub.com/entry/sickos-12,144/ 靶机IP&#xff1a;192.168.54.30 kali IP: 192.168.5…

如何修复 SSH Client_loop: send disconnect: Broken pipe Error

动动发财的小手&#xff0c;点个赞吧&#xff01; SSH 是 Secure Shell 的缩写&#xff0c;是一种远程网络协议&#xff0c;用于通过 TCP/IP 网络安全地连接到远程设备&#xff0c;例如服务器和网络设备。 它是一种加密网络协议&#xff0c;可提供强大的加密技术和散列法来保护…

SpringSecurity多源认证之全部交给spring容器

文章目录 一. 前言二. 配置流程2.1 SecurityConfig.class2.2 JwtAuthenticationTokenFilter2.3 AuthenticationManagerProcessingFilter 疑问 一. 前言 相关文章: 认证/支付/优惠劵策略模式-security多源认证 这篇文章没有将自定义的认证管理器注入容器. spring-security2.6.…

【计算机网络详解】——运输层(学习笔记)

&#x1f4d6; 前言&#xff1a;两台主机的通信&#xff0c;实际上两台主机中的应用进程进行通信&#xff0c;而在一台计算机中&#xff0c;用不同的端口号标识不同的应用进程。本节将介绍传输层的相关内容&#xff0c;包括端口号的分配方法、端口号的复用与分用、以及传输层的…

吴恩达 ChatGPT Prompt Engineering for Developers 系列课程笔记--07 Expanding

07 Expanding 本节示例如何用ChatGPT生成一封电子邮件的回复。 1) 定制化情绪 给定客户评论&#xff0c;我们根据评论内容和情绪产生定制的回复。下面是给定情感&#xff08;positive/negative&#xff09;&#xff0c;让ChatGPT产生相应回复的prompt。 """…

Solidwoks PDM Add-ins (C#) 创建Add-ins

本主题演示如何在Microsoft Visual Studio Enterprise 中使用Visual C#创建并调试add-in。 注意&#xff1a; 因为 SOLIDWORKS PDM Professional无法强制重新加载在 .NET 中编写的add-in程序&#xff0c;则必须重新启动所有客户端计算机&#xff0c;以确保使用最新版本的add-i…

【建议收藏】什么是测试金字塔?如何使用测试金字塔来构建自动化测试体系?

测试金字塔 &#xff08;Test Pyramid&#xff09;是一套使用单元测试&#xff0c;集成测试和端到端测试来构建自动化测试体系的方法。 如下图所示&#xff0c;在金字塔的最下方是单元测试&#xff0c;中段是集成测试&#xff0c;最上方是端到端测试。单元测试实现的成本最低&…

【论文笔记】SAM3D: Zero-Shot 3D Object Detection via Segment Anything Model

原文链接&#xff1a;https://arxiv.org/pdf/2306.02245.pdf 1.引言 分割一切模型&#xff08;SAM&#xff09;作为视觉领域的基石模型&#xff0c;有强大的泛化性&#xff0c;能解决很多2D视觉问题。但是SAM是否可以适用于3D视觉任务&#xff0c;仍需要被探索。   目前几乎…

深蓝学院C++基础笔记 第 2 章 对象和基本类型

第 2 章 对象和基本类型 1. 从初始化/赋值语句谈起 初始化 / 赋值语句是程序中最基本的操作&#xff0c;其功能是将某个值与一个对象关联起来 – 值&#xff1a;字面值、对象&#xff08;变量或常量&#xff09;所表示的值…… – 标识符&#xff1a;变量、常量、引用…… –…

《Lua程序设计》--学习2

表 Lua语言中的表本质上是一种辅助数组&#xff08;associative array&#xff09;&#xff0c;这种数组不仅可以使用数值作为索引&#xff0c;也可以使用字符串或其他任意类型的值作为索引&#xff08;nil除外&#xff09;。 Lua语言中的表要么是值要么是变量&#xff0c;它…

Linux进程间通信【命名管道】

✨个人主页&#xff1a; 北 海 &#x1f389;所属专栏&#xff1a; Linux学习之旅 &#x1f383;操作环境&#xff1a; CentOS 7.6 阿里云远程服务器 文章目录 &#x1f307;前言&#x1f3d9;️正文1、什么是命名管道1.1、创建及简单使用1.2、命名管道的工作原理1.3、命名管道…

Shell脚本攻略:文本三剑客之awk

目录 一、理论 1.awk原理 2.awk打印 3.awk条件判断 4.awk数组与循环 5.awk函数 6.常用命令 二、实验 1.统计磁盘可用容量 2.统计/etc下文件总大小 3.CPU使用率 4.统计内存 5.监控硬盘 一、理论 1.awk原理 &#xff08;1&#xff09;概念 awk由 Aho&#xff0c;W…

PriorityBlockingQueue的介绍及方法内部实现

SynchronousQueue的介绍 SynchronousQueue是一个优先级队列&#xff0c;不满足先进先出FIFO的概念。 会将插入的数据进行排序&#xff0c;输出排序之后的结果&#xff08;小根堆&#xff0c;由小变大升序&#xff09; 内部实现原理介绍 SynchronousQueue是基于二叉堆结构实现…

Linux——多线程

Linux多线程 多线程进程内进行资源划分什么是线程进一步理解线程线程的优缺点Linux进程VS线程线程的异常 创建线程两个的接口线程的控制线程的创建线程的终止线程的等待线程取消C的线程库线程的分离如何理解每个线程都有自己独立的栈结构 封装线程接口 多线程 进程内进行资源划…

Java代码块和属性的赋值顺序

代码块 类的成员之四&#xff1a;代码块(初始化块)&#xff08;重要性较属性、方法、构造器差一些&#xff09; 1.代码块的作用&#xff1a;用来初始化类、对象的信息 2.分类&#xff1a;代码块要是使用修饰符&#xff0c;只能使用static 分类&#xff1a;静态代码块 vs 非静态…

nacos升级到2.0.3(单机模式)

前提&#xff1a;https://github.com/alibaba/spring-cloud-alibaba/wiki/版本说明 Spring Cloud AlibabaSpring CloudSpring BootNacos2.2.7.RELEASESpring Cloud Hoxton.SR122.3.12.RELEASE2.0.3 一、pom.xml文件 <parent><groupId>org.springframework.boot&…

网工内推 | 高级网工专场,上市公司,3年经验以上,HCIE证书优先

01 名创优品&#xff08;广州&#xff09;有限责任公司 &#x1f537;招聘岗位&#xff1a;高级网络工程师 &#x1f537;职责描述&#xff1a; 1、负责集团总部有线&#xff06;无线、公有云、仓库的网络规划建设与运维&#xff1b; 2、负责公有云的网络台日常上线部署、规划…

3.3 分析特征内部数据分布与分散状况

3.3 分析特征内部数据分布与分散状况 3.3.1 绘制直方图 bar()3.3.2 绘制饼图 pie()3.3.3 绘制箱线图 boxplot()3.3.4 任务实现1、绘制国民生产总值构成分布直方图2、绘制国民生产总值构成分布饼图3、绘制国民生产总值分散情况箱线图 小结 3.3.1 绘制直方图 bar() 直方图&#x…

Vue源码解析

【尚硅谷】Vue源码解析之虚拟DOM和diff算法 【Vue源码】图解 diff算法 与 虚拟DOM-snabbdom-最小量更新原理解析-手写源码-updateChildren] 文章目录 2. snabbdom 简介 及 准备工作2.1 简介2.2 搭建初始环境1. 安装snabbdom2. 安装webpack5并配置3. 复制官方demo Example 3. …

IJCAI 2023 | 如何从离散时间事件序列中学习因果结构?

本文分享一篇我们在IJCAI 2023的最新工作&#xff0c;文章分析了在离散时间事件序列上存在的瞬时效应问题&#xff0c;提出了一种利用瞬时效应的结构霍克斯模型&#xff0c;且在理论上证明了事件序列上的瞬时因果关系同样是可识别的。 相关论文&#xff1a; Jie Qiao et al. “…