【C++】list的使用和模拟

list是什么?

你可以把list理解为一个双向带头循环的链表,如果你对双向循环链表不太了解的话可以看我往期的博客【数据结构】双向带头循环链表
list是stl(标准模板库)里面的一个容器,也就是说它和vector一样都是模板,可以存储各种类型,我们只要在实例化的时候声明想要存储的类型,就可以的到相应类型的链表。
例:

list<int> i;//存储int类型的链表
list<double> d;//存储double类型的链表
list<string> str;//存储string(字符串)类型的链表

list的使用

当然list是不可以直接使用的,你需要包一个头文件

#include <list>

这样你才可以使用list这个容器。

关于list的使用我只介绍它的几个重要的接口,包括模拟实现也是。

list的构造函数

在这里插入图片描述
我们可以看到list一共有4个构造函数,我们先从第一个开始介绍。
在这里插入图片描述
第一个构造函数是一个默认构造函数,它里面的那个alloc不用去管它,这是内存池,现阶段你可以把它就是new开辟的空间。
了解了这个构造函数我们就可以进行以下操作

#include <iostream>
#include <list>
using namespace std;
int main()
{
	list<int> i;
	list<double> d;
	list<char> c;
	list<int>();
	return 0;
}

在这里插入图片描述
这个构造函数你可以指定要开辟几个节点,并且初始化成什么,当然这个val有缺省值,你也可以不传初始化参数,这是就回去调用这个链表存储类型的默认构造来进行初始化,这里有人可能会问那像内置类型怎么办,这里我们要引入一个新概念,有了模板之后,内置类型也进行了升级,内置类型也有了构造函数。如果你不相信,我可以用下面这串代码证明,虽然这个验证我已经在vector中演示过了,但是还是再验证一下吧。

#include <iostream>

using namespace std;

int main()
{
	int i(10);
	double d(1.11);
	char c('a');
	cout << i << endl;
	cout << d << endl;
	cout << c << endl;
	return 0;
}

运行结果如下:
在这里插入图片描述
该构造函数的使用如下:

#include <iostream>
#include <list>
using namespace std;
int main()
{
	list<int> v1(2, 1);
	list<int> v2(2);
	list<double> v3(2, 1.1);
	list<double> v4(2);
	return 0;
}

在这里插入图片描述
在这里插入图片描述
我们可以看到这个构造函数是一个函数模板,stl的容器都用一个共同工具,叫迭代器,这个构造函数就支持使用不同容器的迭代器来进行构造。
就比如下面这种情况:

#include <iostream>
#include <list>
#include <vector>
using namespace std;
int main()
{
	vector<int> v1(10, 20);
	list<int> lt1(v1.begin(), v1.end());
	list<int> lt2(lt1.begin(), lt1.end());
	return 0;
}

在这里插入图片描述
在这里插入图片描述
拷贝构造函数,用法如下:

#include <iostream>
#include <list>
using namespace std;
int main()
{
	list<int> lt1(2, 100);
	//以下两种初始化都是再调用拷贝构造
	list<int> lt2(lt1);
	list<int> lt3 = lt2;//不要有因为这里使用了=,就认为这里是赋值重载

	return 0;
}

list的迭代器

在这里插入图片描述
list的迭代器和vector的迭代器唯一的区别就是list的迭代器不支持[ ],但是list迭代器支持 -> ,而vector的迭代器支持 ->。除了以上的区别外,用法上是一样的,没有区别。

list的插入和删除

push_back:尾部插入元素
在这里插入图片描述

pop_back:删除尾部元素
在这里插入图片描述

push_front:头部插入元素
在这里插入图片描述
pop_front:删除头部元素
在这里插入图片描述

