C++的STL---->list

本文深入解析了C++中的list容器,介绍了其O(1)插入删除原理,涉及构造函数、常用接口如push_back/push_front/insert/erase等的使用,以及迭代器的工作机制。通过模拟实现,展示了list的底层链表结构和迭代器设计。
摘要由CSDN通过智能技术生成


什么是list
话不多说,我们直接上list的官方源文档
在这里插入图片描述
list也是一个类模版。那么list和前面所学的vector还有string又有什么区别呢?我们进一步来看官方文档

Lists are sequence containers that allow constant time insert and erase operations anywhere within the sequence, and iteration in both directions.
这是截取自官方文档对于list的说明,用通俗一点的话语来描述就是list是一个在任意位置插入删除时间复杂度是O(1)的容器


从源代码来探索list的底层结构
那么list的底层到底是一个什么结构能够在任意位置插入删除达到O(1)的时间复杂度呢?我们结合SGI的的list的源代码来进行分析:

//节选并简化的关键代码
template<class T>
 struct _List_node_base
    {
      _List_node_base<T> * _M_next;
      _List_node_base<T> * _M_prev;
      T val;
    };  

list是由一个个的节点组成的,而这里面的每一个节点都是双向的!
我们继续向下查找源代码:
找到并简化源代码

template<class T>
class list
{  typedef _List_node_base<T>  Node;
  protected:
    Node* _head;
} 

到这里我们基本就可以确定,list是一个双向带头循环的链表了!这种链表能够做到任意位置插入删除的效率都是O(1).


list的常见构造和接口的使用
同样,我们先来看一看构造函数:

list的构造效果
list()构造一个空的list
list(size_t n ,const T& val)构造n个值是val的节点
list(InputIterator first, InputIterator last)迭代器区间构造
list(const list& x)拷贝构造

常用的接口:
在这里插入图片描述
对于C++来说,由于list在插入删除上面极度优秀的表现,所以说list的使用场景多用于频繁进行任意位置的插入删除,也就是说对于list来讲,增删的成员函数使用的很多,而其他的成员函数相对用的就会偏少
用到的时候,查文档了解即可。

#include<iostream>
#include<list>
using namespace std;
void test()
{ 
   list<int>lt;
	//push_back--->尾插
	lt.push_back(1);
	lt.push_back(2);
	for (auto c : lt)
	{
		cout << c << " ";
	}
	cout << endl;
	//push_front--->头插
	lt.push_front(0);
	lt.push_front(-1);
	for (auto c : lt)
	{
		cout << c << " ";
	}
	cout << endl;
	//insert--->依旧是只能提供迭代器作为位置
	auto it = lt.begin();
	lt.insert(++it, -5);
	for (auto c : lt)
	{
		cout << c << " ";
	}
	cout << endl;
	it++;
	//erase--->删除对应的迭代器指向的元素
	lt.erase(it);
	for (auto c : lt)
	{
		cout << c << " ";
	}
	cout << endl;
	//尾删
	lt.pop_back();
	//头删
	lt.pop_front();
	for (auto c : lt)
	{
		cout << c << " ";
	}
	cout << endl;
}
int main()
{  test();
   return 0;
}

在这里插入图片描述
list也提供了sort方法进行对元素进行排序,链表的sort是用归并进行的,不过链表的排序效率非常低,不太推荐使用list结构进行排序


list的迭代器
接下来我们来看一看list的迭代器。和前面的vector不一样,list的迭代器的底层不是一个原生的指针!而是一个更为复杂的自定义类型!二而这个迭代器只有++和–这两种迭代的行为,而没有vector的迭代器可以加减一个常数位置进行访问的功能!

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);
	//看一看list的迭代器
	auto it = lt.begin();
	while (it != lt.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
	//cout<<*(it+3)<<endl; list的迭代器没有随机访问对的功能
}
int main()
{   
	test2();
	return 0;
}

