c++ list的使用 及 模拟实现

1. list学习

1.1. list 的介绍

https://legacy.cplusplus.com/reference/list/list/?kw=list
这里我们还是和前面一样,跟着文档走,主要学习和 string 和 vector 中没有的函数
在这里插入图片描述
list 是一个带头双向循环链表
list允许在O(1) 的时间复杂度的情况下,进行插入删除
下面是 list 中实现的函数
在这里插入图片描述

1.2. 迭代器的使用

void test_list1()
{
	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(auto e : lt)
	{
		cout << e << "  ";
	}
	cout << endl;

	list<int>::reverse_iterator it2 = lt.rbegin();
	while(it2 != lt.rend())
	{
		cout << *it2 << "  ";
		it2++;
	}
	cout << endl;
}

在这里插入图片描述
正向,反向,范围for,和之前一样的写法

由于之前对string 和 vector 中的函数学习,这里的迭代器应该没什么问题,至于迭代器详细的内容,后面会放在迭代器的模拟实现中介绍。

1.3. Operation

在这里插入图片描述
由于 list 的结构和原先不同(list是带头双向循环链表),所以这里的函数主要介绍前面没有的,比如这里的 Operation 都是以前 string 和 vector 中没见过的函数。

1.3.1. reverse

在这里插入图片描述

上面写反向迭代器的时候,我们使用过这个单词,是翻转的意思,所以这个函数在这起到的作用就是翻转链表

void test_list2()
{
	list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	lt.push_back(5);
	for(auto e : lt)
	{
		cout << e << "  ";
	}
	cout << endl;
	lt.reverse();
	for(auto e : lt)
	{
		cout << e << "  ";
	}
	cout << endl;
}

在这里插入图片描述
注:一定要注意,翻转的单词是 reverse,reserve是 保留。在 string 中是保留传入的空间大小

1.3.2. sort

看名字就知道,排序
在这里插入图片描述
注意,这里的排序默认顺序是 从小到大

void test_list2()
{
	list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	lt.push_back(5);
	for(auto e : lt)
	{
		cout << e << "  ";
	}
	cout << endl;
	lt.reverse();
	for(auto e : lt)
	{
		cout << e << "  ";
	}
	cout << endl;
	lt.sort();
	for (auto e : lt)
	{
		cout << e << "  ";
	}
	cout <<endl;
}

在这里插入图片描述
这里我们先逆置,然后在排序,就可以将原本由大到小的顺序,排成由小到大的样子。
sort本身不是太难,sort的底层采用的是归并排序。
但是下面还有三个问题

1. 为什么不使用库中的排序函数

在算法库 中,我们可以找到,库中原本已经实现了一个 sort 排序函数,但是为什么在 list 库中还要自己实现一个呢
在这里插入图片描述
我们先看看 算法库 中的 sort 有什么特点
在这里插入图片描述
这里的 sort 使用的是模版,模版参数是 RandomAccessIterator
random(随机) iterator(迭代器)
这一长串的意思是 随机访问迭代器
那我们的 list 使用的是什么迭代器呢
我们打开 list 的 begin 函数
在这里插入图片描述
这里清楚的写了,BidirectionalIterator 双向迭代器
我们之前学的有 正向迭代器,反向迭代器,但是这是从功能上的分类。
在传入参数的时候,这里的迭代器存在性质上的区别,分别有 :
单向,双向,随机 三种迭代器
单向迭代器(只支持++,不支持–):单链表,哈希表
双向迭代器(即支持++,也支持–):双向链表,list
随机迭代器(除了支持++,–,还支持+,-操作):顺序表 vector / string。
string中的迭代器:
在这里插入图片描述
这里我们可以看见 string 中的迭代器是随机访问迭代器,所以我们就可以在算法库中的sort中传入 string 类型的对象。
如果我们强行使用的话

 void test_list2()
 {
 	list<int> lt;
 	lt.push_back(1);
    lt.push_back(67);
    lt.push_back(9);
    lt.push_back(4);
    lt.push_back(5);
    for (auto e : lt)
    {
		cout << e << "  ";		
	}
	sort(lt.begin(),lt.end());
	for (auto e : lt)
	{
		cout << e << "  ";
	}
	cout << endl;
}

