C++STL之list容器:基本使用及模拟实现

news2024/11/13 9:27:30

目录

有了vector,为何还需list

list的使用

1,push_back、push_front、pop_back、pop_front的使用

2,正向、反向、const正向、const反向迭代器的使用

正向、反向迭代器的使用

const正向、const反向迭代器的使用

3,operator= 赋值

4,insert、erase 任意位置的插入、删除

5,迭代器失效(★)

情景一:

情景二:

小结:

list的模拟实现

源码的引入

1.基本结构的搭建

__list_node

 list

__list_node 的完善

push_back的实现

__list_itertaor

基本结构的整体代码

迭代器是如何执行起来的

2.迭代器的进一步理解

3.const 迭代器的实现

const迭代器的实现

4.insert 和 erase 的实现

5. pop_back、push_front、pop_front 的实现

6.拷贝构造

7.析构函数

8.operator= 赋值

9.深浅拷贝问题

10.整体代码

list.h

test.cpp

常见面试题

1,vector 和 list的区别?

2,vector 和 list 底层是如何实现的 ?

3,vector 是如何增容的?

4,什么是迭代器失效?


有了vector,为何还需list

为什么会有 list ?

因为 list 补充 vector 的缺点而存在的。

vector的缺点:

1、头部和中部的插入删除效率低。0(N), 因为需要挪动数据。

2、插入数据空间不够需要增容。增容需要开新空间、拷贝数据、释放旧空间,会付出很大的代价。

vector的优点:

支持下标的随机访问。 间接的就很好的支持排序、二分查找、堆算法等等。

list的产生就是为了解决vector的缺陷而存在的。

list的优点:

1、list头部、中间插入不再需要挪动数据,效率高。0(1)。

2、list插入数据是新增节点,不需要增容。

list的缺点:

不支持随机访问。

所以实际使用中vector和list是相辅相成的两个容器。


list的使用

list 的使用方法在一定程度上与 vector 类似。若熟悉 vector 的使用,那么在使用 list 时也会感到有一定的相似性。以下是list最常使用的接口,比较简单。

1,push_back、push_front、pop_back、pop_front的使用

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include <list>
void test1() 
{
	list<int> lt;
	//push_back 尾插
	lt.push_back(1); 
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	lt.push_back(5);
	//pop_back 尾删
	lt.pop_back();
	//push_front 头插
	lt.push_front(0);
	lt.push_front(-1);
	//pop_front 头删
	lt.pop_front();
	for (auto e : lt) 
	{
		cout << e << " ";
	}
	cout << endl;
}
int main() 
{
	test1();
	return 0;
}

2,正向、反向、const正向、const反向迭代器的使用

正向、反向迭代器的使用

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include <list>
void test2() 
{
	list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	lt.push_back(5);
	lt.push_back(6);
	//1,正向迭代器
	list<int>::iterator it = lt.begin();
	while (it != lt.end()) 
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
	//反向迭代器
	list<int>::reverse_iterator rit = lt.rbegin();
	while (rit != lt.rend())
	{
		cout << *rit << " ";
		++rit;
	}
	cout << endl;

}
int main() 
{
	test2();
	return 0;
}

const正向、const反向迭代器的使用

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include <list>
//1,const正向迭代器
void Print_list(const list<int>& lt) //const对象在传参的时候会产生
{
	list<int>::const_iterator it = lt.begin();  // lt是 const 会去调用const的begin
	while (it != lt.end())
	{
		//*it = 1;  不能修改
		cout << *it << " ";
		++it;
	}
	cout << endl;
}
//2,const反向迭代器
void Print_reverse_list(const list<int>& lt) 
{
	list<int>::const_reverse_iterator rit = lt.rbegin();
	while (rit != lt.rend()) 
	{
		cout << *rit << " ";
		++rit;
	}
	cout << endl;
}
void test3() 
{
	list<int> lt;
	lt.push_back(10);
	lt.push_back(20);
	lt.push_back(30);
	lt.push_back(40);
	lt.push_back(50);
	lt.push_back(60);
	//1,const正向迭代器
	Print_list(lt);
	//2,const反向迭代器
	Print_reverse_list(lt);
}
int main() 
{
	test3();
	return 0;
}

3,operator= 赋值

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include <list>
void test4() 
{
	list<int> lt1;
	lt1.push_back(1);
	lt1.push_back(2);
	lt1.push_back(3);
	lt1.push_back(4);
	lt1.push_back(5);
	list<int> lt2;
	lt2.push_back(10);
	lt2.push_back(20);
	lt2.push_back(30);
	lt2.push_back(40);
	//赋值
	lt1 = lt2;
	//只要一个容器支持迭代器就可以支持范围for的操作
	for (auto e : lt1) 
	{
		cout << e << " ";
	}
	cout << endl;
}
int main() 
{
	test4();
	return 0;
}