insert:在指定位置插入元素
在这里插入图片描述
insert有三种用法,上图中第一种就是在指定位置插入元素,第二种是在指定位置插入n个元素,第三种是在指定位置插入一个迭代器区间的数据,因为这是的insert为函数模板,所以这个迭代器区间的迭代器可以是其他容器的迭代器。
erase:删除指定位置元素
在这里插入图片描述
erase两种用法,上图中第一个就是指定位置删除,第二个是指定迭代器区间删除。

其他函数接口

empty:检测list是否为空链表,是返回true,不是返回false。

#include <iostream>
#include <list>
using namespace std;
int main()
{
	list<int> lt1;
	if (lt1.empty())
	{
		cout << "lt1为空链表" << endl;
	}
	list<int> lt2(10, 1);
	if (lt2.empty())
	{
		cout << "lt2为空链表" << endl;
	}
	return 0;
}

size:返回list中有效节点的个数

#include <iostream>
#include <list>
using namespace std;
int main()
{
	list<int> lt1;
	cout << lt1.size() << endl;
	list<int> lt2(10);
	cout << lt2.size() << endl;
	return 0;
}

front:返回list第一个节点中的值引用
back:返回list最后一个节点的值引用

#include <iostream>
#include <list>
using namespace std;
int main()
{
	list<int> lt(5, 1);
	cout << "front:" << lt.front() << ", back:" << lt.back() << endl;
	lt.front() = 100;
	lt.back() = 200;
	cout << "front:" << lt.front() << ", back:" << lt.back() << endl;
	return 0;
}

clear:清空list中的有效数据

#include <iostream>
#include <list>
using namespace std;
int main()
{
	list<int> lt(10, 1);
	cout << lt.size() << endl;
	lt.clear();
	cout << lt.size() << endl;
	return 0;
}

swap:交换两个list的有效数据

#include <iostream>
#include <list>
using namespace std;
int main()
{
	list<int> lt1(10, 1);
	list<int> lt2(5, 2);
	lt1.swap(lt2);
	return 0;
}

在这里插入图片描述

list迭代器失效问题

以前说过,可以把迭代器理解为行为类似于指针的东西,迭代器失效就是迭代器所指向的节点的无效,就是该节点被删除了。因为list的底层结构为带头双向循环链表,因此在list中进行插入时是不会导致list的迭代器失效的,只有在删除时才会失效,并且失效的只是指向被删除节点的迭代器,
其他迭代器不会受到影响。
像下面这种情况就会造成迭代器失效。

#include <iostream>
#include <list>
using namespace std;
int main()
{
    int a[] = { 1,2,3,4,5,6,7,8,9 };
    list<int> lt;
    for (auto e : a)
    {
        lt.push_back(e);
    }
    auto it = lt.begin();
    ++it;
    while (it != lt.end())
    {
        lt.erase(it);
    }

    it = lt.begin();
    while (it != lt.end())
    {
        cout << *it << endl;
        ++it;
    }

    return 0;
}

所以我们可以看到erase这个函数接口是会返回一个迭代器,这个迭代器代表被删除节点的下一个位置。

lt.erase(it);

因此我们只需要对这行进行一些小小的改动就可以防止迭代器失效的问题,改动如下:

it=lt.erase(it);

正确的代码

#include <iostream>
#include <list>
using namespace std;
int main()
{
    int a[] = { 1,2,3,4,5,6,7,8,9 };
    list<int> lt;
    for (auto e : a)
    {
        lt.push_back(e);
    }
    auto it = lt.begin();
    ++it;
    while (it != lt.end())
    {
        it=lt.erase(it);
    }

    it = lt.begin();
    while (it != lt.end())
    {
        cout << *it << endl;
        ++it;
    }

    return 0;
}

list的模拟实现

前置工作

我们实现的list会与库里面的list发生冲突,所以我们要搞一个命名空间,将我们实现的list放入这个命名空间里面。

namespace lzq
{
}

list是一个模板,我们想要什么类型的链表,编译器就回去帮我们实例化出对应的链表,我们先创建一个list的类模板。