在这里插入图片描述
这里就会出现问题
算法库中的 sort 使用的是快排,存在迭代器之间±的操作
随机访问迭代器支持 ± 操作,所以没问题。
双向链表物理空间不连续,它的迭代器是双向迭代器,不能支持 ± 。
在这里插入图片描述

2. sort逆序输出

上面我们说了,list 的 sort 只能支持 从小到大 的顺序
那么如果我们想要 从大到小 的顺序排序呢?
(这里的内容稍微有点超纲,后面学习会详细补充,这里先简单介绍怎么用)
想要 降序,就需要使用 greater
greater() 创建匿名对象

void test_list2()
{
 	list<int> lt;
 	lt.push_back(1);
    lt.push_back(2);
    lt.push_back(3);
    lt.push_back(4);
    lt.push_back(5);
    for (auto e : lt)
    {
		cout << e << "  ";		
	}
	lt.sort()//从小到大排序
	for (auto e : lt)
	{
		cout << e << "  ";
	}
	cout << endl;
	lt.sort(greater<int>());//从大到小排序
	for (auto e : lt)
	{
		cout << e << "  ";
	}
	cout << endl;
}

在这里插入图片描述
list 的 sort , 使用的是归并排序,使用归并代价能小点。

3. 效率问题

这里我们用 list 中的 sort 和 算法库中的 sort 进行一个时间对比,看两者效率上有何差距

void test_op()
{
	srand(time(0));
	const int N = 100000;
	vector<int> v;
	v.reserve(N);

	list<int> lt1;
	list<int> lt2;
	for (int i = 0; i < N; i++)
	{
		ayto e = rand();
		lt1.push_back(e);
		v.push_back(e);
	}
	int begin1 = clock();
	sort(v.begin(), v.end());
	int end1 = clock();

	int begin2 = clock();
	lt1.sort();
	int end2 = clock();

	printf("vector sort:%d\n", end1 - begin1);
	printf("list sort:%d\n", end2 - begin2);
}

(一般排序默认都是在 release 版本下,跑的更快)
在这里插入图片描述
因为我们可以使用其他容器的迭代器对 list 初始化
所以我们可以先在 vector 中把数据排好序,然后再用排好序的数据对 list 初始化(典型的空间换时间)

	int begin1 = clock();
	sort(v.begin(), v.end());
	lt2.assign(v.begin(), v.end());
	int end = clock();

在这里插入图片描述
这里 vector排序 + 数据复制 的速度能比 list 的 sort 快一点,但是实际上差距已经不是太大了。
list 的 sort 在性能上并没有太大的优势,但是相对来说方便点。

1.3.3. merge

merge 合并
在这里插入图片描述
单词的意思应该比较好理解,这个函数就是用来合并两个链表的。