4,insert、erase 任意位置的插入、删除

list不支持随机访问,vector支持,list空间都不连续,加的不是你想要的位置。

list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(5);
lt.push_back(6);
//4前插入 30
lt3.insert(lt3.begin() + 3, 30);  //这样是错误的写法

怎么写呢,需要用到算法里面的 find 

insert 任意位置的插入

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include <list>
#include <algorithm>
void test5()
{
	list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	lt.push_back(5);
	lt.push_back(6);
	//4 前插入 30
	//lt3.insert(lt3.begin() + 3, 30); //错误写法

	list<int>::iterator pos = find(lt.begin(), lt.end(), 4);
	//这里必须做一个判断,因为如果没有找到就不会去调用insert
	// 如果不做判断返回的是end的位置,由于没有找到值为 4 的元素,
	// pos会指向end(),直接插入就会在末尾插入新元素。
	if(pos != lt.end())  
	{
		lt.insert(pos, 30);
	}
	//打印
	for (auto e : lt) 
	{
		cout << e << " ";
	}
	cout << endl;
}

int main() 
{
	test5();
	return 0;
}

erase 任意位置的删除

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include <list>
#include <algorithm>
void test5()
{
	list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	lt.push_back(5);
	lt.push_back(6);
	list<int>::iterator pos = find(lt.begin(), lt.end(), 4);
	if(pos != lt.end())  
	{
		//为什么重新获取pos
		//因为迭代器已经失效了,erase会返回删除元素的下一个位置,所以需要重新获取
		pos = lt.erase(pos); 
	}
	//打印
	for (auto e : lt) 
	{
		cout << e << " ";
	}
	cout << endl;
}

int main() 
{
	test5();
	return 0;
}

5,迭代器失效(★)

情景一:

为什么 vector 报错了,而 list 不报错呢?

迭代器我们都想象成指针一样的东西。

在vector中,进行插入操作的时候,pos迭代器已经不是 3下标了,或者扩容,迭代器还指向旧地址空间,指针直接失效了。

在list中,insert 不会失效,因为这个链表,pos迭代器指向不受增容的影响。

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include <list>
#include <vector>
#include <algorithm>
void test6()
{
	list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	lt.push_back(5);
	lt.push_back(6);
	list<int>::iterator pos = find(lt.begin(), lt.end(), 4);
	if (pos != lt.end())
	{
		lt.insert(pos, 30); // 插入以后pos迭代器失效了吗?
		lt.erase(pos); //删除 4
	}
	//打印
	for (auto e : lt) 
	{
		cout << e << " ";
	}
	cout << endl;
}
void test7()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);
	v.push_back(6);
	vector<int>::iterator pos = find(v.begin(), v.end(), 4);
	if (pos != v.end())
	{
		v.insert(pos, 30); // 插入以后pos迭代器失效了吗?
		v.erase(pos); //删除 4
	}	
	//打印
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}
int main() 
{
	test6();
	//test7();
	return 0;
}

图解如下: vector 和 list 的对比

如何解决:

 重新获取迭代器 pos

情景二:

删除所有的偶数,当 erase 之后,it 指向释放的空间,对野指针进行++或者解引用操作会导致程序崩溃。

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include <list>
#include <vector>
#include <algorithm>
void test8() 
{
	list<int> lt;
	lt.push_back(3);
	lt.push_back(2);
	lt.push_back(1);
	lt.push_back(5);
	lt.push_back(4);
	lt.push_back(6);
	list<int>::iterator it = lt.begin();
	while (it != lt.end())
	{
		if (*it % 2 == 0)
		{
			lt.erase(it); // it 变成了野指针,
			cout << *it << endl;  //失效了以后也不能进行解引用操作
		}
		++it;  //对野指针进行++
	}
}
int main() 
{
	test8();
	return 0;
}

如何解决:  

代码中,已经修复了迭代器失效问题,为什么不能直接输出呢

  1. 迭代器指向的不确定性:虽然erase返回了下一个有效的迭代器,但我们不能确定这个迭代器指向的元素一定是可以安全输出的。例如,如果在删除一个偶数元素后,下一个元素仍然是偶数,并且也满足删除条件,那么这个新的迭代器实际上指向的是即将被删除的元素。在这种情况下输出该元素的值是不安全的,属于未定义行为。
  2. 可能的迭代器失效场景:即使在当前情况下看起来似乎没有问题,但在更复杂的场景中,比如容器中存在其他并发操作或者其他条件可能导致迭代器在输出之前就已经失效。
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include <list>
#include <vector>
#include <algorithm>
void test8() 
{
	list<int> lt;
	lt.push_back(3);
	lt.push_back(2);
	lt.push_back(1);
	lt.push_back(5);
	lt.push_back(4);
	lt.push_back(6);
	list<int>::iterator it = lt.begin();
	while (it != lt.end())
	{
		if (*it % 2 == 0)
		{
			it = lt.erase(it); // erase会返回下一个位置
            //cout<<*it <<endl;  这里不能直接输出
		}
		else
		{
			++it;
		}
	}
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;
}
int main() 
{
	test8();
	return 0;
}