namespace lzq
{
	template<class T>
	class list{
	}
}

然后我们在写一类模板,这个类是每个节点的类型。

namespace lzq
{
	template<class T>
	struct __list_node
	{
		T _val;
		__list_node<T>* _prev;
		__list_node<T>* _next;

		__list_node(const T& val = T())
			:_val(val)
			,_prev(nullptr)
			,_next(nullptr)
		{}
	};
	template<class T>
	class list{
	typedef __list_node<T> Node;
	public:
	private:
		Node* _head;
	}
}

由于list的底层是一个带头双向循环链表,所以我们要一些函数来生成头节点,这个函数要放入每个构造函数里面。

尾插

		void push_back(const T& val)
		{
			Node* newNode = new Node(val);
			Node* tail = _head->_prev;//最后一个节点
			//更新尾节点
			tail->_next = newNode;
			newNode->_prev = tail;

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

增加size函数

新增成员变量size_t _size = 0;
将该变量放入到push_back中,每次插入都++_size;
包括后面实现的头插和insert,只要执行插入动作就++_size;
只要是执行删除就 - -_size;

		void push_back(const T& val)
		{
			Node* newNode = new Node(val);
			Node* tail = _head->_prev;//最后一个节点
			//更新尾节点
			tail->_next = newNode;
			newNode->_prev = tail;

			_head->_prev = newNode;
			newNode->_next = _head;
			++_size;
		}

		size_t size() const
		{
			return _size;
		}

增加empty

只要_size是0就返回真,非0就返回假。

		bool empty() const
		{
			reutnr !size();
		}

list的迭代器

实现list的迭代器还是有一些难度的,list不像是vector和string,空间连续,list的迭代器在++或者–等操作时需要对这些操作符进行重载,所以我们要给list的迭代器单独搞一个类模板。

	template<class T>
	struct __list_iterator
	{
		typedef __list_node<T> Node;
		typedef __list_iterator<T> self;
		
		__list_iterator(Node* node)
			:_node(node)
		{}
		
		Node* _node = nullptr;
	};
		

对前置++和后置++的重载

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

		self operator++ (int)//后置++
		{
			Node* tmp = _node;
			_node = _node->_next;
			return tmp;
		}

对前置–和后置–的重载

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

		self operator-- (int)//后置--
		{
			Node* tmp = _node;
			_node = _node->_prev;
			return tmp;
		}

对==和!=的重载

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

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

对*进行重载

		T& operator* ()
		{
			return _node->_val;
		}

对->进行重载

		T* operator-> ()
		{
			return &(_node->_val);
		}

const迭代器

const迭代器我们不需要在重新搞一份,我们只需要在刚才完成的迭代器模板基础上修改以下就可以实现const迭代器。
我们对该类模板增加两个类型模板参数。

template<class T, class refrence, class pointer>

refrence代表T&
pointer代表T*
用这两个类型模板参数对重载的返回类型进行替换。
然后再list这个类模板里面对迭代器类型typedef出正常的迭代器和const迭代器

		typedef __list_iterator<T, T&, T*> iterator;
		typedef __list_iterator<T,const T&,const T*> const_iterator;

这样我们的const迭代器就出来了,下面只需要完成使用迭代器的接口,迭代器就完成了。

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

		iterator end()
		{
			return _head;
		}

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

		const_iterator end()const
		{
			return _head;
		}

最终的迭代器代码如下:

template<class T, class refrence, class pointer>
	struct __list_iterator
	{
		typedef __list_node<T> Node;
		typedef __list_iterator<T, refrence, pointer> self;

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

		self operator++ (int)//后置++
		{
			Node* tmp = _node;
			_node = _node->_next;
			return tmp;
		}

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

		}

		self operator-- (int)//后置--
		{
			Node* tmp = _node;
			_node = _node->_prev;
			return tmp;
		}

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

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

		refrence operator* ()
		{
			return _node->_val;
		}
		
		pointer operator-> ()
		{
			return &(_node->_val);
		}


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

		Node* _node = nullptr;
	};