void test_list3()
{
	list<int> lt1{1, 3, 5, 7, 9};
	list<int> lt2{2, 4, 6, 8, 10};
	for (auto e : lt1)
	{
		cout << e << "  ";
	}
	cout << endl;
	for (auto e : lt2)
	{
		cout << e << "  ";
	}
	cout << endl;
	lt1.merge(lt2);
	for (auto e : lt1)
	{
		cout << e << "  ";
	}
	cout << endl;
	for (auto e : lt2)
	{
		cout << e << "  ";
	}
	cout << endl;

在这里插入图片描述
这里有几点需要注意:

  1. 这里的数组合并,前提是两个数组必须都是有序的
  2. 这里的合并,是直接把传入 list对象 的节点转移,并不会创建新的节点。
  3. merge 是 成员函数,所以调用方式是,list对象调用,传入一个 list 对象。
  4. 合并以后得新链表,顺序是从小到大的。
    如果我们传入的并不是两个有序的 list 对象
list<int> lt1{1, 3, 5, 7, 9};
list<int> lt2{10, 8, 6, 4, 2};


这里就直接崩了,所以使用 merge 之前,最好也使用 sort 先进行排序。

1.3.4. unique

unique 独特的
在这里插入图片描述

这个函数是用来去重的,但是和上面一样,数据必须要有序。

void test_list3()
{
	list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(3);
	lt.push_back(3);
	lt.push_back(4);
	lt.push_back(4);
	lt.push_back(5);
	lt.push_back(5);
	lt.push_back(6);
	lt.sort();
	lt.unique();
	for (auto e : lt)
	{
		cout << e << "  ";
	}
	cout << endl;
}

在这里插入图片描述
这里的去重的方法采用的是 双指针的方法,必须要有序,所以这里还是最好配合 sort 进行使用。

1.3.5. splice

splice 粘接
在这里插入图片描述
就是把一个链表转移到另一个链表上,但是这里和上面的 merge 一样,是转移节点,并不是复制节点。

void test_list4()
{
	list<int> lt1, lt2;
	list<int>::iterator it;
	for (int i = 1; i <= 4; i++)
	{
		lt1.push_back(i);
	}
	for (int i = 1; i <= 3; i++)
	{
		lt2.push_back(i * 10);
	}
	it = lt1.begin();
	++it;
	for (auto e : lt1)
	{
		cout << e << "  ";
	}
	cout << endl;
	for (auto e : lt2)
	{
		cout << e << "  ";
	}
	cout << endl;
	lt1.splice(it, lt2);
	for (auto e : lt1)
	{
		cout << e << "  ";
	}
	cout << endl;
	for (auto e : lt2)
	{
		cout << e << "  ";
	}
	cout << endl;
}

在这里插入图片描述
这里就不需要像上面那样必须有序。
注意传入的参数
上面我们使用的是第一种,第一个参数是被插入的对象,第二个参数是往第一个对象里插入的对象。
其他方式基本上差不太多,知道怎么使用就行了。
就是要注意这里插入是 节点直接插入,会改变传入参数的链表。

2. list 的模拟实现

2.1. 成员变量

和我们之前学过的双向链表一样,每一个节点都是由 data , next , prev 组成的。
所以我们先写这个节点的成员变量

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

节点的结构体,这里使用模版,支持传入的各种类型。
有了节点的结构体,下面就开始实现带头双向循环链表
成员变量:

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

2.2. 构造函数

我们先实现 list 的构造函数

template<class T>
class list
{
	typedef list_node<T> Node;
public:
	void empty_init()
	{
		_head  = new Node;
		_head->_next = _head;
		_head->_prev = _head;
	}
	list()
	{
		empty_init();
	}
private:
	Node* _head;
}

这里我们使用了 new 来动态开辟空间,所以我们可以给节点的结构体写一个构造函数 (new = operator new + 构造函数),这样我们就不需要自己手动创建节点了。

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

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

注意这里的额 list_node 的构造函数,里面的缺省参数是匿名对象,这里不能直接给 0 或者其他的类型,T 的类型是不确定的,可能是 int, float , 也可能是 string 等类型,所以最好传入 匿名对象。

2.3. push_back

尾插,思路还是双向链表的尾插,唯一不同的就是注意模版即可

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

	tail->_next = newnode;
	newnode->_prev = tail;

	_head->_prev = newnode;
	newnode->_next = _head;
}

2.4. 迭代器的实现

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

这里实现迭代器比前面模拟实现 string 和 vector 是有难度的。
迭代器,在使用上我们可以把他当成指针,但是底层不一定是指针实现的。
前面模拟实现的 string 和 vector 是数组实现的顺序表,物理空间是连续的,所以我们直接对指针 ++,-- 的操作就可以完成迭代器的操作
但是 list 是链表,物理空间不连续,所以我们不能直接使用指针进行++,–的操作

