c++----list模拟实现

目录

1. list的基本介绍

2. list的基本使用 

2.1 list的构造

用法示例 

2.2 list迭代器 

 用法示例

2.3. list容量(capacity)与访问(access)

用法示例

2.4 list modifiers 

用法示例 

2.5 list的迭代器失效 

3.list的模拟实现 

3.1 构造、析构

3.2 迭代器 

3.3 list modifiers 

4. list与vector的区别 

5.完整代码 


1. list的基本介绍

1. list是可以在常数范围内在任意位置进行插入和删除的序列式容器,并且该容器可以前后双向迭代。
2. list的底层是双向带头循环链表结构,双向链表中每个元素存储在互不相关的独立节点中,在节点中通过指针指向其前一个元素和后一个元素。
3. list与forward_list非常相似:最主要的不同在于forward_list是单链表,只能朝后迭代。
4. 与其他的序列式容器相比(array,vector,deque),list通常在任意位置进行插入、移除元素的执行效率更好。
5. 与其他序列式容器相比,list和forward_list最大的缺陷是不支持任意位置的随机访问,比如:要访问list的第6个元素,必须从已知的位置(比如头部或者尾部)迭代到该位置,在这段位置上迭代需要线性的时间开销;list还需要一些额外的空间,以保存每个节点的相关联信息(对于存储类型较小元素的大list来说这可能是一个重要的因素)

下图是list的底层结构。

从图中可以看出,list底层由一个双向带头循环链表构成。假设有一链表list<int> lt, 头节点为_head则lt.begin()指向_head->next, lt.end()指向_head。

2. list的基本使用 

2.1 list的构造

构造函数( (constructor))接口说明
list (size_type n, const value_type& val = value_type())构造的list中包含n个值为val的元素
list()构造空的list
list (const list& x)拷贝构造函数
list (InputIterator first, InputIterator last)用[first, last)区间中的元素构造list

用法示例 

void list_test1()
{
	list<string> lt(5, "xy");//n val构造
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;

	list<string> llt(lt);//拷贝构造
	for (auto e : llt)
	{
		cout << e << " ";
	}
	cout << endl;

	list<string> lltt(++lt.begin(), --lt.end());//迭代器区间构造
	for (auto e : lltt)
	{
		cout << e << " ";
	}
}

2.2 list迭代器 

函数声明接口说明
begin +
end
返回第一个元素的迭代器+返回最后一个元素下一个位置的迭代器
rbegin +
rend
返回第一个元素的reverse_iterator,即end位置,返回最后一个元素下一个位置的
reverse_iterator,即begin位置

 用法示例