小结:

对于 vector 而言,insert erase 都会失效。

对于 list 而言,insert不会失效,erase 才会失效,erase之后空间被释放了指针还指向这块空间。失效了是指不能去取访问它,去访问失效迭代器才会有问题。


list的模拟实现

源码的引入

这段源码是主干部分,我们看完该源码是如何搭建的主干部分的,接下来我们就实现一个类似的。

其中有个注意的地方:

self& operator++(){
node =(link_type)((*node).next);  //node是void*类型,转成__list_node* 类型
return *this;
}

1,对node进行解引用拿到该对象(节点/结构体),对该对象进行 . 操作拿到里面的数据或者指针,然后再强转成 __list_node* 类型。

即:(*node).data,, node是节点的指针,解引用返回节点,.data 取节点里面的值

2, (link_type)((*node).next),需要得到下一个节点的指针也就是节点指向下一个位置。拿到该节点,返回类型是void*,  强转成节点类型 __list_node<T>*

list 中,原生指针的行为已经不是我们想要的了,我们可以用自定义类型去封装原生指针,通过运算符重载来构建我们想要的行为。

其它细节在接下来的实现中会一一叙述。


template<class T>
struct __list_node {
	typedef void* void_pointer;
	void_pointer next;
	void_pointer prev;
	T data;
};

template<class T, class Ref, class Ptr>
struct __list_iterator {
typedef  __list_iterator<T, T&, T*>  iterator;
typedef __list_iterator<T, const T&, const T*>  const_iterator;
typedef __list_iterator <T, Ref, Ptr> self;

typedef __list_node<T>* link_type;
link_type node;
__list_iterator(link_type x): node(x){}


bool operator==(const self&x)const{return node==x.node;}
bool operator!=(const self& x)const {return node!= x.node;}

reference operator*()const {return(*node).data;}
self& operator++(){
node =(link_type)((*node).next);
return *this;
}


self operator++(int) {
	self tmp = *this;
	++*this;
	return tmp;
}

self& operator--()
{
	node=(link type)((*node).prev);
	return *this;
}
self operator--(int){
	self tmp *this;
	--*this:
	return tmp;
}
};

template<class T,class Alloc=alloc>
class list
{
protected:
	typedef void* void_pointer;
	typedef __list_node<T> list_node;
	typedef list_node* link_type;
public:
	typedef T value_type;
public:
	typedef list_iterator<T, T&, T*>   iterator;
protected:
	link_type node;
}


1.基本结构的搭建

__list_node

源码是定义了void*类型,需要强转,这里我们直接这样定义,我们不需要强转类型。

struct __list_node  //对象<类型>
{
	__list_node<T>* _next;
	__list_node<T>* _prev;
	T _data;
};

 list

我们发现,在 list 初始化的时候,当我们 new 一个节点的时候需要进行初始化该节点,所以__list_node,需要进行初始化,即实现构造函数

	template<class T>
	class list
	{
		typedef __list_node<T> Node;   //给节点取别名为 Node
	public:
		//带头双向循环链表
		list()
		{
			_head = new Node;  //除了开空间,还会去调用Node的构造函数完成初始化
			_head->_next = _head;
			_head->_prev = _head;
		}
	private:
		Node* _head;  //一个指针用来初始化虚拟头结点
};

__list_node 的完善

template<class T>
struct __list_node  //对象<类型>
{
	__list_node<T>* _next;
	__list_node<T>* _prev;
	T _data;
	//Node类型的构造函数完成初始化
	__list_node(const T& x = T()) //初始化头结点可以给值,也可以给缺省
		:_data(x)
		, _next(nullptr)
		, _prev(nullptr)
	{}
};

push_back的实现

这是我们在数据结构链表所学的,一摸一样的,有节点和没有节点的情况下都是满足的。

	template<class T>
	class list
	{
		typedef __list_node<T> Node;
	public:
		//带头双向循环链表
		list()
		{
			_head = new Node;  //除了开空间,还会去调用Node的构造函数完成初始化
			_head->_next = _head;
			_head->_prev = _head;
		}
		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;
};

有了这些,我们需要打印链表插入的数据,需要用到迭代器。

__list_itertaor

template<class T>
struct __list_iterator
{
	typedef __list_node<T> Node;
	Node* _node; //核心还是一个指针
	__list_iterator(Node* node)   //构造函数用于初始化
		:_node(node)
	{}
	// *it
	T& operator*()
	{
		return _node->_data;
	}
    __list_iterator<T>& operator++()   //返回值还是一个迭代器
    {
	    _node = _node->_next;
	    return *this;
    }
    // it != end()
    bool operator!=(const __list_iterator<T>& it)
    {
	    return _node != it._node;
    }
};