templatee<class T>
struct __list_node Node;
{
	typedef list_node<T> Node;
	typedef __list_iterator<T> self;
	Node* node;

	__list_iterator(Node* node)
		:_node(node)
	{};
	self& operator++()
	{
		_node =_node->_next;
		reeturn *this;
	}
	T& operator*()
	{
		return _node->_data;
	}
	bool operator!=(const self& s)
	{
		return _ndoe != s._node;
	}
};

这里我们实现了 " -> " 的运算符重载,如果想详细了解的话,可以先看下面 3.3 的细节问题中的第3点。
这里我们就简单模拟实现了 list 的普通的迭代器
这里我们已经实现了 3 个类了,一个是节点的结构体,一个是迭代器的结构体,一个是 list 对象的类,不要搞混。
这里理解一下这个迭代器的结构体实现了什么功能。
首先,我们在 list 类中创建 iterator,就是创建了一个 __list_node 的结构的对象
这个对象,我们是传入 node 节点的指针初始化 _node
,我们在这个结构体中实现了 ++,–,* 等操作,所以我们直接可以对 _node 进行上述操作,如 ++ 我们返回 _node->_next ,这样就能完成我们迭代器的需求。
按照上面的过程,我们最后就可以像使用数组的指针的方法一样使用迭代器。
上面实现迭代器的方式,我们称之为封装

typedef __list_node<T> iterator;
iterator begin()
{
	return iterator(_head->_next);
}
iterator end()
{
	return _head;
}

我不需要知道 这个迭代器底层是什么实现的,这和我这里使用迭代器没什么关系,我这里使用迭代器的方式和 string , vector 是一样的,所以这就是封装的好处,对使用者来说方便。

这里还需要注意:
begin 返回第一个节点的位置,end 返回最后一个节点后面的位置。这里 _head 作为头结点,不存储值,所以begin 需要返回 _head->_next,end 返回最后一个节点的下一个位置,就是 _head 的位置。
因为我们返回值都是 iterator,我们这里直接返回 _head ,编译器会自己用这个值 去构造一个 iteratoer 的对象。

实现了普通迭代器,我们也可以使用范围for 对 list 进行输出

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

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

在这里插入图片描述
至于 const 迭代器怎么实现,文章后面会说

2.5. insert

插入,老朋友了
以前链表就是想,但是这里的 insert 作为容器内的函数,我们就不能像以前那样用指针来修改,我们就需要使用 迭代器来修改。
pos 前插入 val

 void insert(iterator pos, const T& val)
 {
 	Node* newnode = new Node(val);
 	Node* cur = pos._node;
 	
	Node* prev = cur->_prev;
	
	newnode->_next = pos;
	pos->_prev = newnode;

	newnode->_prev = prev;
	prev->_next = newnode;
}

注意, vector 的 insert 存在迭代器失效的问题,但是 list 这里并不会存在 迭代器失效的问题。
前面 vector 出现迭代器失效的问题,是因为:在插入元素后,当前迭代器指向的位置会发生改变,有可能会出现野指针
list 是链表插入,不存在迭代器失效的问题。
不过我们也可以写上返回值,返回值就是插入元素位置的 迭代器。

iterator insert(iterator pos, const T& val)
{
	//...
	return newnode;
}

2.6. erase

删除,删除当前迭代器所指的节点。

void erase(iterator pos)
{	
	Node* cur = pos._node;
	Node* prev = cur->-prev;

	Node* next = cur->_next;
	delete cur;
	prev->_next = next;
	next->_prev = prev;
}

这里就要注意,删除当前节点后,迭代器就会失效,原先迭代器指向的节点已经删除了。
所以为了使使用起来能方便点,我们可以让 erase 有个返回值,这个返回值就是删除节点的下一个节点。

