C++STL容器系列(三)list的详细用法和底层实现

目录

对2024年航空航天与力学国际学术会议(ICAM 2024) 感兴趣的伙伴可以点击【click】

一:介绍

  • list是一种序列式容器。该容器的底层是用双向链表实现的, 在链表的任一位置进行元素的插入、删除操作都是快速的。

二:list的创建和方法

首先,使用vector时需包含头文件:

  • #include <list>

创建list

list本质是类模板,可以存储任何类型的数据。数组在声明前需要加上数据类型,而list则通过模板参量设定类型。

比如,声明一个int型的list列表。

cpp 复制代码
list<A> listname;					// 一个空列表

list<A> listname(size);				// 开辟size个空间,值默认为0

list<A> listname(size, value);      // size个值为value的数组

list<A> listname(elselist);         //  拷贝构造参数是其他list

list<A> listname(first, last);      // 迭代器初始化

方法

✨iterators(迭代器) \colorbox{pink}{✨iterators(迭代器)} ✨iterators(迭代器)

名字 描述
begin 返回指向容器中第一个元素的迭代器。
end 返回指向容器最后一个元素所在位置后一个位置的迭代器
rbegin 返回容器逆序的第一个元素的迭代器
rend 返回容器逆序的最后一个元素的前一个位置的迭代器
cbegin 和begin()功能相同,在其基础上增加了 const 属性,不能用于修改元素。
cend 和end()功能相同,在其基础上增加了 const 属性,不能用于修改元素。
crbegin 和rbegin()功能相同,在其基础上增加了 const 属性,不能用于修改元素。
crend 和rend()功能相同,在其基础上增加了 const 属性,不能用于修改元素。

✨Capacity(容量) \colorbox{pink}{✨Capacity(容量)} ✨Capacity(容量)

名字 描述
size 返回实际元素的个数
empty 判断list是否为空,为空返回true否则false
max_size 返回元素个数的最大值。

✨Element access(元素访问) \colorbox{pink}{✨Element access(元素访问)} ✨Element access(元素访问)

名字 描述
front 返回第一个元素
back 返回最后一个元素

✨Modifiers(修改器) \colorbox{pink}{✨Modifiers(修改器)} ✨Modifiers(修改器)

名字 描述
push_back 在容器的尾部插入元素
pop_back 删除最后一个元素
push_front 在容器的头部插入元素
pop_front 删除第一个元素
erase 删除元素
clear 清除容器内容,size=0,存储空间不变
swap 交换两个元素的所有内容
assign 用新元素替换原有内容。
emplace_front 插入元素,和insert实现原理不同,速度更快
emplace_back 在容器的尾部插入元素,和push_back不同, 速度更快

三:list的具体用法

3.1 push_back、pop_back、push_front、pop_front

cpp 复制代码
list<int> lt1;
for (int i = 0; i < 5; i++)
{
    lt1.push_back(i);
    lt1.push_front(i);
}
for (int i = 0; i < 5; i++)
{
    lt1.pop_back();
    lt1.pop_front();
}
  • emplace_back的效果和push_back一样,具体可以查看前一篇vector的博客

3.2 insert() 和 erase()

  • insert():在指定位置插入一个或多个元素(三个重载)
cpp 复制代码
l1.insert(l1.begin(), 100);  在l1的开始位置插入100。

l1.insert(l1.begin(), 2, 200);  在l1的开始位置插入2个100。

l1.insert(l1.begin(), l2.begin(), l2.end()); 在l1的开始位置插入l2的从开始到结束的所有位置的元素。
  • erase():删除一个元素或一个区域的元素(两个重载)
cpp 复制代码
l1.erase(l1.begin());  	 将l1的第一个元素删除。

l1.erase(l1.begin(), l1.end());  将l1的从 begin() 到 end() 之间的元素删除。

3.3 splice 函数

  • splice函数是list中的一个转移函数 ,将另外一个list中的元素转移到本list中。共有3个重载:

1、list1.splice(position, list2): 将list2中的所有元素剪贴到list1的position位置;

2、list1.splice(position, list2, iter): 将list2中某个位置的迭代器iter指向的元素剪贴到list1中的position位置;

