C++:list(用法篇+模拟实现)

文章目录

  • 前言
  • 一、list 的用法
    • 1. list 简介
    • 2. 用法代码演示
      • 1)头/尾 插/删和迭代器遍历
      • 2)insert与erase
      • 3)排序sort相关
      • 4)其他相关
  • 二、list模拟实现
    • 1. 结点类模板list_node
    • 2. 定义迭代器
      • 1)为什么要专门封装一个迭代器?
      • 2)迭代器类的实现
        • (1)普通迭代器
          • 初始化
          • operator*与operator->
          • operator++
          • operator!=
        • (2)const迭代器——参数T以及为什么有三个参数?
        • (3)迭代器代码实现
    • 3. list类实现
      • 1)成员变量
      • 2)迭代器接口相关
      • 3)构造相关
      • 4)插入删除相关
        • (1)insert与erase及其迭代器失效问题
        • (2)头插/头删/尾插/尾删
  • 三、list与vector对比
  • 模拟实现list完整代码


前言

今天我们一起来看看C++中stl库里list是什么样子的,以及模拟实现list~

在这里插入图片描述


一、list 的用法

1. list 简介

在这里插入图片描述

第一个参数是模板类型,第二个参数是空间配置器我们先暂时不管他。

list的在stl中就是一个双向带头循环链表,它的使用比vector简单一些。

stl这些风格接口都是一样的,他的构造也是这样。
在这里插入图片描述

因为list是链式结构,因此他的访问和遍历不会用到operator[],而是通过迭代器来遍历,对于插入删除操作除了push_back,insert等等这些,还提供了头插头删这样的接口,因为他们不用进行数据的挪动。


2. 用法代码演示

1)头/尾 插/删和迭代器遍历

#include<list>

int main()
{
	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_front(30);
	lt.push_front(88);

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

	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;

	lt.pop_back();
	lt.pop_back();
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;

	lt.pop_front();
	lt.pop_front();
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;
	return 0;
}

2)insert与erase

首先,list的底层空间不是连续的,不能直接通过偏移量访问指定位置(如lt.begin() + 5会报错)。其次,insert操作不会导致迭代器失效,因为list的插入操作不需要扩容,插入后迭代器仍指向有效的逻辑位置。相反,erase操作会导致迭代器失效,删除后需要用返回值更新迭代器以避免野指针。最后,在删除元素时(如删除偶数),应通过erase返回的迭代器更新循环变量,确保迭代器有效。

void test02()
{
	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;

	//list的空间不是连续的,+5 并不能找到list中第5个位置,这样写编译器会报错的
	//lt.insert(lt.begin() + 5, 10);

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

	lt.insert(it, 89);

	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;

	//对于insert,迭代器不会失效,因为插入数据list不牵扯扩容,it所指向的逻辑位置也不发生改变
	//库里的没找到会返回last的位置

	it = find(lt.begin(), lt.end(), 3);
	if (it != lt.end())
	{
		lt.insert(it, 30);

		// insert以后,it不失效
		*it *= 100;
	}
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;

	//但是对于erase,使用过后迭代器会失效
	it = find(lt.begin(), lt.end(), 2);
	if (it != lt.end())
	{
		lt.erase(it);

		// erase以后,it失效,她的空间已经没了,就是一个野指针
		//*it *= 100;
	}
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;

	//要解决这个问题,可以拿iterator做返回值接收,更新it的值
	it = lt.begin();
	
	//还是我们的老朋友,删除list中偶数
	while (it != lt.end())
	{
		if (*it % 2 == 0)
		{
			//就是这里
			it = lt.erase(it);
		}
		else
			it++;

	}
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;
}

3)排序sort相关

由于list的迭代器是双向的,不能直接用标准库的sort(因为那是针对随机访问迭代器设计的快排),所以list自带了sort方法,它内部用的是归并排序。接着,代码还演示了两种反转方式:你可以用标准库的reverse函数,也可以直接用listreverse方法。二者效果相同。
在这里插入图片描述
在这里插入图片描述

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

	//库中的sort不可以,因为库中的排序是快排,迭代器的种类是随机,而这里迭代器的种类是双向
	//sort(lt.begin(), lt.end());

	//因此库中提供了一种底层为归并排序的排序
	lt.sort();
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;

	//list中还有类似逆置,交换这样的,库中和这里的都可以用
	reverse(lt.begin(), lt.end());
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;

	lt.reverse();
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;
}