iterator erase(iterator pos)
{
	//...
	return next;
}

2.7. pop,push

有了上面实现的 insert 和 erase,我们就可以实现 pop_back , pop_front , push_back , push_front.
上面我们虽然实现了 push_back , 但是push_back 和 insert 的函数实现基本相似,所以我们也可以使用 insert 实现 push_back。

void push_back(const T& x)
{
	insert(end()--,x);
}
void push_front(const T& x)
{
	insert(begin(),x);
}
void pop_back()
{
	erase(end()--);
}
void pop_front()
{
	erase(begin();
}

因为 begin(), end() 能返回迭代器,所以这里可以使用这两个函数来返回。也可以直接给 insert 和 erase 传入 _head->_next 或者 _head->_prev;
这样复用上面函数,实现 push, pop, 可以减少代码量,方便我们实现。

2.8. clear

删除除了头结点之外的所有节点。
我们之前使用的是 使用指针遍历链表,这里我们可以使用迭代器遍历链表

void clear()
{
	iterator it = begin();
	while (it != end())
	{
		it = erase(it);
	}
}

我们上面实现的 erase ,会返回删除节点的下一个结点,所以这里我们 erase it 后并不需要对 it++,直接接受返回值即可。

2.9. 析构函数

直接复用上面的 clear 函数,然后删除头结点即可。

~list()
{
	claer();
	delete _head;
	_head = nullptr;
}

我们创建节点的时候,每个节点都只是 new 了一块空间,并没有 new Node[] ,所以这里我们使用 delete删除节点就行了。

2.10. 拷贝构造

list(list<T>& lt)
{
	empty_init();
	for (auto e : lt)
	{
		push_back(e);
	}
}

先对调用函数的对象,进行初始化,然后用 push_back 把 需要拷贝的对象中的顺序一个一个 push进调用函数的对象。
原本我们这里应该传入 const 对象,因为传入的对象我们不能对其修改,但是由于我们上面没有实现 const 迭代器,这里传入 const 的话吗,下面 范围for没有const迭代器使用,就会出现问题。

2.11. 赋值运算符重载

拷贝构造,在 string 的模拟实现中学过,有两种实现方式
第一种是先删除原有数据,然后拿传入数据覆盖进去。
第二种是直接用传入的对象构造一个对象,然后让构造的新对象和需要被赋值的对象成员变量进行交换。
第一种:

list<T>& operator=(const list<T>& lt)
{
	if (this != &lt)
	{
		clear();
		for (auto e : lt)
		{
			push_back(e);
		}
	}
	return *this;
}

第二种的现代写法

void swap(list<T>& tmp)
{
	std::swap(_head, tmp._head);
}
list<T>& operator=(list<T> tmp)
{
	swap(tmp);
	return *this;
}

在这里插入图片描述

2.12. size

返回链表中有数据的节点个数。
这里可以是遍历一遍链表,创建一个 count 变量,最后返回,也可以直接修改 双向链表的成员变量,新建一个 _size 变量,每次插入数据++,删除数据–。
因为我们上面 push,pop 都是直接复用的 insert 和 erase,所以我们只需要对 insert 和 erase 中 加上 _size++ 和 _size-- 即可。

size_t size()
{
	return _size();
}
template <class T>
class list
{
	//...
private:
	Node* _head;
	size_t _size;
};

3. const迭代器的实现

上面我们简单实现了 list 的普通迭代器,但是没有实现 const 迭代器,这里我们先简单说一下原因;
我们的const迭代器可以这样写吗

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

我们要实现的迭代器是可以遍历整个链表的,const迭代器是可以遍历 const 修饰的 list 对象的。
遍历简单来说就是支持上面 ++,==,等操作。
iterator----对普通对象实现可读可写可遍历的操作
const_iterator----const对象实现可读,可遍历
const iterator----只读。
我们前面 对普通迭代器进行封装,所以普通迭代器支持了 ++,–的操作,但是上面那种写法,传入的是 const 对象,返回的是 const 修饰的 迭代器。
因为权限问题,我们这个 const 的 迭代器,是没办法调用普通迭代器的成员函数的。
所以想要让 const 支持的迭代器也能实现 ++,–的操作。

3.1. 方法一:新类

直接和上面实现 iterator 一样,我们直接实现一个 __list_const_iterator 类,这个类支持 const_iterator;

template<class T>
struct __list_const_iterator
        {
                typedef list_node<T> Node;
                typedef __list_const_iterator<T> self;
                Node* _node;

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

                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)
                {
                        Node* tmp(*this);
                        _node = _node->_prev;
                        return tmp;
                }
                T& operator*()
                {
                        return _node->_data;
                }
                // it->_a1 = 10 
                T* operator->()
                {
                        return &_node->_data;
                }
                bool operator!=(const self& s)
                {
                        return _node != s._node;
                }
                bool operator==(const self& s)
                {
                        return _node == s._node;
                }
        };