有了迭代器,我们需要在 list 中使用 iterator,一个取begin(), 一个取end()

	template<class T>
	class list
	{
		typedef __list_node<T> Node;
	public:
		typedef __list_iterator<T> iterator;
		iterator begin()
		{
			return iterator(_head->_next); // __list_iterator(_head->_next)
		}
		iterator end()
		{
			return iterator(_head);
		}
		//带头双向循环链表
		list()
		{
			_head = new Node;  //除了开空间,还会去调用Node的构造函数完成初始化
			_head->_next = _head;
			_head->_prev = _head;
		}
		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;
};

基本结构的整体代码

list.h

#pragma once
namespace my_list 
{
	template<class T>
	struct __list_node  //对象<类型>
	{
		__list_node<T>* _next;
		__list_node<T>* _prev;
		T _data;
		//Node类型的构造函数完成初始化
		__list_node(const T& x = T()) //初始化头结点可以给值,也可以给缺省
			:_data(x)
			, _next(nullptr)
			, _prev(nullptr)
		{}
	};
	template<class T>
	struct __list_iterator
	{
		typedef __list_node<T> Node;
		Node* _node; //核心还是一个指针
		__list_iterator(Node* node)   //构造函数用于初始化
			:_node(node)
		{}
		// *it
		T& operator*()
		{
			return _node->_data;
		}
		__list_iterator<T>& operator++()   //返回值还是一个迭代器
		{
			_node = _node->_next;
			return *this;
		}
		// it != end()
		bool operator!=(const __list_iterator<T>& it)
		{
			return _node != it._node;
		}
	};
	template<class T>
	class list
	{
		typedef __list_node<T> Node;
	public:
		typedef __list_iterator<T> iterator;
		iterator begin()
		{
			return iterator(_head->_next); // __list_iterator(_head->_next),类似于vector(1);
		}
		iterator end()
		{
			return iterator(_head);
		}
		//带头双向循环链表
		list()
		{
			_head = new Node;  //除了开空间,还会去调用Node的构造函数完成初始化
			_head->_next = _head;
			_head->_prev = _head;
		}
		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;
	};
	void test_list()
	{
		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;
	}
}

test.cpp

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include "list.h"
int main() 
{
	my_list::test_list();
	return 0;
}

迭代器是如何执行起来的

2.迭代器的进一步理解

源码中的 typedef __list_iterator<T, T&, T*> iterator;

iterator就是__list_iterator<T, T&, T*> ,因为 list 迭代器不像 vector 空间是连续的,所以不能构建指针,只能去自定义一个类去实现该迭代器。

int* p1 = new int;    *p1 拿到了 int

Date* p2 = new Date;  *p2;拿到这个对象,(*p2)._year或者 p2->_year,拿到里面的数据。

//定义一个简单的日期类
struct Date
{
	int _year = 0;
	int _month = 1;
	int _day = 1;
};
void test_list()
{
	list<Date> lt;
	lt.push_back(Date());
	lt.push_back(Date());
	list<Date>::iterator it = lt.begin();
	while (it != lt.end())
	{    //Date是别人写的,就没写输出,如何应对?
		//cout << *it << " "; //解引用出来是Date自定义类型,不支持输出,没有重载operator输出
		cout << it->_year << "-" << it->_month << "-" << it->_day << endl; //我们没有重载operator->,所以需要去实现operator->
		++it;
	}
	cout << endl;
}

还可以这样输出,先拿到 it 指向的对象 Date,再去访问该对象里面的数据

cout << (*it)._year << "-" << (*it)._month << "-" << (*it)._day << endl;

3.const 迭代器的实现

以下代码在print_list中,lt 是 const,调用begin的时候,只能调 const 的 begin,所以给begin 加 const。

下面的代码看似没有问题,实际呢,遍历这个迭代器的时候还把我改了,这个迭代器不合理啊

const对象去调用const 返回的是普通迭代器,普通迭代器是可读可写的。

void print_list(const list<int>& lt)
{
	list<int>::iterator it = lt.begin(); //const &lt 传给 list* this 加const=> const list * this
	while (it != lt.end())
	{
		*it = 1;
		cout << *it << " ";
		++it;
	}
	cout << endl;
}
void test_list()
{
	list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	print_list(lt);
}

const迭代器的实现

const迭代器的唯一需求只有一个,可以遍历,可以读,可以加加,但是不能改写迭代器指向的数据/内容,不是迭代器本身不能修改,而是迭代器指向的内容不能修改。

迭代器指向的内容是通过 * -> 来修改的.

*it it->怎么实现呢??

把整个迭代器类拷贝出来,改两个地方,* -> 的返回类型变成const T& const T*

把迭代器拷贝一份, 把类名改成__list_ const _iterator,并给operator* operator->,加上const