反向迭代器的话这里先挖一个坑,以后会填。

尾删

		void pop_back()
		{
			assert(empty());//判断链表是否为空
			Node* del = _head->_prev;//获取尾节点
			Node* prev = del->_prev;
			delete del;//删除节点
			//更新尾节点
			_head->_prev = prev;
			prev->_next = _head;
			--_size;
		}

增加clear

clear直接复用尾插即可

		void clear()
		{
			while (!empty())
			{
				pop_back();
			}
		}

析构函数

这里直接复用clear将所有除头节点外所有节点全部释放,最后释放头节点即可。

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

增加front和back函数

front和back一定要注意返回的是引用,是可以对链表中的内容进行修改的。

		T& front()
		{
			assert(empty());
			return _head->_next->_val;
		}

		T& back()
		{
			assert(empty());
			return _head->_prev->_val;
		}

insert

这里的插入,插入的是pos位置的前面。

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

我们insert实现出来之后头插就不用再去写了直接复用insert就可以,既然头插可以,那么尾插也是可以的。
所以尾插和头插的代码如下:

		void push_back(const T& val)//尾插
		{
			insert(end(), val);
		}
		void push_front(const T& val)//头插
		{
			insert(begin(), val);
		}

erase

erase删除pos位置的节点后会返回pos位置的下一个位置,这是为了防止迭代器失效。

		iterator erase(iterator pos)
		{
			assert(!empty());//判断链表是否为空
			Node* del = pos._node;
			Node* prev = del->_prev;
			Node* next = del->_next;
			prev->_next = next;
			next->_prev = prev;
			delete del;
			--_size;
			return next;
		}

同样erase的出现头删和尾删都可对其进行复用,来完成相应的操作。
头删尾删复用后的程序如下:

		void pop_back()//尾删
		{
			erase(--end());
		}

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

增加swap

		void swap(list<T>& s)
		{
			std::swap(_head, s._head);
			std::swap(_size, s._size);
		}

增加构造函数

在这里插入图片描述

第一个构造函数

		list(size_t n, const T& val = T())
		{
			Get_head();
			while (n--)
			{
				push_back(val);
			}
		}

第二个构造函数

		template<class InputIterator>
		list(InputIterator first, InputIterator last)
		{
			Get_head();
			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}

这个构造函数的增加会出现一些问题,比如我们再使用第一个构造函数时,要构建一个int类型的链表,编译器会按照最匹配原则来调用第二个构造函数,所以我们还要再写一个第一个构造函数的重载来解决这个问题。
代码如下:

		list(int n, const int& val = 0)
		{
			Get_head();
			while (n--)
			{
				push_back(val);
			}
		}

第三个构造函数

我们这里可以使用范围for搭配尾插来进行拷贝构造。

		list(list<T>& x)
		{
			Get_head();
			for (auto e : x)
			{
				push_back(e);
			}
		}

赋值重载函数

这里直接复用swap函数就可以了,与tmp进行交换,当赋值重载函数执行完后,tmp生命周期也就结束了,会自动销毁,不需要担心内存泄露的问题。

		list<T> operator= (list<T> tmp)
		{
			swap(tmp);
			return *this;
		}

list模拟实现的程序

#pragma once
#include <assert.h>
namespace lzq
{
	template<class T>
	struct __list_node
	{
		T _val;
		__list_node<T>* _prev;
		__list_node<T>* _next;

		__list_node(const T& val = T())
			:_val(val)
			,_prev(nullptr)
			,_next(nullptr)
		{}
	};

	template<class T, class refrence, class pointer>
	struct __list_iterator
	{
		typedef __list_node<T> Node;
		typedef __list_iterator<T, refrence, pointer> self;

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