template<class T>
class list
{
public:
	typedef __list_iterator<T> iterator;
	typedef __list_const_iterator<T> const_iterator;
	//...

这样,在 xsz 这个命名空间,我们就是实现了 4个类,节点一个结构体,iterator 一个类, const_iterator 的类,还有 list 的类。
在实现了这个之后,我们前面的拷贝构造就可以 传入const对象了。

3.2. 大佬的方式

既然这里的 const_iterator 和 iterator 的主要函数基本上没变,那么我们可不可以把他们写在一起?
我们可以参考一下 stl 中的原码是怎么实现的
在这里插入图片描述
原码中是把 T,T& ,T* 都作为模版参数 T,Ref,Ptr传入 __list_iterator 的类中,我们可以试试

template<class T, class Ref, class Ptr>
class __list_iterator
{
	typedef list_node<T> Node;
	typedef __list_iterator<T, Ref, Ptr> self;
	Node* _node;
	typedef Ptr pointer;
	typedef Ref reference;

		__list_iterator(Node* node)
			:_node(node)
		{}
		self& operator++()
		{
			_node = _node->_next;
			return *this;
		}
		self operator++(int)
		{
			self tmp(_node);
			_node = _node->_next;
			return tmp;
		}
		self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}
		self operator--(int)
		{
			self tmp(_node);
			_node = _node->_next;
			return tmp;
		}
		reference operator*()
		{
			return _node->_data;
		}
		pointer operator->()
		{
			return &_node->_data;
		}
		bool operator==(const self& s)
		{
			return _node == s._node;
		}
		bool operator!=(const self& s)
		{
			return _node != s._node;
		}
	};