list和list的迭代器的模拟实现
讲了这么多的list的使用,接下来我们来模拟实现一个list来帮助我们更好了解list的底层结构。我们知道,list是一个双向带头循环链表,所以我们需要链表的节点,所以我们需要定义节点的结构

template<typename T>
	struct list_node
	{
		list_node<T>* _prev;
		list_node<T>* _next;
		T _val;
	//因为这里变成了模板类,所以这里全缺省的默认构造函数的缺省值要这么写	
		list_node(const T& val=T())
			:_prev(nullptr)
			,_next(nullptr)
			,_val(val)
		{}
	};

有了这么一个节点结构,我们就可以快速上手搭起来一个list类的框架了

//list类的框架
template<class T>
class list
{ public:
      typedef _list_node<T> Node;
      //提供默认构造函数
    	list()
			: _head(new Node)
		{
			_head->_next = _head;
			_head->_prev = _head;
		} 
  private:
   Node _head;
};

对于双向带头循环链表,我们只要实现insert和erase方法,然后其他的接口复用就可以了,这两个方法需要迭代器。所以我们再来实现list的迭代器。

//简单的list迭代器
 template<typename T>
	struct _List_iterator
	{
		typedef list_node<T> Node;
		typedef _List_iterator<T> Self;
		//迭代器本质是封装了节点的指针
		Node* _node;
		_List_iterator(Node* node)
			:_node(node)
		{}
		//*
		T& operator*()
		{
			return _node->_val;
		}
		//->
		T* operator->()
		{
			return &_node->_val;
		}
		//前置++
		Self& operator++()
		{
			_node = _node->_next;
			return *this;
		}
		//前置--
	     Self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}
		//后置++
		self operator++(int)
		{
			self tmp(*this);
			_node = _node->_next;
			return tmp;
		}
		//后置--
		self operator--(int)
		{
			self tmp(*this);
			_node = _node->_prev;
			return tmp;
		}
		//重载!=
		bool operator!=(const self& it) const
		{
			return _node != it._node;
		}
	};

首先这里的迭代器存在一个很严重的问题:如果是一个const对象无法调用这个迭代器!而你可能会想说在创造一个const版本的迭代器类,这样固然是可以,但是这样会使得代码冗余!我们来 看一看官方是怎么处理这个问题的

template<class T,class Ref,class Ptr>
struct __list_iterator
{  
///具体实际就不用关注了
};
//list的地方如何处理
template<class T>
class list
{
   public:
     typedef _list_iterator<T,T&,T*> iterator;
	typedef _list_iterator<T, const T&, const T*> const_iterator;
}

可以看到,STL里面利用了三个模板参数,T, Ref,Ptr。而Ref,Ptr,分别就是引用和指针。所以当我们传递的是非const的迭代器,编译器就会匹配非const的,反之const就会匹配const。这就是大佬设计的独到之处

//完整的模拟实现list的迭代器
template<typename T,typename Ref,typename Ptr>
	struct _List_iterator
	{
		typedef list_node<T> Node;
		typedef _List_iterator<T,Ref,Ptr> Self;
		Node* _node;
		_List_iterator(Node* node)
			:_node(node)
		{}
		//*
		Ref  operator*()
		{
			return _node->_val;
		}
		//->
		Ptr operator->()
		{
			return &_node->_val;
		}
		//前置++
		Self& operator++()
		{
			_node = _node->_next;
			return *this;
		}
		//前置--
	     Self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}
		//后置++
		Self operator++(int)
		{
			Self tmp(*this);
			_node = _node->_next;
			return tmp;
		}
		//后置--
		Self operator--(int)
		{
			Self tmp(*this);
			_node = _node->_prev;
			return tmp;
		}
		bool operator!=(const Self& it) const
		{
			return _node != it._node;
		}
	};

有了迭代器就可以实现insert和erase方法了

//插入pos位置前
		iterator insert(iterator pos, const T& val)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(val);
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;
			return iterator(newnode);
		}
		//删除pos位置
		iterator erase(iterator pos)
		{
			assert(pos != end());
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* next = cur->_next;
			prev->_next = next;
			next->_prev = prev;
			delete cur;
			return iterator(next);
		}