当然,list中的排序我们是不建议用的,要对list中的数据进行排序,我们应该将其转化成vector,然后调用快排,最后在拷贝回去。

下面这是一段性能测试的代码:
可以看到,越大数据下快排性能越好
在这里插入图片描述

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)
	{
		auto e = rand();
		lt2.push_back(e);
		lt1.push_back(e);
	}

	// 拷贝到vector排序,排完以后再拷贝回来
	int begin1 = clock();
	// 先拷贝到vector
	for (auto e : lt1)
	{
		v.push_back(e);
	}

	// 排序
	sort(v.begin(), v.end());

	// 拷贝回去
	size_t i = 0;
	for (auto& e : lt1)
	{
		e = v[i++];
	}

	int end1 = clock();

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

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

4)其他相关

第一是remove,它用来删除链表中指定的值,找到了就删,找不到就不做任何操作。这个操作比较简单直接。

第二个是unique,它用来去除链表中的重复元素,但通常需要先排序,这样可以确保去重更高效。unique只会保留相邻元素中的一个,所以排序是去重前的重要一步。

第三个是splice,它用来将一段结点转移到另一个结点的后面。splice有几种用法,可以转移整个list、单个元素或一段区间的元素。splice不会创建新元素,而是直接在链表节点间重新链接,效率很高。

void test04()
{
	int myints[] = { 17,89,7,14 };
	/*std::list<int> mylist(myints, myints + 4);

	//remove是删除特定的值,找到了就删除,没找到就什么也不干
	mylist.remove(890);

	for (auto e : mylist)
	{
		cout << e << " ";
	}
	cout << endl*/;

	//unique是去重,一般要先排序,这要去重效率高
	//mylist.sort();             //  2.72,  3.14, 12.15, 12.77, 12.77,
	 15.3,  72.25, 72.25, 73.0,  73.35

	//mylist.unique();           //  2.72,  3.14, 12.15, 12.77
	 15.3,  72.25, 73.0,  73.35



	std::list<int> mylist1, mylist2;
	std::list<int>::iterator it;

	// set some initial values:
	for (int i = 1; i <= 4; ++i)
		mylist1.push_back(i);      // mylist1: 1 2 3 4

	for (int i = 1; i <= 3; ++i)
		mylist2.push_back(i * 10);   // mylist2: 10 20 30

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

	for (auto e : mylist2)
	{
		cout << e << " ";
	}
	cout << endl << endl;

	it = mylist1.begin();
	++it;                         // points to 2

	//转移是从iterator的位置,转移另一个list (整个/第i个位置/一个迭代区间)
	// 全部转移
	mylist1.splice(it, mylist2);

	// 部分转移
	//mylist1.splice(it, mylist2, ++mylist2.begin());
	//mylist1.splice(it, mylist2, ++mylist2.begin(), mylist2.end());

	//转移自己
	//mylist1.splice(mylist1.begin(), mylist1, ++mylist1.begin(), mylist1.end());

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

	for (auto e : mylist2)
	{
		cout << e << " ";
	}
	cout << endl;
}

二、list模拟实现

1. 结点类模板list_node

首先,要写出一个list,就需要先写出它的每一个结点的构成,这里的结构是我们之前学过的双向链表,为了泛用性,list的结点数据可以是任意类型的数据,因此我们需要写成类模板。

与双向链表一样,我们这里有三个变量分别是_next(后继), _prev(前驱), val(值),
并利用初始化模板将其初始化

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

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

2. 定义迭代器

1)为什么要专门封装一个迭代器?

list不想string和vector,它们两个的底层是由数组实现的,它们的迭代器就是其原生指针,因为原生指针就可以满足迭代器的行为。