template<class T>
class list
{
	typedef list_ndoe Node;
	typedef __list_iterator<T, T&, T*> iterator;
	typedef __list_iterator<T, const T&, const T*> const_iterator;
	//...

这里可能不是那么好理解,简单来说,我们可以从表面上把 模版传参理解成含函数传参,只不过模版传的是类型。
传入 T&,T*,那么迭代器的模版就要生成一个对应的迭代器类,传入 const T&, const T* ,迭代器就要生成这个类。

3. 3. 细节问题

1. typename

我们上面实现了 const 类,此时我们想要使用 const 来完成 自己的 print 函数

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

我们会发现,这里运行不了
在这里插入图片描述
注意这句代码

list<T>::const_iterator it = lt.begin();

list::const_iterator
主要我们要搞清楚这是什么,我们上面认为这是一个类型名,但是这种写法和静态成员变量的使用会发生冲突。
编译器会认为 const_iterator 可能是静态成员变量,也可能是类型名,会起冲突。
所以我们要在前面强调,他就是个类型名

typename list<T>::const_iterator it = lt.begin();

在这里插入图片描述
这样这里就能正常运行了。

2. print_container

上面我们实现了 print_list 函数,可以对我们模拟实现的 list 中的各种类型进行输出。
既然所有的迭代器遍历写法都是相同的,我们有没有办法写一个函数,让这个函数对所有的容器都能输出?

templatee<typename container>
void print_container(const container& con)
{
	typename container::const_iterator it = con.begin();
	while (it != end())
	{
		cout << *it << "  ";
		it++;
	}
	cout << endl;
}

在这里插入图片描述

3. “ -> ”的运算符重载

这里有个小优化。
如果我们模拟实现的 list 对象内部成员是 自定义类型

struct AA
{
	AA(int a1 = 0, int a2 = 0)
		:_a1(a1)
		,_a2(a2)
	{}
	int _a1;
	int _a2;
};
void test_list3()
{
	xsz::list<AA> lt;
	lt.push_back(AA(1,2));
	lt.push_back(AA(3,4,));
	lt.push_back(AA(5,6));
	
	xsz::list<AA>::iterator it = lt.begin();
	while (it != lt.end())
	{
		cout << *it << "  ";
		it++;
	}
	cout << endl;
}

首先,这里最先报的错就是 cout << *it 这里,因为我们的 AA 类并没有实现cout 的功能,所以这里会出问题,那么应不应该支持 cout 呢。
如果支持 cout 我们应该怎么输出,是 _a1 和 _a2 中间空一格吗,还是不空,他们中间要加上 ‘|’ 吗,不确定,因为不一定满足所有的需求,所以最好的方法就是不写 cout(vector 就没有实现 cout,cin的操作)。
这种情况下,要么 AA 实现一个 a1函数 a2函数的接口,返回 _a1,_a2 的值,要么函数成员变量设置为公有让直接访问,外面爱怎么输出怎么输出。
这里是采用了公开成员变量让外界访问
所以我们这里直接访问就行

cout << (*it)._a1 << "  " << (*it)._a2 << endl;

在这里插入图片描述

OK,那么问题来了,我们实现的迭代器是想像指针那样使用的,但是这里用到了解引用,很麻烦,但是我们可以直接使用 -> 吗

cout << it._node->_data._a1 << endl;

在这里插入图片描述

我们就要这样写,因为 it 并不是 _data 的指针,it 是迭代器,成员变量是 _data,我们不能像使用指针那样使用它。
因此我们可以想办法让 -> 返回的就是 _data 的地址,这里就能通过 重载 -> 实现

T* operatoe->()
{
	return &_node->_data;
}

在这里插入图片描述
还有一个细节:
(it-> ) 会返回 _data 的地址,明明应该后面再加上一个 ->才可以使用
(it->)_a1;
这里也是个编译器的优化,只需要一个就可以实现了。
我们这里的 -> 也是为了 自定义类型准备的。

4. 深拷贝问题

前面模拟实现 vector 存在一个问题

void test1()
{
	xsz::vector<string> v;
	v.push_back("11111111111111111");
	v.push_back("11111111111111111");
	v.push_back("11111111111111111");
	v.push_back("11111111111111111");
	v.push_back("11111111111111111");
	for (auto e : v)
	{
		cout << e << "  ";
	}
	cout << endl;
}

这里之前发生的问题是只有最后一个能输出,因为memcpy 只能是浅拷贝,也就是只会把 内部成员 stirng对象的地址拷过去,所以前面4个都是乱码。
但是 list 我们需要考虑这个问题吗
在这里插入图片描述
这里使用 我们模拟实现的 list 是没有问题的。
因为每个 string 对象对 list 来说只是相当于一个节点,在进行其他节点插入操作的时候,是不会印象其他节点的地址的。这也是链表优于数组顺序表的地方。

最后呢,因为前面考试多,加上英语4级考试,事情确实比较多,最近还要做数据结构和数字电路逻辑的课设,写的速度会慢很多。
总之这里先祝愿看到的大学生考试不挂科,考英语46级的,考研的必过😁

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值