但是仔细想想,当我们拷贝完之后就改两个地方,其它都一样,这样整体代码重复率就很高,代码显得很挫啊。所以我们别这样搞,我们这里可以增加两个模板参数来处理这种问题。

把迭代器的模版参数修改成:template<class T, class Ref, class Ptr> 因为迭代器只需修改两个地方,我们就增加两个参数,一个是接收引用,一个是接收指针。

这模板参数部分开始第一次接触看来很复杂,其实很简单,就是对着图里的这个步骤走下,就很好理解了。

#pragma once
namespace my_list 
{
	template<class T>
	struct __list_node  //对象<类型>
	{
		__list_node<T>* _next;
		__list_node<T>* _prev;
		T _data;
		//Node类型的构造函数完成初始化
		__list_node(const T& x = T()) //初始化头结点可以给值,也可以给缺省
			:_data(x)
			, _next(nullptr)
			, _prev(nullptr)
		{}
	};
	template<class T, class Ref, class Ptr>
	struct __list_iterator
	{
		typedef __list_node<T> Node;
		Node* _node; //核心还是一个指针
		__list_iterator(Node* node)   //构造函数用于初始化
			:_node(node)
		{}
		// *it
		Ref operator*()
		{
			return _node->_data;
		}
		//operator->
		Ptr operator->()
		{
			return &_node->_data;
		}
		__list_iterator<T, Ref, Ptr>&operator++()   //返回值还是一个迭代器,里面模板参数也变成了三个
		{
			_node = _node->_next;
			return *this;
		}
		// it != end()
		bool operator!=(const __list_iterator<T, Ref, Ptr>& 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;
		iterator begin()
		{
			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);
		}
		//带头双向循环链表
		list()
		{
			_head = new Node;  //除了开空间,还会去调用Node的构造函数完成初始化
			_head->_next = _head;
			_head->_prev = _head;
		}
		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;
	};
	void print_list(const list<int>& lt)
	{
		list<int>::const_iterator it = lt.begin(); //const &lt 传给 list* this 加const=> const list * this
		while (it != lt.end())
		{
			//*it = 1;  现在就不能修改了
			cout << *it << " ";
			++it;
		}
		cout << endl;
	}
	void test_list()
	{
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);
		print_list(lt);
	}
}

 补充一点:

类型决定了对空间的解释权

假设有 Node* cur,  iterator it

当他们都指向同一个节点,那么在物理内存中他们都存的是这个节点地址,物理上是一样的
但是他们的类型不一样,他们的意义就不一样。
比如*cur是一个指针的解引用,取到的值是节点。
比如 *it 是去调用迭代器的operator*,返回值是节点中存的值。


4.insert erase 的实现

接下来的的实现都很简单,因为都是数据结构所学的。

insert的实现

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

erase的实现

void erase(iterator pos) 
{
	assert(pos != end());
	Node* cur = pos._node;
	Node* prev = cur->_prev;
	Node* next = cur->_next;
	delete cur;
	prev->_next = next;
	next->_prev = prev;
}

5. pop_back、push_front、pop_front 的实现

复用上面的代码

	void pop_back() 
	{
		//erase(iterator(_head->_prev));
		erase(--end());
	}
	void push_front(const T& x) 
	{
		insert(begin(), x);
	}
	void pop_front() 
	{
		erase(begin());
	}

6.拷贝构造

拷贝构造也是构造函数的一种,用来初始化,需要先创建一个新的头节点,然后把数据全尾插到该对象中,在list中可直接使用迭代器,在里面可以直接使用, 不需要指定哪个类域和加类域符号,即 list<int>:: 。

	//拷贝构造
	list(const list<T>& lt) 
	{
		_head = new Node;
		_head->_next = _head;
		_head->_prev = _head;
		//const_iterator it = lt.begin();
		//while (it != lt.end())
		//{
		//	//this->push_back(*it);
		//	push_back(*it);
		//	++it;
		//}
		for (auto e : lt) 
		{
			push_back(e);
		}
	}

7.析构函数

clear 用来清除所有的数据,释放所有的节点,但是不处理头节点。

		void clear() 
		{
			iterator it = begin();
			while (it != end()) 
			{
				erase(it++);
			}
		}
		~list() 
		{
			clear();
			delete _head;
			_head = nullptr;
		}

8.operator= 赋值

传统写法中,先清空所有的数据,然后把赋值过来的数据全部尾插到该对象中。

现代写法中,传参的时候就,直接拷贝构造一份一摸一样的链表,拷贝构造是深拷贝(已经实现了),然后把该对象和这个拷贝出来的两者头结点一交换,就达到了目的。

//operator= 赋值
传统写法
//list<T>& operator=(const list<T>& lt) 
//{
//	if (this != &lt) 
//	{
//		clear(); //清空所以的数据
//		for (auto e : lt)
//		{
//			push_back(e);
//		}
//	}
//	return *this;
//}
//赋值-现代写法
list<T>& operator=(list<T> lt)
{
	//clear();  不加也可以
	swap(_head, lt._head);
	return *this;
}