		self operator++ (int)//后置++
		{
			Node* tmp = _node;
			_node = _node->_next;
			return tmp;
		}

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

		}

		self operator-- (int)//后置--
		{
			Node* tmp = _node;
			_node = _node->_prev;
			return tmp;
		}

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

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

		refrence operator* ()
		{
			return _node->_val;
		}
		
		pointer operator-> ()
		{
			return &(_node->_val);
		}


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

		Node* _node = nullptr;
	};

	 


	template<class T>
	class list
	{
		typedef __list_node<T> Node;
		typedef __list_iterator<T, T&, T*> iterator;
		typedef __list_iterator<T,const T&,const T*> const_iterator;

		void Get_head()
		{
			_head = new Node();
			_head->_next = _head;
			_head->_prev = _head;
		}
	public:
		iterator begin()
		{
			return _head->_next;
		}

		iterator end()
		{
			return _head;
		}

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

		const_iterator end()const
		{
			return _head;
		}

		list()
		{
			Get_head();
		}


		list(size_t n, const T& val = T())
		{
			Get_head();
			while (n--)
			{
				push_back(val);
			}
		}

		template<class InputIterator>
		list(InputIterator first, InputIterator last)
		{
			Get_head();
			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}

		list(int n, const int& val = 0)
		{
			Get_head();
			while (n--)
			{
				push_back(val);
			}
		}

		list(list<T>& x)
		{
			Get_head();
			for (auto e : x)
			{
				push_back(e);
			}
		}

		void push_back(const T& val)
		{
			insert(end(), val);
		}

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

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

		list<T> operator= (list<T> tmp)
		{
			swap(tmp);
			return *this;
		}

		size_t size() const
		{
			return _size;
		}

		bool empty() const
		{
			return !size();
		}

		T& front()
		{
			assert(!empty());
			return _head->_next->_val;
		}

		T& back()
		{
			assert(!empty());
			return _head->_prev->_val;
		}

		void clear()
		{
			while (!empty())
			{
				pop_back();
			}
		}

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

		iterator erase(iterator pos)
		{
			assert(!empty());//判断链表是否为空
			Node* del = pos._node;
			Node* prev = del->_prev;
			Node* next = del->_next;
			prev->_next = next;
			next->_prev = prev;
			delete del;
			--_size;
			return next;
		}

		void swap(const list<T>& s)
		{
			std::swap(_head, s._head);
			std::swap(_size, s._size);
		}


		~list()
		{
			clear();
			delete _head;
			_head = nullptr;
		}
	private:
		Node* _head;
		size_t _size = 0;
	};
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
引用和提供了关于实现vector的两种方法。其中,引用展示了一个使用reserve和push_back方法的示例,而引用展示了一个使用new和memcpy函数的示例。这两种方法都是常见的实现vector的方式。 在第一种方法中,通过reserve函数可以预留足够的内存空间,然后使用push_back函数逐个将元素添加到vector中。这种方法的好处是可以避免不必要的内存重分配,提高了效率。 而第二种方法使用new操作符在堆上分配内存空间,并使用memcpy函数将已有的vector对象的数据复制到新的内存空间中。通过这种方式,可以实现深拷贝,即两个vector对象拥有独立的内存空间。这种方法的好处是可以在不修改原始vector对象的情况下创建一个新的vector对象。 除了以上两种方法,还可以使用其他方式实现vector类。例如,可以使用动态数组来实现vector的底层数据结构,然后通过成员函数实现vector的各种操作,如增加、删除、查找等。 总结来说,c语言模拟实现vector的关键是动态内存管理和对元素的增删改查操作。可以使用预留空间和逐个添加元素的方式,也可以使用动态数组和复制数据的方式来实现vector类。具体的实现方式可以根据需求和实际情况选择。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [C++——vector模拟实现](https://blog.csdn.net/weixin_49449676/article/details/126813526)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

罗!伯!特!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值