迭代器所需要做到的行为如下:
*:解引用获取迭代器对应的值,类似与指针解引用获得值
!=:如:判断迭代器到没到end()等应用
++/--:迭代器迭代到下一个地点

对于vector来说,他的迭代器可以完美符合以上几点,因为vector底层空间是连续的,是数组,就像这样:

在这里插入图片描述

但是,对于list还能这样吗?
答案是不能,因为list底层不是连续的,*it并不是对应的值,而是一个结点,++it并不能让it指向下一个结点,因为空间是不连续的,++后指到哪里谁也不知道。

因此现有的迭代器的行为不符合我们的预期,我们期望的是迭代器表层使用的方式是一致的,因此我们写一个类,封装迭代器,用运算符重载来改变迭代器的行为,使他符合我们的预期。
在这里插入图片描述


2)迭代器类的实现

(1)普通迭代器
初始化
Node* _node;
__list_iterator(Node* node)
	:_node(node)
{}

operator*与operator->

我们先不关心它的返回值,我们先来让他符合我们的预期。

我们想让*返回的不是它的结点,而是结点中的值。
我们想让->返回的是T类型值(结构体)的地址。
注意:
在这里插入图片描述

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

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

operator++

我们想让operator++能到下一个结点的位置,--同理,因此需要让_node = _node->next,而不是指针地址的++,因为结点与结点之间空间是不连续的。

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)
{
	self tmp(*this);
	_node = _node->_prev;
	return tmp;
}

operator!=

我们想让迭代器的对应的结点与结点比较,而不是迭代器本身。

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

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

(2)const迭代器——参数T以及为什么有三个参数?

在学习list迭代器的封装时,我们发现了一个很神奇的现象,就是迭代器的模板参数居然有3个!!!

这是为什么呢?
在这里插入图片描述

T在这个地方我们都能理解,因为迭代器的类型就是T,也就是我们list的类型,那为什么还要这个RefPtr呢?

其实原因都是const迭代器搞的鬼。

我们在实现const的版本的时候,遇到一些非常坑的问题。

首先,这样设计const迭代器可以吗?
// typedef const __list_iterator<T> const_iterator;
答案是不可以,因为这样设计const迭代器是不行的,因为const迭代器期望指向内容不能修改
这样设计是迭代器本身不能修改,我们迭代器本身是要++的,不能改变的是它的值。

第二个问题,我们发现,const迭代器与普通迭代器的不同仅仅是因为迭代器所指向的值不能修改,那const迭代器不就是重载operator*的时候加一个const吗?
//typedef __list_const_iterator const_iterator;
但是问题是,这样设计太冗余了,有大量重复的代码,改变的仅仅是operator*前加const以及函数名字。

因此我们的解决方法是:增加一个模板参数Ref,它的作用是typedef定义const_iterator的时候,传得参数有一个是const与普通迭代器不一样,而operator*的返回值就使用Ref.
而Ref就是T的引用,与返回值保持一致.

像这样:
在这里插入图片描述


那么,第三个参数Ptr是什么呢?

类似于operator*,在对自定义类型解引用的方式还有->

比如我们下面有一个类:

class A
{
public:
	A(int a = 0, int b = 0)
		: _a(a)
		, _b(b)
	{}

	int _a;
	int _b;
};

我们想对这个类用迭代器遍历它的值有两种方式:

  1. 通过重载的operator*解引用,然后.出A类的_a和_b.
  2. 通过operator->直接访问到_a和_b.

在这里插入图片描述

而对于const对象,我们就不能直接调用operator->,因为会造成权限放大,所以我们引入了第三个模板参数Ptr表示T类型的指针,用它来区别const对象和普通对象。
因此operator->的返回值就成了Ptr
在这里插入图片描述

实际上,传不同的模板参数,就是生成了不同的模板,我们这样写其实就是让编译器帮我们写了之前那个冗余的写法,是我们代码更简洁,可读性更高~


(3)迭代器代码实现
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)
	{}

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

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

	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)
	{
		self tmp(*this);
		_node = _node->_prev;
		return tmp;
	}

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

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

};

3. list类实现

1)成员变量