9.深浅拷贝问题

和vector 深浅拷贝问题都是一样的,我们需要手动实现,完成深拷贝。operator= 也是同理。


10.整体代码

list.h

#pragma once
#include <assert.h>
namespace my_list 
{
	template<class T>
	struct __list_node  //对象<类型>
	{
		__list_node<T>* _next;
		__list_node<T>* _prev;
		T _data;
		//Node类型的构造函数完成初始化
		__list_node(const T& x = T()) //初始化头结点可以给值,也可以给缺省
			:_data(x)
			,_next(nullptr)
			,_prev(nullptr)
		{}
	};
	//迭代器
	// __list_iterator<T, T&, T*> --> iterator;
	// __list_iterator<T, const T&, const T*>  ---> const_iterator;
	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)
		{}
		// *it
		Ref operator*()
		{
			return _node->_data;
		}
		//operator->
		Ptr operator->()
		{
			return &_node->_data;
		}
		//++it
		Self& operator++()
		{
			_node = _node->_next;
			return *this;
		}
		//it++ 后置
		Self operator++(int)
		{
			Self tmp(*this);
			//_node = _node->_next;
			++(*this);
			return tmp;
		}
		//--it
		Self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}
		Self operator--(int)
		{
			Self tmp(*this);
			//_node = _node->_prev;
			--(*this);
			return *this;
		}
		// it != end()
		bool operator!=(const Self& it)
		{
			return _node != it._node;
		}
		// it ==
		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;
		//const 迭代器
		typedef __list_iterator<T, const T&, const T*> const_iterator;
		iterator begin() 
		{
			return iterator(_head->_next); // __list_iterator(_head->_next),类似于vector(1);
		}
		iterator end() 
		{
			return iterator(_head);
		}
		const_iterator begin() const 
		{
			return const_iterator(_head->_next);
		}
		const_iterator end() const
		{
			return const_iterator(_head);
		}
		//带头双向循环链表
		list() 
		{
			_head = new Node;  //除了开空间,还会去调用Node的构造函数完成初始化
			_head->_next = _head;
			_head->_prev = _head;
		}
		//拷贝构造
		list(const list<T>& lt) 
		{
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;
			//const_iterator it = lt.begin();
			//while (it != lt.end())
			//{
			//	//this->push_back(*it);
			//	push_back(*it);
			//	++it;
			//}
			for (auto e : lt) 
			{
				push_back(e);
			}
		}
		//operator= 赋值
		//list<T>& operator=(const list<T>& lt) 
		//{
		//	if (this != &lt) 
		//	{
		//		clear(); //清空所以的数据
		//		for (auto e : lt)
		//		{
		//			push_back(e);
		//		}
		//	}
		//	return *this;
		//}
		//赋值-现代写法
		list<T>& operator=(list<T> lt)
		{
			//clear();  不加也可以
			swap(_head, lt._head);
			return *this;
		}
		//析构函数
		void clear() 
		{
			iterator it = begin();
			while (it != end()) 
			{
				erase(it++);
			}
		}
		~list() 
		{
			clear();
			delete _head;
			_head = nullptr;
		}
		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 pop_back() 
		{
			//erase(iterator(_head->_prev));
			erase(--end());
		}
		void push_front(const T& x) 
		{
			insert(begin(), x);
		}
		void pop_front() 
		{
			erase(begin());
		}
		void insert(iterator pos, const T& x) 
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(x);
			// prev newnode cur
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;
		}
		void erase(iterator pos) 
		{
			assert(pos != end());
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* next = cur->_next;
			delete cur;
			prev->_next = next;
			next->_prev = prev;
		}
	private:
		Node* _head; //虚拟头结点dummyHead
	};

	void test_list1() 
	{
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);
		list<int>::iterator it = lt.begin();
		while (it != lt.end())    //调用的是 operator!=()
		{
			cout << *it << "->";
			++it;
		}
		cout << "nullptr" << endl;
	}

	//定义一个日期类
	struct Date
	{
		int _year = 0;
		int _month = 1;
		int _day = 1;
	};
	void test_list2() 
	{
		list<Date> lt;
		lt.push_back(Date());
		lt.push_back(Date());
		list<Date>::iterator it = lt.begin();
		while (it != lt.end())
		{    //Date是别人写的,就没写输出,如何应对?
			//cout << *it << " "; //解引用出来是Date自定义类型,不支持输出,没有重载operator输出
			//(*it)._year 这样也可以
			cout << it->_year << "-" << it->_month << "-" << it->_day << endl;
			cout << (*it)._year << "-" << (*it)._month << "-" << (*it)._day << endl;
			++it;              
		}
		cout << endl;
	}

	void print_list(const list<int>& lt) 
	{
		list<int>::const_iterator it = lt.begin(); //const &lt 传给 list* this 加const-> const list * this
		while (it != lt.end()) 
		{
			//*it = 1;
			cout << *it << " ";
			++it;
		}
		cout << endl;
	}
	void test_list3() 
	{
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);
		lt.push_back(5);
		lt.push_back(6);
		lt.pop_back();
		lt.pop_front();
		print_list(lt);
	}
	//拷贝构造
	void test_list4() 
	{
		list<int> lt1;
		lt1.push_back(1);
		lt1.push_back(2);
		lt1.push_back(3);
		lt1.push_back(4);
		list<int> lt2(lt1);
		print_list(lt2);
	}
}