3、list1.splice(position, list2, iter1, iter2): 将list2中的某一段位置iter1 ~ iter2的元素剪贴到list1中的position位置

cpp 复制代码
void list_splice()
{
	list<int>mylist1, mylist2;

	for (int i = 1; i <= 4; i++) mylist1.push_back(i);
	for (int i = 1; i <= 4; i++) mylist2.push_back(i * 10);
	std::list<int>::iterator it1 = mylist1.begin();
	it1++;

	mylist1.splice(it1, mylist2);  // 转移mylist2到it1位置之前
	// 注意:此时链表2就为空链表了 所以splice名字叫做转移

	for (auto& e : mylist1)
	{
		cout << e << " ";
	}
}

// 输出:1 10 20 30 40 2 3 4

四:list容器底层实现

容器的底层是用双向链表实现的,甚至一些 STL 版本中(比如 SGI STL),list 容器的底层实现使用的是双向循环链表。

头指针,使用链表存储数据,并不会将它们存储到一整块连续的内存空间中。恰恰相反,各元素占用的存储空间(又称为节点)是独立的、分散的,它们之间的线性关系通过指针来维持。

4.1 list 容器节点结构

双向链表的各个节点中存储的不仅仅是元素的值,还应包含 2 个指针,分别指向前一个元素和后一个元素。

cpp 复制代码
template<class T>  
struct ListNode    // 当一个类不想要访问限定符限制的时候就用struct
{
	ListNode* _next;
	ListNode* _prev;

	T _data;

	ListNode(const T& data)
		:_next(nullptr)
		, _prev(nullptr)
		, _data(data)
	{}
};

注:为了方便阅读和理解,本文章所实现的list都省略了与本文核心内容不相关的内容,如果读者对此部分感兴趣,可查看 list 容器实现源码。

  • 本人的 gitee上具有本文实现的完整代码及测试代码,需要的可以自取【click】

5.2 list容器迭代器的底层实现

和 vector 这些容器迭代器的实现方式不同,由于 list 容器的元素并不是连续存储的,所以该容器迭代器中,必须包含一个可以指向 list 容器的指针,并且该指针还可以借助重载的 *、++、--、==、!= 等运算符,实现迭代器正确的递增、递减、取值等操作。

  • 所以我们就需要对迭代器进行封装!!

可以看到,迭代器的移动就是通过操作节点的指针实现的。

  • 注意:因为存在const迭代器 和 非const迭代器 所以这里我们的模版参数有三个一个为存储类型 一个为 引用返回类型 一个为 指针类型(const 和 非const)
cpp 复制代码
// 通过模板,给不同的模板参数,让编译器帮我们实例化两个类
template<class T, class Ref, class Ptr>
struct ListIterator
{
	typedef ListNode<T> Node;
	typedef ListIterator<T, Ref, Ptr> Self;

	Node* _node;

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

	Self& operator++()  // 迭代器++返回自己 所以typedef一下
	{
		_node = _node->_next;
		return *this;
	}

	Self operator++(int)   // 后置++ 不能用引用返回了 tmp会销毁
	{
		Self tmp(*this);   // 默认的拷贝构造
		_node = _node->_next;
		return tmp;
	}

	Self& operator--()
	{
		_node = _node->_prev;
		return *this;
	}

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

	Ptr operator->()
	{
		return &_node->_data;
	}

	Ref operator*()
	{
		return _node->_data;
	}

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

	bool operator==(const Self& it)
	{
		return _node == it._node;
	}
};
  • 接下来是reverse_iterator的实现 注意反向迭代器的实现可以对正向迭代器进行复用 具体代码如下:
cpp 复制代码
// 此处的参数就是正向迭代器了
template<class iterator, class Ref, class Ptr>
struct Reverse_ListIterator
{
	typedef Reverse_ListIterator<iterator, Ref, Ptr> Self;

	iterator _it;

	Reverse_ListIterator(iterator it)
		:_it(it)
	{}

	Ref operator*()
	{
		iterator tmp = _it;
		return tmp._node->_prev;
	}


	// 作用是为pos这种结构体服务的
	Ptr operator->()
	{
		return &_it._node->_prev->_data;
	}