对于每一个链表,我们需要直到它的哨兵位,还有它的大小。

private:
	Node* _head;
	size_t _size;

2)迭代器接口相关

前面我们定义了迭代器类,现在我们要用它获取begin()与end()。

对于带头双向循环链表来说,begin()是第一个位置的元素,也就是哨兵位的下一个,end()是最后一个元素的下一个位置,也就是又回到了哨兵位。
在这里插入图片描述

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

iterator begin()
{
	//这里做了隐式类型转换
	return _head->_next;
	//也可以写成这样
	//return iterator(head->_next);
}

iterator end()
{
	//这里做了隐式类型转换
	return _head;
	//也可以写成这样
	//return iterator(head);
}

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

const const_iterator end() const
{
	return _head;
}

3)构造相关

构造函数:

void empty_init()
{
	_head = new Node;
	_head->_next = _head;
	_head->_prev = _head;

	_size = 0;
}

list()
{
	empty_init();
}

拷贝构造:
这里依然沿用vector那里拷贝构造的思想,复用push_back来做到深拷贝。

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

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

赋值重载
使用现代写法。

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

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

析构函数
复用clear,再把头结点清理掉。

对于clear来说,vector的空间要释放必须完全释放,所以它只清理值,不回收空间,而list结点之间不连续,因此可以直接回收空间,当然,哨兵位是不动的。

~list()
{
	clear();

	delete _head;
	_head = nullptr;
}

void clear()
{
	iterator it = begin();

	while (it != end())
	{
		it = erase(it);
	}

	_size = 0;
}

4)插入删除相关

(1)insert与erase及其迭代器失效问题

insert任意位置插入
逻辑完全就是双链表的逻辑,先开辟出新结点空间,在改变对应的指向。

insert无法对pos进行断言,哪里插入都可以。

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

	prev->_next = newnode;
	newnode->_next = cur;

	cur->_prev = newnode;
	newnode->_prev = prev;

	++_size;

	return newnode;
}

因为list空间是不连续的,因此这里也不用扩容,对于insert来说,迭代器不失效。


erase删除任意位置的数据

erase需要对pos位置进行断言,pos不能改变我们的头结点,而end()就是头结点。

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;
	--_size;

	return next;
}
因为删除了这一个结点,而迭代器又指向这个结点空间,因此erase后迭代器会失效,是铁铁的野指针!

(2)头插/头删/尾插/尾删

逻辑与vector一样,直接复用insert与erase就好。