test.cpp

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include "list.h"
int main() 
{
	/*my_list::test_list1();
	my_list::test_list2();*/
	//my_list::test_list3();
	my_list::test_list4();
	return 0;
}

常见面试题

1,vector list的区别?

vector是一个可动态增长的数组

优点:随机访问 operator[] ->很好的支持排序、二分查找、堆算法的等等

缺点:头部或者中间的插入删除效率低 +空间不够了以后增容的代价较大

list是一个带头双向循环的链表

优点:任意位置插入删除数据效率高,0(1)

缺点:不支持随机访问

总结:vectorlist是两个相辅相成,互补的容器

2,vector list 底层是如何实现的

vector 可随机访问,当vector插入数据的时候,需要挪动数据,空间不够的时候,开新的空间,拷贝数据,释放旧空间,这个过程代价有点大,list 不支持随机访问,需要遍历整个链表找到某个值。节点的内存不是连续分配的,是随机分配的,链表插入数据,不需要挪动数据,不需进行大量的内存赋值和重新分配操作,每个节点需要单独分配空间,节点被插入或者删除的时候,效率较高 O(1)

3,vector 是如何增容的?

容量不够的时候,开新空间,拷贝数据,释放旧空间。

4,什么是迭代器失效?

迭代器失效就是在对容器进行特定操作(如插入、删除元素等)后,原来能用的迭代器可能就不能用了。

vector中的迭代器,当vector需要增容的时候,开了新空间,迭代器还指向旧空间,迭代器便失效了

list 中进行了erase操作,去使用该迭代器的时候,此时,操作该迭代器就相对于操作野指针,因为该空间已经释放了。

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

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

相关文章

Datawhale x李宏毅苹果书AI夏令营深度学习详解进阶Task03

在深度学习中&#xff0c;批量归一化&#xff08;Batch Normalization&#xff0c;BN&#xff09;技术是一种重要的优化方法&#xff0c;它可以有效地改善模型的训练效果。本文将详细讨论批量归一化的原理、实现方式、在神经网络中的应用&#xff0c;以及如何选择合适的损失函数…

淘宝商品评论API:获取商品使用场景与评价

淘宝的商品评论API&#xff08;通常通过淘宝开放平台提供&#xff09;允许开发者获取商品的评论信息&#xff0c;包括评价内容、评分、图片等。然而&#xff0c;直接获取特定商品的使用场景&#xff08;即用户如何使用商品的具体描述&#xff09;可能不是所有API都直接提供的&a…

【mysql】mysql目录结构和源码和mysql基础练习

mysql目录结构和源码的说明&#xff1a; 也就是之前说四个位置有提到的两个位置&#xff0c; 1软件安装位置bin 把bin目录加入环境变量就可以直接在命令行调用&#xff0c; "***\MySQL\MySQL Installer for Windows\bin" 2还有一个数据库文件的安装位置 &#…

华为云征文|基于华为云Flexus X实例部署Redis及案例实践详解

目录 前言 一、华为云Flexus X实例购买 1.1 Flexus X实例购买 1.2 登录Flexus X实例 二、Flexus X实例安装宝塔面板 2.1 安装宝塔面板 2.2 开放宝塔面板端口 2.3 登录宝塔面板 三、华为云Flexus X实例部署Redis 3.1 宝塔面板安装Redis 3.2 Redis密码设置及第三方登录 3.3 开放R…

【分立元件】电阻的基础知识

电阻与电容、电感一样都是最基本的元器件,大量使用于各种电气或电子设备中。对从事电气工作的人而言或许过于普通,平时忽视了它,但如果没有电阻,电气或电子电路就无法建立。电阻就是如此重要的元器件。 电阻的原理 电阻的数值取决于电阻材料的电阻率及其截面积和长度。 …

Java 自定义注解实现统一日志记录功能【Java核心技术_二十七】

一、什么是 Java 注解&#xff1f; 想象一下&#xff0c;你正在写一本日记&#xff0c;你可以在某些页面上贴上彩色的便签&#xff0c;上面写着一些关键词或者提醒。在 Java 中&#xff0c;注解就像是这些便签&#xff0c;它可以贴在类、方法、变量等程序元素上&#xff0c;为…