void list_test2()
{
	list<int> lt{ 1,2,3,4,5};//这样构造很奇特
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;

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

那么大家肯定会有一个疑问,之前的string,vector迭代器可以++,是因为他们的底层物理空间是连续的,但list底层可是双向带头循环链表啊,它的结构物理空间可是不连续的,我们++,不就错了吗?

不急,我们后面模拟实现的时候细讲。

2.3. list容量(capacity)与访问(access)

函数声明接口说明
empty检测list是否为空,是返回true,否则返回false
size

返回list中有效节点的个数

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

用法示例

void list_test3()
{
	list<int> lt{ 1,2,3,4,5 };//这样构造很奇特
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;
	cout << "链表是否为空:   ";
	if (lt.empty())
		cout << "true" << endl;
	else
		cout << "false" << endl;

	cout << "size: " << lt.size() << endl;

	cout << "Front Element: " << lt.front() << endl;

	cout << "Back Element: " << lt.back() << endl;

	lt.clear();
	cout << "已执行链表清空操作,链表是否为空:   ";
	if (lt.empty())
		cout << "true" << endl;
	else
		cout << "false" << endl;

}

2.4 list modifiers 

函数声明接口说明
push_front在list首元素前插入值为val的元素
pop_front删除list中第一个元素
push_back在list尾部插入值为val的元素
pop_back删除list中最后一个元素
insert在list position 位置中插入值为val的元素
erase删除list position位置的元素
swap交换两个list中的元素
clear清空list中的有效元素

用法示例 

void list_test4()
{
	list<int> lt{ 1,2,3,4,5 };//这样构造很奇特
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;

	lt.push_back(0);
	lt.push_back(0);
	lt.push_front(9);
	lt.push_front(9);
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;

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

	list<int>::iterator it = lt.begin();
	while (it != lt.end())
	{
		if (*it == 5)
		{
			it = lt.erase(it);
			break;
		}
			
		it++;
	}
	lt.insert(it, 999);
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;
}

2.5 list的迭代器失效 

这里我们将迭代器理解为指针,list的迭代器失效是因为指针所指向的空间被销毁,导致指针变为野指针。list的底层是双向带头循环链表,因此list不存在插入导致迭代器失效的问题。list的迭代器失效出现在erase内,如果该节点被删除,空间已被销毁,那么就必须更新迭代器,否则迭代器失效。 

3.list的模拟实现 

list的模拟实现存在几个难点,首先是使用了大量的typedef,这会让人头晕眼花,其次是模板迭代器的构建。我们快来看看吧。

注意:list的迭代器由于list底层空间不连续的问题,因此要实现和vector一样的迭代器效果(即支持*it,it++)需要对操作符进行重载,所以要封装Node*。

list需要创建一个类内类Node,用list访问Node类的变量。

下面是ListNode类,类内包含双链表节点的三要素。

template<class T>
struct ListNode//节点的创建
{
	T _Date;
	ListNode<T>* _next;
	ListNode<T>* _prev;

	ListNode(const T& x=T())//节点的构造函数
		:_Date(x)
		,_next(nullptr)
		,_prev(nullptr)
	{}
};

3.1 构造、析构

这里的构造函数为简易版本,只支持创建空链表。 

void list_init()//初始化每一个节点
{
	_head = new Node;//new调用Node的默认构造,不传参
	_head->_next = _head;
	_head->_prev = _head;
}
list()
{
	list_init();
}
list(list<T>& lt)//拷贝构造
{
	list_init();//创建头节点

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

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

//list& operator=(list lt)
list<T>& operator=(list<T> lt)
{
	swap(lt);
	return *this;
}
void clear()
{
	list<T>::iterator it = this->begin();
	while (it != end())
	{
		it = erase(it);
	}
}
~list()
{
	delete _head;
	_head = nullptr;
}

3.2 迭代器 

前面说,要对++等操作符进行重载,这时需要重新定义iterator类封装节点的指针,通过指针对节点进行操作。且iterator类需要支持普通对象与const对象的访问,因此需要定义成模板类。

模板类iterator 

template<class T,class Ref,class Ptr>
struct _list_iterator//迭代器本质就是指针,这里封装Node*,对Node的指针进行操作,重载++等操作符
{
	typedef ListNode<T> Node;
	typedef _list_iterator<T,Ref,Ptr> self;
	//typedef _list_iterator<T> self;
	Node* _node;//成员变量

public:
	_list_iterator(Node* node)//构造函数,直接用node构建
		:_node(node)
	{}
	//没有参数int ,前置++
	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;
	}
	Ptr operator->()
	{
		return &_node->_Date;
	}
	Ref operator*()
	{
		return _node->_Date;
	}
	bool operator!=(const self& s)
	{
		return _node != s._node;
	}
	bool operator==(const self& s)
	{
		return _node == s._node;
	}
};

list类内的迭代器函数 

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

 这里模板参数有三个,分别是T,T&,T*,这是为什么呢?

T&:因为重载普通迭代器的*时,返回值需要设为引用类型,以便于外界可以更改。

 T*:重载->时,由于list的->逻辑不能自洽,list的重载->需要返回数据的地址

这里需要着重解释一下,如果是list<int>,那么重载->完全没有问题,但如果是list<Date>呢?

Node的数据域是自定义类型,又该怎么办呢?

因此我们返回Node.Date的地址,然后再解引用,如果是内置类型不受影响,如果是自定义类型,稍加控制就ok了。

我们来看看示例:

struct AA
{
	int _a1;
	int _a2;

	AA(int a1 = 1, int a2 = 1)
		:_a1(a1)
		, _a2(a2)
	{}
};
void test_list5()
{
	list<AA> lt;
	AA aa1;
	lt.push_back(aa1);

	lt.push_back(AA());

	AA aa2(2, 2);
	lt.push_back(aa2);

	lt.push_back(AA(2, 2));

	list<AA>::iterator it = lt.begin();
	while (it != lt.end())
	{
		std::cout << (*it)._a1 << ":" << (*it)._a2 << std::endl;
		std::cout << it.operator*()._a1 << ":" << it.operator*()._a2 << std::endl;

		std::cout << it->_a1 << ":" << it->_a2 << std::endl;
		std::cout << it.operator->()->_a1 << ":" << it.operator->()->_a2 << std::endl;

		++it;
	}
	std::cout << std::endl;
}

Node的数据域存放的是AA类对象,Node* ptr,ptr->Date就是AA类的实例化对象,此时返回AA对象的地址,也就是AA* p,p->就可以访问AA类对象的成员了。 

3.3 list modifiers 

这里需要注意erase的迭代器失效问题。 

void push_back(const T& x=T())
{
	Node* node = new Node;
	Node* tail = _head->_prev;
	node->_Date = x;

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

iterator insert(iterator pos,const T& x = T())//不存在迭代器失效问题
{
	Node* node = new Node;
	node->_Date = x;
	Node* cur = pos._node;
	Node* prev = cur->_prev;

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

	return node;
}

iterator erase(iterator pos)//这里的pos有可能会产生迭代器失效问题
{
	Node* cur = pos._node;
	Node* prev = cur->_prev;
	Node* next = cur->_next;

	prev->_next = next;
	next->_prev = prev;
	delete cur;
	cur = nullptr;
	return next;//更新pos
}

4. list与vector的区别 

list与vector都是c++STL序列容器的重要组成部分,由于底层结构的不同,造成了他们的功能和特性有所不同,详见下表。 

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


 

5.完整代码 

template<class T>
struct ListNode//节点的创建
{
	T _Date;
	ListNode<T>* _next;
	ListNode<T>* _prev;

	ListNode(const T& x=T())//节点的构造函数
		:_Date(x)
		,_next(nullptr)
		,_prev(nullptr)
	{}
};

template<class T,class Ref,class Ptr>
struct _list_iterator//迭代器本质就是指针,这里封装Node*,对Node的指针进行操作,重载++等操作符
{
	typedef ListNode<T> Node;
	typedef _list_iterator<T,Ref,Ptr> self;
	//typedef _list_iterator<T> self;
	Node* _node;//成员变量

public:
	_list_iterator(Node* node)//构造函数,直接用node构建
		:_node(node)
	{}
	//没有参数int ,前置++
	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;
	}
	Ptr operator->()
	{
		return &_node->_Date;
	}
	Ref operator*()
	{
		return _node->_Date;
	}
	bool operator!=(const self& s)
	{
		return _node != s._node;
	}
	bool operator==(const self& s)
	{
		return _node == s._node;
	}
};

template<class T>
class list
{
	typedef ListNode<T> Node;  //typedef
public:
	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;
	}
	void list_init()//初始化每一个节点
	{
		_head = new Node;//new调用Node的默认构造,不传参
		_head->_next = _head;
		_head->_prev = _head;
	}
	list()
	{
		list_init();
	}
	list(list<T>& lt)//拷贝构造
	{
		list_init();//创建头节点

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

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

	//list& operator=(list lt)
	list<T>& operator=(list<T> lt)
	{
		swap(lt);
		return *this;
	}
	void clear()
	{
		list<T>::iterator it = this->begin();
		while (it != end())
		{
			it = erase(it);
		}
	}
	~list()
	{
		delete _head;
		_head = nullptr;
	}
	void push_back(const T& x=T())
	{
		Node* node = new Node;
		Node* tail = _head->_prev;
		node->_Date = x;

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

	iterator insert(iterator pos,const T& x = T())//不存在迭代器失效问题
	{
		Node* node = new Node;
		node->_Date = x;
		Node* cur = pos._node;
		Node* prev = cur->_prev;

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

		return node;
	}

	iterator erase(iterator pos)//这里的pos有可能会产生迭代器失效问题
	{
		Node* cur = pos._node;
		Node* prev = cur->_prev;
		Node* next = cur->_next;

		prev->_next = next;
		next->_prev = prev;
		delete cur;
		cur = nullptr;
		return next;//更新pos
	}

	size_t size()const//返回size
	{
		int cnt = 0;
		list<int>::const_iterator it = begin();
		while (it != end())
		{
			cnt++;
			it++;
		};
		return cnt;
	}
	bool empty()const//判空
	{
		return _head ->_next== _head;
	}


private:
	Node* _head;//成员变量为ListNode* 即访问节点的指针
};


 

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

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

相关文章

sqli第24关二次注入

注入点 # Validating the user input........$username $_SESSION["username"];$curr_pass mysql_real_escape_string($_POST[current_password]);$pass mysql_real_escape_string($_POST[password]);$re_pass mysql_real_escape_string($_POST[re_password]);if($p…

高档次定制线缆工厂-精工电联:智能化生产如何提升线缆品质

在百年未有之大变局的当下&#xff0c;科技发展日新月异的今天&#xff0c;智能化生产已经成为各行各业追求高效、高品质的重要手段。作为线缆行业的领先者&#xff0c;精工电联始终站在行业前沿&#xff0c;致力于通过智能化生产提升线缆品质&#xff0c;为客户创造更多、更有…

SpringMVC常见面试题

1&#xff1a;Spring mvc执行流程 回答&#xff1a; 版本1&#xff1a;视图版本&#xff0c;jsp 用户发送出请求到前端控制器DispatcherServletDispatcherServlet收到请求调用HandlerMapping(处理映射器)HandlerMapping找到具体的处理器&#xff0c;生成处理器对象及处理器拦…

ctfshow web入门 XXE

XXE基础知识 XXE&#xff08;XML External Entity&#xff09;攻击是一种针对XML处理漏洞的网络安全攻击手段。攻击者利用应用程序在解析XML输入时的漏洞&#xff0c;构造恶意的XML数据&#xff0c;进而实现各种恶意目的。 所以要学习xxe就需要了解xml xml相关&#xff1a; …

【应用层协议原理】

文章目录 第二章 应用层2.1 应用层协议原理2.1.1 网络应用的体系结构2.1.2 客户-服务器&#xff08;C/S&#xff09;体系结构2.1.3 对等体&#xff08;P2P&#xff09;体系结构2.2.4 C/S和P2P体系结构的混合体2.2.5 进程通信问题1&#xff1a;对进程进行编址&#xff08;addres…

YOLOv5改进系列:升级版ResNet的新主干网络DenseNet

一、论文理论 论文地址&#xff1a;Densely Connected Convolutional Networks 1.理论思想 DenseNet最大化前后层信息交流&#xff0c;通过建立前面所有层与后面层的密集连接&#xff0c;实现了特征在通道维度上的复用&#xff0c;不但减缓了梯度消失的现象&#xff0c;也使其…

蓝桥杯刷题day12——元素交换【算法赛】

一、题目描述 给定个大小为2N的二进制数组A&#xff0c;其中包含N个0和N个1。 现在&#xff0c;你可以交换数组中的任意两个元素。请你计算&#xff0c;至少需要多少次交换操作&#xff0c;才能保证数组中不存在连续的0或1. 输入格式 第行包含一个整数N(1<N≤10^5),表示数…

【微服务】OpenFeign+Sentinel集中处理远程调用异常

文章目录 1.微服务基本环境调整1.对10004模块的application.yml调整2.启动nacos以及一个消费者两个提供者3.测试1.输入http://localhost:8848/nacos/index.html 来查看注册情况2.浏览器访问 http://localhost:81/member/nacos/consumer/get/13.结果 2.使用OpenFeign实现微服务模…

【echart】数据可视化

什么是数据可视化&#xff1f; 数据可视化主要目的:借助于图形化手段&#xff0c;清晰有效地传达与沟通信息。 数据可视化可以把数据从冰冷的数字转换成图形&#xff0c;揭示蕴含在数据中的规律和道理。 如何绘制&#xff1f; echarts 图表的绘制&#xff0c;大体分为三步:…

使用1panel部署Ollama WebUI(dcoekr版)浅谈

文章目录 说明配置镜像加速Ollama WebUI容器部署Ollama WebUI使用问题解决&#xff1a;访问页面空白 说明 1Panel简化了docker的部署&#xff0c;提供了可视化的操作&#xff0c;但是我在尝试创建Ollama WebUI容器时&#xff0c;遇到了从github拉取镜像网速很慢的问题&#xf…

pytest--python的一种测试框架--pytest初阶

前言 使用pytest去做测试时我们对文件名的命名其实是有规范的&#xff0c;要用test_开头&#xff01;&#xff01;&#xff01; 一、pytest初阶 def test_one():expect1actual1assert expectactual#测试专用语句&#xff1a;assert&#xff0c;识别期望与实际值是否相等这个…

后疫情时代CS保研沉思录暨2023年个人保研经验贴

个人情况 正如古话所说&#xff0c;最适合你的才是最好的。因此这里先贴上个人基本情况&#xff0c;用作参考。 如果你的个人情况与我相近&#xff0c;则有更强的参考作用。如果情况相差较大&#xff0c;也可以姑且引为例子来研究。 学校层次&#xff1a;中流至末流211 专业…

Linux 学习之路 -- 工具篇 -- gcc / g++

在 Linux 系统中&#xff0c;gcc 和 g 是两个常用的编译工具&#xff0c;分别用于编译 C 和 C 代码。下面我将介绍gcc、g的一些基本用法 目录 一、简单的认识 二、简单了解一下编译的过程 <1> 预处理阶段 <2>编译 <3>汇编 <4>链接…

Redis慢日志

SLOWLOG 是用来读取和重置 Redis 慢查询日志的命令&#xff0c;Redis 2.2.12 版本开始支持 1.Redis 慢查询日志概述 客户端从发送命令到获取返回结果经过了以下几个步骤&#xff1a; 1. 客户端发送命令 2. 该命令进入 Redis 队列排队等待执行 3. Redis 开始执行命令 - Red…

飞天使-k8s知识点28-kubernetes散装知识点5-helm安装ingress

文章目录 安装helm添加仓库下载包配置创建命名空间安装 安装helm https://get.helm.sh/helm-v3.2.3-linux-amd64.tar.gztar -xf helm-v3.2.3-linux-amd64.tar.gzcd linux-amd64mv helm /usr/local/bin修改/etc/profile 文件&#xff0c;修改里面内容,然后重新启用export PATH$P…

嵌入式数据库-Sqlite3

阅读引言&#xff1a; 本文将会从环境sqlite3的安装、数据库的基础知识、sqlite3命令、以及sqlite的sql语句最后还有一个完整的代码实例&#xff0c; 相信仔细学习完这篇内容之后大家一定能有所收获。 目录 一、数据库的基础知识 1.数据库的基本概念 2.常用数据库 3.嵌入式…

在A中删除既在B表中出现又在C表中出现的元素

方法一&#xff08;感觉有点取巧&#xff0c;不太推荐&#xff0c;但是实现简单&#xff09;&#xff1a; 算法思想:保留La的头节点&#xff0c;并用pcur指针指向La链中的第一个结点&#xff0c;通过pcur指针遍历La中的每一个元素&#xff0c;并判断该元素是否在Lb和Lc链中出现…

优化选址问题 | 基于帝国企鹅算法求解工厂-中心-需求点三级选址问题含Matlab源码

目录 问题代码问题 "帝国企鹅算法"并不是一个广为人知的优化算法,可能是一个特定领域或者特定情境下提出的方法。不过,对于工厂-中心-需求点三级选址问题,它可能是一种启发式优化方法,用于在多个候选位置中选择最优的工厂、中心和需求点位置。 这类问题通常涉及…

HAL STM32 硬件I2C方式读取AS5600磁编码器获取角度例程

HAL STM32 硬件I2C方式读取AS5600磁编码器获取角度例程 &#x1f4cd;相关篇《STM32 软件I2C方式读取AS5600磁编码器获取角度例程》 ✨stm32使用硬件I2C去读取角度数据&#xff0c;通过STM32CubeMX工具配置工程&#xff0c;读取角度数据&#xff0c;只需要调用一个函数&#xf…

css3之动画animation

动画animation 一.优点二.定义和使用三.动画序列和解释四.常见属性及解释五.简写&#xff08;名字和时间不能省略&#xff09;&#xff08;持续时间在何时开始的时间前&#xff09;&#xff08;简写中无animation-play-state)六.例子1.大数据热点图2.奔跑的熊大&#xff08;一个…