void push_back(const T& x)
{
	/*Node* newnode = new Node(x);

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

	insert(end(), x);
}

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

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

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

三、list与vector对比

下面是 vectorlist 的对比表格,涵盖了它们的主要特性和性能:

特性vectorlist
底层结构动态顺序表,一段连续空间带头结点的双向循环链表
随机访问支持随机访问,访问某个元素效率 O(1)不支持随机访问,访问某个元素效率 O(N)
插入和删除任意位置插入和删除效率低,需要搬移元素,时间复杂度 O(N),插入时有可能需要增容,增容:开辟新空间,拷贝元素,释放旧空间,导致效率更低任意位置插入和删除效率高,不需要搬移元素,时间复杂度为 O(1)
空间利用率底层为连续空间,不容易造成内存碎片,空间利用率高,缓存利用率高底层节点动态开辟,小节点容易造成内存碎片,空间利用率低,缓存利用率低
迭代器原生态指针,对原生态指针(节点指针)进行封装迭代器封装了节点指针
迭代器失效在插入元素时,要给所有的迭代器重新赋值,因为插入元素有可能会导致重新扩容,致使原来迭代器失效,删除时,当前迭代器需要重新赋值否则会失效插入元素不会导致迭代器失效,删除元素时,只会导致当前迭代器失效,其他迭代器不受影响
使用场景需要高效存储,支持随机访问,不关心插入删除效率大量插入和删除操作,不关心随机访问

总结

  • vector 更适合需要频繁随机访问和对存储效率要求较高的场景,但在插入和删除时性能较低。
  • list 更适合需要频繁插入和删除操作的场景,尤其是在中间位置进行操作时效率高,但不支持随机访问。

模拟实现list完整代码

#pragma once
#include<assert.h>
#include<iostream>
using namespace std;

namespace jyf
{

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

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

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

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

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

		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)
		{
			self tmp(*this);
			_node = _node->_prev;
			return tmp;
		}

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

		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;
		typedef __list_iterator<T, const T&, const T*> const_iterator;

		iterator begin()
		{
			//这里做了隐式类型转换
			return _head->_next;
			//也可以写成这样
			//return iterator(head->_next);
		}

		iterator end()
		{
			//这里做了隐式类型转换
			return _head;
			//也可以写成这样
			//return iterator(head);
		}

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

		const const_iterator end() const
		{
			return _head;
		}


		void empty_init()
		{
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;

			_size = 0;
		}

		list()
		{
			empty_init();
		}

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

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

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

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

		~list()
		{
			clear();

			delete _head;
			_head = nullptr;
		}

		void clear()
		{
			iterator it = begin();

			while (it != end())
			{
				it = erase(it);
			}

			_size = 0;
		}

		void push_back(const T& x)
		{
			/*Node* newnode = new Node(x);

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

			insert(end(), x);
		}

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

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

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

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

			prev->_next = newnode;
			newnode->_next = cur;

			cur->_prev = newnode;
			newnode->_prev = prev;

			++_size;

			return newnode;
		}

		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;
			--_size;

			return next;
		}

		size_t size()
		{
			return _size;
		}

	private:
		Node* _head;
		size_t _size;
	};

	void Print(const list<int>& lt)
	{
		list<int>::const_iterator it = lt.begin();
		while (it != lt.end())
		{
			// (*it) += 1;
			cout << *it << " ";
			++it;
		}
		cout << endl;
	}

	void test_list01()
	{
		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;
	}

	class A
	{
	public:
		A(int a = 0, int b = 0)
			: _a(a)
			, _b(b)
		{}

		int _a;
		int _b;
	};

	void test_list02()
	{
		list<A> lt;
		lt.push_back(A(1, 1));
		lt.push_back(A(2, 2));
		lt.push_back(A(3, 3));
		lt.push_back(A(4, 4));

		list<A>::iterator it = lt.begin();
		while (it != lt.end())
		{
			cout << (*it)._a << " " << (*it)._b << endl;
			++it;
		}
		cout << endl;

		it = lt.begin();
		while (it != lt.end())
		{
			cout << it->_a << " " << it->_b << endl;
			++it;
		}
		cout << endl;


	}

	void test_list03()
	{
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);
		lt.push_front(5);
		lt.push_front(6);
		lt.push_front(7);
		lt.push_front(8);
		for (auto e : lt)
		{
			cout << e << " ";
		}
		cout << endl;

		lt.pop_front();
		lt.pop_back();

		for (auto e : lt)
		{
			cout << e << " ";
		}
		cout << endl;

		lt.clear();
		lt.push_back(10);
		lt.push_back(20);
		lt.push_back(30);
		lt.push_back(40);
		for (auto e : lt)
		{
			cout << e << " ";
		}
		cout << endl;

		cout << lt.size() << endl;
	}

	void test_list04()
	{
		list<int> lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);

		for (auto e : lt)
		{
			cout << e << " ";
		}
		cout << endl;

		list<int> lt1(lt);
		for (auto e : lt1)
		{
			cout << e << " ";
		}
		cout << endl;

		list<int> lt2;
		lt2.push_back(10);
		lt2.push_back(20);
		lt2.push_back(30);
		lt2.push_back(40);

		for (auto e : lt2)
		{
			cout << e << " ";
		}
		cout << endl;

		lt1 = lt2;

		for (auto e : lt1)
		{
			cout << e << " ";
		}
		cout << endl;
	}
}

到这里模拟实现list就结束啦~

谢谢大家!

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

使用可白嫖的高配置服务器——DAMODEL进行AI开发教程

DAMODEL&#xff1a;DAMODEL 目前DAmodel注册并实名赠送50大洋的免费额度&#xff0c;搭载4090的服务器费用不到2r/h 教程&#xff1a; 完成注册并实名后 在此点击创建实例 选择实例配置 选择镜像&#xff0c;看你使用哪种dl框架 。 实例自带的磁盘会随实例释放。需要自己…

FineReport 图表切换维度

1、导入数据 可以参考导入Excel数据&#xff0c;直接导入数据也可以在数据库建表&#xff0c;Navicat直接导入数据 以下是数据库建表操作 -- 创建表 create table test11 ( orderTime date NULL, -- 下单时间 quantity int NULL -- 数量 ); 导入数据 2、SQL判断统计维度…

Docker 的数据管理

前置资源 Docker的数据管理资源.zip资源-CSDN文库 一、容器中数据管理 管理 Docker 容器中数据主要有两种方式&#xff1a;数据卷&#xff08;Data Volumes&#xff09;和数据卷容器&#xff08;DataVolumes Containers&#xff09;。 1&#xff0e;数据卷 数据卷是一个供容…

2024 年 Mac 下这些生产力工具,好用到哭

每段关系最终都会结束 即使不是因为别的原因 也会因为死亡 我只知道 你不对她说出来 她就永远不知道 你的心意 她那天离开的时候 才知道一个道理 有时候 保护一样重要的东西的方式 不是守在她旁边 而是离开她 离得远远的远到看起来谁也 不在乎谁一样 今天呢&#x…

Go-知识泛型

Go-知识泛型 1. 认识泛型1.1 不使用泛型1.2 使用泛型 2. 泛型的特点2.1 函数泛化2.2 类型泛化 3. 类型约束3.1 类型集合3.2 interface 类型集合3.2.1 内置interface类型集合3.2.2 自定义interface类型集合3.2.2.1 任意类型元素3.2.2.2 近似类型元素3.2.2.3 联合类型元素 3.2.3 …

Linux网络命令:用于配置防火墙规则的一个用户友好的工具ufw详解

目录 一、概述 二、安装 UFW 三、启动、重启和关闭 UFW 1、启动 2、关闭UFW 3、 重启 UFW 四、查看 UFW 状态 五、UFW 基本命令 1. 允许端口 &#xff08;1&#xff09;单个 TCP 端口 &#xff08;2&#xff09;允许单个 UDP 端口 &#xff08;3&#xff0…

音频响度归一化 - python 实现

在处理音频样本时&#xff0c;往往我们的音频样本由于录制设备&#xff0c;环境&#xff0c;人发音的音量大小的不同影响&#xff0c;会造成音频响度不统一&#xff0c;分布在一个不同的响度值域上。为了让语音模型更好的学习音频特征&#xff0c;就有必要对音频的响度进行归一…

【AIGC】ChatGPT是如何思考的:探索CoT思维链技术的奥秘

博客主页&#xff1a; [小ᶻZ࿆] 本文专栏: AIGC | ChatGPT 文章目录 &#x1f4af;前言&#x1f4af;什么是CoT思维链CoT思维链的背景与技术发展需求 &#x1f4af;CoT思维链的工作原理&#x1f4af;CoT思维链的应用领域&#x1f4af;CoT思维链的优势&#x1f4af;CoT思维…

ppt压缩文件怎么压缩?压缩PPT文件的多种压缩方法

ppt压缩文件怎么压缩&#xff1f;当文件体积过大时&#xff0c;分享和传输就会变得困难。许多电子邮件服务对附件的大小有限制&#xff0c;而在网络环境不佳时&#xff0c;上传和下载大文件可能耗时较长。此外&#xff0c;在不同设备上播放时&#xff0c;较大的PPT文件还可能导…

基于Java+SpringBoot+Uniapp的博客系统设计与实现

项目运行截图 技术框架 后端采用SpringBoot框架 Spring Boot 是一个用于快速开发基于 Spring 框架的应用程序的开源框架。它采用约定大于配置的理念&#xff0c;提供了一套默认的配置&#xff0c;让开发者可以更专注于业务逻辑而不是配置文件。Spring Boot 通过自动化配置和约…

<OS 有关> Windows 11 对不习惯菜单所做修改 自用

新安装 Windows 11 23H2 不习惯菜单&#xff0c;做的修改&#xff1a; 1. 禁用 Show More Options 鼠标右键 想使用旧版的 鼠标右键菜单&#xff0c; 不需要点 show more options , 如下图的方式&#xff1a; 创建一个 注册表文件&#xff1a; disable_content.reg Windows …

Maven 高级之分模块设计与继承、聚合

在软件开发中&#xff0c;随着项目规模的扩大&#xff0c;代码量和复杂度不断增加&#xff0c;传统的一体化开发模式逐渐暴露出诸多问题。为了解决这些问题&#xff0c;模块化开发应运而生&#xff0c;而 Maven 正是模块化开发的利器&#xff0c;它提供的继承和聚合机制为构建和…

STL源码剖析:STL算法

STL 算法总览 质变算法 mutating algorithms—会改变操作对象之值 所有的 STL算法都作用在由迭代器(first,last)所标示出来的区间上。所谓“质变算法”,是指运算过程中会更改区间内(迭代器所指)的元素内容。诸如拷贝(copy)、互换(swap)、替换(replace)、填写(fill)、删除(remov…

【H2O2|全栈】更多关于HTML(2)HTML5新增内容

目录 HTML5新特性 前言 准备工作 语义化标签 概念 新内容 案例 多媒体标签 音频标签audio 视频标签 video 新增部分input表单属性 预告和回顾 后话 HTML5新特性 前言 本系列博客是对入门专栏的HTML知识的补充&#xff0c;并伴随一些补充案例。 这一期主要介绍H…

一文区分SSTI 和 CSTI

前言 有时&#xff0c;SSTI&#xff08;服务器端模板注入&#xff09;和 CSTI&#xff08;客户端模板注入&#xff09;可能会由于它们相似的负载语法而混淆。这种混乱可能会导致渗透测试人员浪费时间尝试实现反向 shell&#xff0c;即使payload仅限于客户端。 定义 &#x1d…

电汽车充电革命:充电桩的过去现在与未来

电动汽车充电革命&#xff1a;中国充电桩行业的过去、现在与未来 一、发展历程概述 中国充电桩行业的发展历程可划分为以下几个阶段&#xff1a; 1. 初始期&#xff08;2006-2008年&#xff09;&#xff1a;在此阶段&#xff0c;国家队主导市场&#xff0c;主要参与者包括国…

linux的学习第二天

1.vmware的功能&#xff1a; 快照 创建快照&#xff1a; 拍摄此虚拟机的快照&#xff1a;记录保存虚拟机的当前状态&#xff0c;如果系统出现故障&#xff0c;可以通过快照还原&#xff08;错删系统时可以找到快照的系统状态&#xff0c;然后恢复系统&#xff09; 恢复快照…

基于LSTM-Transformer混合模型实现股票价格多变量时序预测(PyTorch版)

前言 系列专栏:【深度学习&#xff1a;算法项目实战】✨︎ 涉及医疗健康、财经金融、商业零售、食品饮料、运动健身、交通运输、环境科学、社交媒体以及文本和图像处理等诸多领域&#xff0c;讨论了各种复杂的深度神经网络思想&#xff0c;如卷积神经网络、循环神经网络、生成对…

如何替换OCP节点(一):使用oat | OceanBase应用实践

前言&#xff1a; OceanBase Cloud Platform&#xff08;简称OCP&#xff09;&#xff0c;是 OceanBase数据库的专属企业级数据库管理平台。 在实际生产环境中&#xff0c;OCP的安装通常是第一步&#xff0c;先搭建OCP平台&#xff0c;进而依赖OCP来创建、管理和监控我们的生…

Spark全网最全总结

Spark 产生之前&#xff0c;已经有 MapReduce 这类非常成熟的计算系统存在了&#xff0c;并提供 了高层次的 API(map/reduce)&#xff0c;把计算运行在集群中并提供容错能力&#xff0c;从而实现 分布式计算。 虽然 MapReduce 提供了对数据访问和计算的抽象&#xff0c…