Vue 3 中如何对接高德地图

1. 准备工作 1.1. 获取高德地图 API Key 首先&#xff0c;你需要在高德开放平台注册并获取一个 API Key。访问 高德开放平台 注册并申请。 1.2. 安装依赖 在 Vue 3 项目中使用高德地图&#xff0c;可以通过以下步骤安装依赖&#xff1a; npm install types/amap-js-api --…

5.注册中心的其他实现-Nacos

文章目录 1.Nacos简介2.Nacos的安装2.1Nacos Windows本地启动不成功&#xff1f;2.2Linux环境下载并启动 3. Nacos的使用4.Nacos的负载均衡5.Nacos 健康检查6.Nacos 环境隔离7.Nacos 配置中心7.1为什么需要配置中心7.2 Nacos配置中心使用7.3 Data id7.4Nacos 上Linux部署服务7.…

鹰眼应急实时三维重建装备,高质量交付中!

在《2024年地方应急管理科技信息化任务书》的指导和应急管理万亿国债支撑下&#xff0c;全国各地正积极推进应急管理现代化建设&#xff0c;全力提升“数据汇聚服务能力、监测预警能力、指挥调度能力、支撑保障能力”四项应急管理能力。 制定标准化流程规范&#xff0c;保障交…

计算机毕业设计选题推荐-茶园茶农文化交流平台-Java/Python项目实战

✨作者主页&#xff1a;IT毕设梦工厂✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Py…

分组密码的模式AES-CBC模式流程解析附:应用代码实现

CBC模式&#xff1a;Cipher Block Chaining mode(密码分组链接模式) CBC模式的加解密 CBC模式中&#xff0c;首先将明文分组与前一个密文分组进行XOR运算&#xff0c;然后再进行加密。密文分组像链条一样相互连接在一起。 CBC模式的加密流程图 CBC模式的解密流程图 将一个分…

使用 `wget` 和 `curl` 命令行工具的全面指南

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

3.3 语法规则

&#x1f393; 微机原理考点专栏&#xff08;通篇免费&#xff09; 欢迎来到我的微机原理专栏&#xff01;我将帮助你在最短时间内掌握微机原理的核心内容&#xff0c;为你的考研或期末考试保驾护航。 为什么选择我的视频&#xff1f; 全程考点讲解&#xff1a;每一节视频都…

Oracle RAC关于多节点访问同一个数据的过程

一、说明 Oracle RAC 存在多个计算节点&#xff0c;但是使用的共享存储。那么多个节点共同访问同一个资源&#xff0c;怎么保证一致性。 白文的逻辑理解简述&#xff1a; 用户1访问rac1 &#xff0c;通过rac1获取AA数据块后&#xff0c;会加上latch锁。用户2通过rac2访问AA数据…

union不能被初始化由于有 non-trivial构造函数

背景 最近遇到一个问题&#xff0c;当定义一个union的时候&#xff0c;发现初始化失败&#xff0c;具体如下&#xff1a; class NonTrivial { public:NonTrivial(const std::string& s) : data(s) {std::cout << "NonTrivial constructed with " <<…

嵌入式OTG硬件电路分析

大家好,今天主要给大家分享一下,如何使用OTG硬件检测电路,和之前的接口有什么区别。 1. OTG接口与转换器 OTG是"On The Go"的英文缩写,字面上可以理解为“安上即可用”。USB传输是主从结构,一切USB传输都有Host发起。比如在开发板上可以插入U盘,这时开发板作为…

计算机毕业设计选题推荐-店铺租赁平台-商铺租赁系统-Java/Python项目实战

✨作者主页&#xff1a;IT研究室✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Python…

【简历】25届武汉某二本JAVA简历:项目描述真是难为学生想这么偏

注&#xff1a;为保证用户信息安全&#xff0c;姓名和学校等信息已经进行同层次变更&#xff0c;内容部分细节也进行了部分隐藏 简历说明 这是一份 25 届武汉某二本学校的JAVA简历。校招第一要点是上来必须先确定自己的求职层次&#xff0c;大中小厂要求不一样。二本同学主体…

Day 10-11:函数、函数传参

基本用法以及传参 基本用法 调用 1.无返回值函数调用 #include<stdio.h> #pragma warning(disable:4996); void my_helloworld() {printf("hello world!"); } int main() {my_helloworld();return 0; } //输出结果&#xff1a;hello world&#xff01;2.实际…

Leetcode每日刷题之438.找到字符串中所有字符异位词

1.题目解析 本题的题目要求是给定一个长字符串s与一个较短字符串p&#xff0c;判断s中是否有一段与p长度相同的字符串是由p打乱而得来&#xff0c;即为异位词&#xff0c;然后返回其开始位置下标&#xff0c;遍历s字符串中所有满足条件字符串的起始位置下标 2.算法原理 我们将p…