	Self& operator++()
	{
		_it._node = _it._node->_prev;
		return *this;
	}

	Self operator++(int)
	{
		Self tmp(*this);
		_it._node = _it._node->_prev;
		return tmp;
	}

	Self& operator--()
	{
		_it._node = _it._node->_next;
		return *this;
	}

	Self operator--(int)
	{
		Self tmp(*this);
		_it._node = _it._node->_next;
		return tmp;
	}

	bool operator!=(const Self& it)
	{
		return _it._node != it._it._node;    // 复用的结果
	}

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

};

5.2 list容器的主体实现

  • 以下是主体实现
cpp 复制代码
template<class T>
class list
{
	typedef ListNode<T> Node;
	// 不符合迭代器的行为,无法遍历 无法进行++和*操作 所以我们要将迭代器封装成一个类进行运算符重载即可
	// typedef Node* iterator;
public:
	typedef ListIterator<T, T&, T*> iterator;   //规范迭代器
	typedef ListIterator<T, const T&, const T*> const_iterator;   // 通过模板,给不同的模板参数,让编译器帮我们实例化两个类
	// list的const迭代器是有东西的

	typedef Reverse_ListIterator<iterator, T&, T*> reverse_iterator;
	typedef Reverse_ListIterator<const_iterator, const T&, const T*> const_reverse_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);
	}

	reverse_iterator rbegin()
	{
		return reverse_iterator(end());    // 匿名对象
	}

	reverse_iterator rend()
	{
		return reverse_iterator(begin());
	}

	list()   // 初始化哨兵卫的头节点
	{
		_head = new Node(T());   // 没有合适的默认构造函数可用 这里用匿名对象初始化

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

	void empty_init()
	{
		_head = new Node(T());

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

	list(const list<T>& lt)
	{
		empty_init();

		for (const auto& e : lt)
		{
			push_back(e);
		}
	}

	// 这里的赋值重载是现代写法
	list<T>& operator=(list<T> lt)
	{
		swap(_head, lt._head);
		return *this;
	}

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

	void push_back(const T& x)
	{
		Node* newnode = new Node(x);    // 就不需要专门写个CreatNewNode的了   自动调用Node的构造函数

		/*newnode->_next = _head;
		newnode->_prev = _head->_prev;
		_head->_prev->_next = newnode;
		_head->_prev = newnode;*/

		insert(end(), x);
	}

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

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

		return iterator(node);
	}

	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);
	}

	void pop_back()
	{
		erase(--end());
	}

	void push_front(const T& x)
	{
		insert(begin(), x);
	}

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

	void clear()
	{
		list<T>::iterator it = begin();
		while (it != end())
		{
			it = erase(it);
		}
	}

private:
	Node* _head;
};

总结

list的详细用法和底层实现就介绍到这里了如果有任何疑问都可以私信我,希望我们共同进步, 有错误还请在评论区指正!

相关推荐
yuanbenshidiaos2 分钟前
C++----------函数的调用机制
java·c++·算法
唐叔在学习6 分钟前
【唐叔学算法】第21天:超越比较-计数排序、桶排序与基数排序的Java实践及性能剖析
数据结构·算法·排序算法
程序员_三木10 分钟前
Three.js入门-Raycaster鼠标拾取详解与应用
开发语言·javascript·计算机外设·webgl·three.js
是小崔啊20 分钟前
开源轮子 - EasyExcel01(核心api)
java·开发语言·开源·excel·阿里巴巴
ALISHENGYA26 分钟前
全国青少年信息学奥林匹克竞赛(信奥赛)备考实战之分支结构(switch语句)
数据结构·算法
tianmu_sama26 分钟前
[Effective C++]条款38-39 复合和private继承
开发语言·c++
黄公子学安全29 分钟前
Java的基础概念(一)
java·开发语言·python
liwulin050630 分钟前
【JAVA】Tesseract-OCR截图屏幕指定区域识别0.4.2
java·开发语言·ocr
jackiendsc34 分钟前
Java的垃圾回收机制介绍、工作原理、算法及分析调优
java·开发语言·算法
Oneforlove_twoforjob39 分钟前
【Java基础面试题027】Java的StringBuilder是怎么实现的?
java·开发语言