接下来,在实现list的begin()和end()成员

     iterator begin()
{    //哨兵位的下一个
	return iterator(_head->_next);
}
     iterator end()
{   
     //哨兵卫就是end
    return iterator(_head);
}

接下来就是复用insert和erase

   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的迭代器失效问题
对于insert方法,因为insert方法仅仅只是改变了指针的指向,所以本质pos指向的那个节点的绝对地址并不会随着insert而改变,所以insert不会导致迭代器失效。反而是erase方法反而因为释放了原来的空间导致出现野指针失效 而和vector的处理方式一致,erase方法也是返回指向被删除元素的下一个位置元素的迭代器

//删除pos位置
		iterator erase(iterator pos)
		{
			assert(pos != end());
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* next = cur->_next;
			prev->_next = next;
			next->_prev = prev;
			delete cur;
			//处理迭代器失效的问题
			return iterator(next);
		}
//插入pos位置前,即使返回值改成void也没问题
		iterator insert(iterator pos, const T& val)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(val);
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;
			return iterator(newnode);
		}		

接下来就是list的拷贝构造和赋值元素重载了,我们也是采取现代的写法进行处理。

//构造哨兵位头节点方便交换
void empty_init()
{
			_head = new Node;
			_head->_prev = _head;
			_head->_next = _head;
}
//迭代器区间构造
template<typename InputIterator>
list(InputIterator first, InputIterator last)
{
	empty_init();
	while (first != last)
   {
		push_back(*first);
		++first;
	}
}
void swap(list<T>& lt)
{
			std::swap(_head, lt._head);
}
//拷贝构造
list(const list<T>& lt)
{
	empty_init();
	list<T> tmp(lt.begin(), lt.end());
	//交换哨兵位头节点
	swap(tmp);	
}
//赋值运算符重载
list<T>& operator=(list<T> lt)
{   
	swap(lt);
	return *this;
}
//添加析构函数
void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				it = erase(it);
			 }
			_head->_prev = _head;
			_head->_next = _head;
		}
		~list()
		{
			clear();
			delete _head;
			_head = nullptr;
		}

以上就是和list有关的知识的讲解,如果有不足之处希望指出。希望大家可以一起进步。

STL是指标准模板库(Standard Template Library),它是C++语言的一部分,提供了一系列的模板类和函数,用于支持通用的数据结构和算法。STL的目标是提供高效、可重用和可扩展的组件,以便开发人员能够更轻松地编写高质量的代码。STL包含了许多常见的数据结构,如vector、list、set、map等,以及各种算法,比如排序、查找、遍历等。通过使用STL,开发人员可以更加高效地处理各种数据结构和算法的问题,提高代码的开发效率和质量。 在STL中,我们可以使用各种容器来存储和管理数据。例如,我们可以使用std::map来创建一个键值对的映射,其中每个键都有一个与之相关联的值。下面是一个示例代码,展示了如何创建和使用一个std::map对象: std::map<std::string, int> disMap() { std::map<std::string, int> tempMap{ {"C语言教程",10},{"STL教程",20} }; return tempMap; } std::map<std::string, int> newMap(disMap()); 在这个示例中,disMap()函数创建了一个临时的std::map对象,并初始化了其中的一些键值对。然后,使用移动构造函数将这个临时对象移动到了一个新的std::map对象newMap中。最终,我们可以通过newMap对象来访问和操作这些键值对。 综上所述,STLC++中的标准模板库,提供了一系列的模板类和函数,用于支持通用的数据结构和算法。STL的使用可以提高代码的开发效率和质量,并且通过各种容器和算法,可以方便地处理各种数据结构和算法的问题。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [C++ STL详解超全总结(快速入门STL)](https://blog.csdn.net/qq_50285142/article/details/114026148)[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: 50%"] - *2* *3* [【C++实验】阅读STL源码并分析](https://blog.csdn.net/qq_35760825/article/details/125311509)[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: 50%"] [ .reference_list ]
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值