【C++】list的模拟实现【完整理解版】

news2025/1/15 23:45:54

目录

一、list的概念引入

1、vector与list的对比

 2、关于struct和class的使用

3、list的迭代器失效问题 

 二、list的模拟实现

1、list三个基本函数类

2、list的结点类的实现

3、list的迭代器类的实现 

3.1 基本框架

3.2构造函数 

3.3 operator*

3.4 operator->

3.5 operator前置++和--与后置++和-- 

3.5 operator==与operator!=

4、list类的实现

 4.1 基本框架

4.2 构造函数 

4.2 begin()和end()

4.3 拷贝构造

4.4  clear

4.5 operator=

4.6  析构函数

 4.7 insert

4.8 push_back 和 push_front

4.9 erase

4.10 pop_back 和 pop_front

三、完整源代码:

list.h:

 test.cpp:


一、list的概念引入

1、vector与list的对比

 两者的区别十分类似于顺序表和链表的区别


 2、关于struct和class的使用

C++中的structclass的唯一区别在于默认访问限定符(即你不写public、private这种访问限定符)不同,struct默认为公有(public),而class默认为私有(private),所以下文写的节点和迭代器类都用struct是因为,struct中的成员函数默认为公有了,也不用写public了,但是你用class就要写个public


3、list的迭代器失效问题 

 list本质:带头双向循环链表 

支持操作接口的角度分迭代器的类型:单向(forward_list)、双向(list)、随机(vector)

从使用场景的角度分迭代器的类型:(正向迭代器,反向迭代器)+const迭代器

 迭代器失效本质内存空间的问题,失效原因:

1、增容完还指向旧空间

2、节点已经被释放(list里面)

list的erase迭代器失效

注意:删除不会报错,迭代器失效也不会报错,是删除以后迭代器失效了,是去访问失效的迭代器才会报错

插入知识点(库里面的find实现,可不看


 二、list的模拟实现

1、list三个基本函数类

list本质是一个带头双向循环链表:

模拟实现list,要实现下列三个类

①、模拟实现结点类
②、模拟实现迭代器的类
③、模拟list主要功能的类
list的类的模拟实现其基本功能(增删等操作)要建立在迭代器类和结点类均已实现好的情况下才得以完成。

2、list的结点类的实现

因为list的本质为带头双向循环链表,所以其每个结点都要确保有下列成员:

  1. 前驱指针
  2. 后继指针
  3. data值存放数据

而结点类的内部只需要实现一个构造函数即可。

//1、结点类
template<class T>
struct __list_node//前面加__是命名习惯,一般这么写表示是给别人用的
{
	//前驱指针和后驱指针
	__list_node<T>* _next;//C++中可不写struct,直接用名定义,但注意这里还要加类型
	__list_node<T>* _prev;
	T _data;//存节点的值

	//构造函数
	__list_node(const T& val = T())//给一个缺省值T()
		:_next(nullptr)
		, _prev(nullptr)
		, _data(val)
	{}
};

①、为什么是__list_node<T>?

首先,C++中用struct定义时可不加struct,重点是这里用了一个类模板,类模板的类名不是真正的类型,即__list_node不是真正的类型,定义变量时__list_node<T>这种才是真正的类型,也就是用类模板定义变量时必须 指定对应的类型 


3、list的迭代器类的实现 

因为list其本质是带头双向循环链表,而链表的物理空间是不连续的,是通过结点的指针顺次链接,我们不能像先前的string和vector一样直接解引用去访问其数据,结点的指针解引用还是结点,结点指针++还是结点指针,而string和vector的物理空间是连续的,所以这俩不需要实现迭代器类,可以直接使用。

为了能让list像vector一样解引用后访问对应节点中的值,++访问到下一个数据,我们需要单独写一个迭代器类的接口实现,在其内部进行封装补齐相应的功能,而这就要借助运算符重载来完成。 

注:迭代器封装后是想模拟指针的行为 

3.1 基本框架

template<class T,class Ref,class Ptr>
struct __list_iterator
{
	typedef __list_node<T> Node;
	typedef __list_iterator<T, Ref, Ptr> Self;
	Node* _node;
}

①、迭代器类模板为什么要三个参数?

若只有普通迭代器的话,一个class T参数就够了,但因为有const迭代器原因,需要加两个参数,两个参数名Ref(reference:引用)和Ptr(pointer:指针),名字怎么起都行,但这种有意义的名字是很推荐的,即这两个参数一个让你传引用,一个让你传指针,具体等下文讲到const迭代器再说

②、迭代器类到底是什么?

迭代器类就一个节点的指针变量_node,但是因为我们要运算符重载等一系列操作,不得不把list的迭代器写成类,完成那些操作,list的迭代器才能正确的++到下一位置,解引用访问节点的值

③、节点指针和迭代器的区别?


3.2构造函数 

	//迭代器的构造函数只需要一个指针构造
	__list_iterator (Node* node)
		:_node(node)
	{ }

3.3 operator*

//*it(调用的是函数,返回节点中的值)
	Ref operator*()
	{//出了作用域还在,引用返回
		return _node->_data;
	}

①、 返回值为什么是Ref?

Ref是模板参数,因为迭代器类的模板参数Ref传入的要么是T&要么是const T&,就是为了const迭代器和普通迭代器的同时实现,底层就是这么实现的,意义就是一个只读,一个可读可写 

注:解引用迭代器就是为了访问对应位置的值,那么list只要通过迭代器返回对应节点的值就好了(比如*it,我们就想要对应的值)


3.4 operator->

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

①、为什么需要operator->?

本质因为自定义类型需要,那需从list存的类型是个自定义类型说起,以Date类型为例

 

若list存了个自定义类型的Date类,程序错误,因为我们并没有重载Date类的operator<<,那写一个operator<<重载吗?不,因为你无法确定要用哪些类,也不能每个类都写个operator<<

从根源上解决问题: 在迭代器中实现个operator->

上面代码返回值应该写为Ptr,方便const迭代器和普通迭代器的通用,Ptr的本质就是const T*和T*,都差不多,上面图示忘记改了,用T*作返回值也可说明问题


3.5 operator前置++和--与后置++和-- 

//++it;(迭代器++本质就是指针往后移,加完后还应是个迭代器)
	Self& operator++()
	{
		_node = _node->_next;
		return *this;
	}
	//it++;
	Self operator++(int)//加参数以便于区分前置++
	{
		Self tmp(*this);//拷贝构造tmp
		_node = _node->_next;//直接让自己指向下一个结点即可实现++
		return tmp;//注意返回tmp,才是后置++
	}
	//--it;
	Self& operator--()
	{
		_node = _node->_prev;//让_node指向上一个结点即可实现--
		return *this;
	}
	//it--;
	Self operator--(int)//记得传缺省值以区分前置--
	{
		Self tmp(*this);//拷贝构造tmp
		_node = _node->_prev;
		return tmp;
	}

①、迭代器++对于list是什么意思?

迭代器++的意思就是想让其指向下一个节点,--正好相反,为了区分前置和后置++(--),我们会用函数重载,也就是多一个“没用的”参数:int,这个参数没什么用,只是为了区分++与--


3.5 operator==与operator!=


	//it != end() 
	bool operator!=(const Self& it)
	{
		//迭代器是否相等只要判断对应节点地址是否相等即可
		return _node != it._node;
	}
	bool operator==(const Self& it)
	{
		return _node == it._node;
	}

①、两个迭代器怎么比较的?

迭代器中就一个成员变量_node,节点指针,只要比较当前的节点指针是否相同即可,个人认为这个操作意义不大


4、list类的实现

 在结点类和迭代器类都实现的前提下,就可实现list主要功能:增删等操作的实现

 4.1 基本框架

//3、链表类
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;
}

①、const_iterator(const迭代器的介绍)

我们知道const_iterator在begin()和end()中的返回值是需要用到的,其主要作用就是当迭代器只读时使用, 因为普通迭代器和const迭代器的实现区别仅仅在于内部成员函数的返回值不同,难道重写一遍吗?不用,我们模板参数多两个就好了,一个是引用class Ref(T&或const T&),一个是指针class Ptr(T*或const T*),当Ref时const T&就是const迭代器的调用,当Ref时T& 时就是普通迭代器的调用,这就利用模板实现了两个迭代器的同时实现


4.2 构造函数 

//带头双向循环链表的构造函数
list()
{
	_head = new Node;
	_head->_next = _head;
	_head->_prev = _head;
}

解释:我们开辟一个头结点,然后使其处于一个对应的初始状态即可


4.2 begin()和end()

iterator begin()
{
	//第一个位置应该是头结点的下一个节点
	return iterator(_head->_next);//用匿名对象构造iterator类型的
}
iterator end()
{
	//最后一个数据的下一个位置应该是第一个节点,即头结点
	return iterator(_head);
}

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

①、关于匿名构造的理解

比如 iterator(_head->_next); iterator是个类模板类型(它被typedef过的),那不应该实例化一个对象再构造吗?这里没有用是因为这里是匿名对象的构造,这里这么用比较方便


4.3 拷贝构造

//拷贝构造:传统写法
list(const list<T>& lt)
{
	_head = new Node;//开辟一样的头结点
	_head->_next = _head;
	_head->_prev = _head;
	//1、用迭代器遍历
	/*const_iterator it = lt.begin();
	while (it != lt.end())
	{
		push_back(*it);
		++it;
	}*/

	//2、范围for遍历
	//遍历lt1,把lt1的元素push_back到lt2里头
	for (auto e : lt)
	{
		push_back(e);//自动开辟新空间,完成深拷贝
	}
}

 解释:list的拷贝构造跟vector不同,它的拷贝是拷贝一个个节点(因为不连续的物理空间), 那么我们可以用迭代器拿到list的一个个节点

①、为什么拷贝构造现代写法需初始化,operator=不需要?

以string的模拟实现为例

这里为什么s2要初始化?

因为s2是要被拷贝构造出来的,没被拷贝构造前还没存在,然后s2要跟tmp交换,如果也就是tmp得到s2的数据,如果s2之前没初始化,析构销毁就出问题了,因为没初始化是随机值

但是赋值不一样,赋值是两个对象都存在,不存在随机值问题,所以不用一上来就初始化

还有一种现代写法先不介绍


4.4  clear

void clear()
{//clear不删除头结点,因为万一删除了头结点你还想插入数据怎么办
	iterator it = begin();
	while (it != end())
	{
		it = erase(it);
	}
}

①、 it = erase(it)什么意思?

防止迭代器失效,因为erase返回的是被删除位置的下一位置,比如删除pos位置的,pos位置被删除后,it都不用动,erase算自动指向下一位置了,故用it接收


4.5 operator=

 

        
	//赋值运算符重载(传统写法)
	//lt1 = lt3
	//list<T>& operator=(const list<T>& lt)
	//{
	//	if (this != &lt)
	//	{
	//		clear();//先释放lt1
	//		for (auto e : lt)
	//			push_back(e);
	//	}
	//	return *this;
	//}

	//赋值运算符重载(现代写法)
	//lt1 = lt3
	list<T>& operator=(list<T> lt)//套用传值传参去拷贝构造完成深拷贝
	{
		swap(_head,lt._head);//交换两个list的头结点即可
		//lt出了作用域,析构函数销毁lt1原来的链表,一举两得
		//swap(lt);
		return *this;
	}

注:传统写法要先把被赋值的对象先释放,然后利用push_bak尾插,push_back在下文说明


4.6  析构函数


	//析构函数
	~list()
	{
		clear();//删除除头结点以外的节点
		delete _head;//删去哨兵位头结点
		_head = nullptr;
	}

 4.7 insert

//insert,插入pos位置之前
iterator insert(iterator pos, const T& x)
{
	Node* newnode = new Node(x);//创建新的结点
	Node* cur = pos._node; //迭代器pos处的结点指针
	Node* prev = cur->_prev;
	//prev newnode cur
	//链接prev和newnode
	prev->_next = newnode;
	newnode->_prev = prev;
	//链接newnode和cur
	newnode->_next = cur;
	cur->_prev = newnode;
	//返回新插入元素的迭代器位置
	return iterator(newnode);
}

①、返回参数为什么是iterator?

本质是为了防止迭代器失效问题

注:insert指的是插入到指定位置的前面


4.8 push_back 和 push_front

//尾插
void push_back(const T& x)
{
	Node* tail = _head->_prev;//找尾
	Node* newnode = new Node(x);//创建一个新的结点
	//_head  tail  newnode
	//使tail和newnode构成循环
	tail->_next = newnode;
	newnode->_prev = tail;
	//使newnode和头结点_head构成循环
	newnode->_next = _head;
	_head->_prev = newnode;

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

4.9 erase

//erase
iterator erase(iterator pos)
{
	assert(pos != end());
	Node* cur = pos._node;
	Node* prev = cur->_prev;
	Node* next = cur->_next;
	//prev cur next
	//链接prev和next
	prev->_next = next;
	next->_prev = prev;
	//delete删去结点,因为每一个节点都是动态开辟出来的
	delete cur;
	//返回被删除元素后一个元素的迭代器位置
	//return next;
	return iterator(next);
}

①、 返回值问题

erase的返回值,返回的是被删除位置的下一位置


4.10 pop_back 和 pop_front

//尾删
void pop_back()
{
	erase(--end());
	//erase(iterator(_head->prev));//构造个匿名对象
}
//头删
void pop_front()
{
	erase(begin());
}

最后, list的排序意义不大,因为实际生活中我们都是对数组等排序

三、完整源代码:

list.h:

#pragma once

namespace mz
{
	//1、结点类
	template<class T>
	struct __list_node//前面加__是命名习惯,一般这么写表示是给别人用的
	{
		//前驱指针和后驱指针
		__list_node<T>* _next;//C++中可不写struct,直接用名定义,但注意这里还要加类型
		__list_node<T>* _prev;
		T _data;//存节点的值

		//构造函数
		__list_node(const T& val = T())//给一个缺省值T()
			:_next(nullptr)
			, _prev(nullptr)
			, _data(val)
		{}
	};

	//2、迭代器类

	//__list_iterator<T,T&,T*>              -> iterator
	//__list_iterator<T,const T&,const T*>  -> const_iterator
	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)
		{ }

		//*it(调用的是函数,返回节点中的值)
		Ref operator*()
		{//出了作用域还在,引用返回
			return _node->_data;
		}

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

		//++it;(迭代器++本质就是指针往后移,加完后还应是个迭代器)
		Self& operator++()
		{
			_node = _node->_next;
			return *this;
		}
		//it++;
		Self operator++(int)//加参数以便于区分前置++
		{
			Self tmp(*this);//拷贝构造tmp
			_node = _node->_next;//直接让自己指向下一个结点即可实现++
			return tmp;//注意返回tmp,才是后置++
		}
		//--it;
		Self& operator--()
		{
			_node = _node->_prev;//让_node指向上一个结点即可实现--
			return *this;
		}
		//it--;
		Self operator--(int)//记得传缺省值以区分前置--
		{
			Self tmp(*this);//拷贝构造tmp
			_node = _node->_prev;
			return tmp;
		}

		//it != end() 
		bool operator!=(const Self& it)
		{
			//迭代器是否相等只要判断对应节点地址是否相等即可
			return _node != it._node;
		}
		bool operator==(const Self& it)
		{
			return _node == it._node;
		}

	};

	//3、链表类
	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 iterator(_head->_next);//用匿名对象构造iterator类型的
		}
		iterator end()
		{
			//最后一个数据的下一个位置应该是第一个节点,即头结点
			return iterator(_head);
		}

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

		//带头双向循环链表的构造函数
		list()
		{
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;
		}

		//拷贝构造:传统写法
		list(const list<T>& lt)
		{
			_head = new Node;//开辟一样的头结点
			_head->_next = _head;
			_head->_prev = _head;
			//1、用迭代器遍历
			/*const_iterator it = lt.begin();
			while (it != lt.end())
			{
				push_back(*it);
				++it;
			}*/

			//2、范围for遍历
			//遍历lt1,把lt1的元素push_back到lt2里头
			for (auto e : lt)
			{
				push_back(e);//自动开辟新空间,完成深拷贝
			}
		}

	
		//赋值运算符重载(传统写法)
		//lt1 = lt3
		//list<T>& operator=(const list<T>& lt)
		//{
		//	if (this != &lt)
		//	{
		//		clear();//先释放lt1
		//		for (auto e : lt)
		//			push_back(e);
		//	}
		//	return *this;
		//}

		//赋值运算符重载(现代写法)
		//lt1 = lt3
		list<T>& operator=(list<T> lt)//套用传值传参去拷贝构造完成深拷贝
		{
			swap(_head,lt._head);//交换两个list的头结点即可
			//lt出了作用域,析构函数销毁lt1原来的链表,一举两得
			//swap(lt);
			return *this;
		}

		//析构函数
		~list()
		{
			clear();//删除除头结点以外的节点
			delete _head;//删去哨兵位头结点
			_head = nullptr;
		}

		void clear()
		{//clear不删除头结点,因为万一删除了头结点你还想插入数据怎么办
			iterator it = begin();
			while (it != end())
			{
				it = erase(it);
			}
		}

		//尾插
		void push_back(const T& x)
		{
			Node* tail = _head->_prev;//找尾
			Node* newnode = new Node(x);//创建一个新的结点
			//_head  tail  newnode
			//使tail和newnode构成循环
			tail->_next = newnode;
			newnode->_prev = tail;
			//使newnode和头结点_head构成循环
			newnode->_next = _head;
			_head->_prev = newnode;

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

		//insert,插入pos位置之前
		iterator insert(iterator pos, const T& x)
		{
			Node* newnode = new Node(x);//创建新的结点
			Node* cur = pos._node; //迭代器pos处的结点指针
			Node* prev = cur->_prev;
			//prev newnode cur
			//链接prev和newnode
			prev->_next = newnode;
			newnode->_prev = prev;
			//链接newnode和cur
			newnode->_next = cur;
			cur->_prev = newnode;
			//返回新插入元素的迭代器位置
			return iterator(newnode);
		}

		//erase
		iterator erase(iterator pos)
		{
			assert(pos != end());
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* next = cur->_next;
			//prev cur next
			//链接prev和next
			prev->_next = next;
			next->_prev = prev;
			//delete删去结点,因为每一个节点都是动态开辟出来的
			delete cur;
			//返回被删除元素后一个元素的迭代器位置
			//return next;
			return iterator(next);
		}
		//尾删
		void pop_back()
		{
			erase(--end());
			//erase(iterator(_head->prev));//构造个匿名对象
		}
		//头删
		void pop_front()
		{
			erase(begin());
		}

	private:
		Node* _head;//头结点
	};
	
	void test1()
	{
		list<int>lt;
		lt.push_back(1);
		lt.push_back(2);
		lt.push_back(3);
		lt.push_back(4);
		//类外访问迭代器需要指定类域,类内访问可直接访问
		list<int>::iterator it = lt.begin();
		while (it != lt.end())
		{
			cout << *it << " ";
			++it;
		}
		cout << endl;
	}

	struct Date
	{
		int _year = 0;
		int _month = 1;
		int _day = 1;
	};

	void test2()
	{
		Date* p2 = new Date;
		*p2;//取到的是Date
		p2->_year;//取到的是Date类中的成员变量

		list<Date>lt;
		lt.push_back(Date());
		lt.push_back(Date());
		//list存了个日期类(自定义类型)的类型
		list<Date>::iterator it = lt.begin();
		while (it != lt.end())
		{
			//cout << *it << " ";
			cout << it->_year << "-" << it->_month << "-" << it->_day << endl;
			++it;
		}
		cout << endl;

	}

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

	void test3()
	{
		list<int>lt1;
		lt1.push_back(1);
		lt1.push_back(2);
		lt1.push_back(3);
		lt1.push_back(4);

		list<int> lt2(lt1);
		print_list(lt2);

		list<int>lt3;
		lt3.push_back(10);
		lt3.push_back(20);
		lt3.push_back(30);
		lt3.push_back(40);
		lt1 = lt3;
		print_list(lt1);

	}
}

 test.cpp:

#include<iostream>
#include<assert.h>
using namespace std;
#include"list.h"

int main()
{
	//mz::test1();
	mz::test3();

	return 0;
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

四叶图-openGL 例子,第四章。计算机图形学 中例子 代码有点瑕疵

第四版 计算机图形学 中例子 代码有点瑕疵&#xff0c;见下图&#xff0c;本道长保证这个程序没有运行过。 可运行代码如下。 #include "stdafx.h" #include <GL/glut.h> #include <stdlib.h> #include <math.h> #include <iostream> using…

UG\NX二次开发 计算uv参数的最小值最大值 UF_MODL_ask_face_uv_minmax

文章作者:里海 来源网站:王牌飞行员_里海_里海NX二次开发3000例,里海BlockUI专栏,C\C++-CSDN博客 简介: UG\NX二次开发 计算uv参数的最小值最大值 UF_MODL_ask_face_uv_minmax 效果: 代码: #include "me.hpp"void ufusr(char* param, int* ret…

探索如何将html和svg导出为图片

笔者开源了一个Web思维导图&#xff0c;在做导出为图片的功能时走了挺多弯路&#xff0c;所以通过本文来记录一下。 思维导图的节点和连线都是通过 svg 渲染的&#xff0c;作为一个纯 js 库&#xff0c;我们不考虑通过后端来实现&#xff0c;所以只能思考如何通过纯前端的方式…

d3dx9_43.dll丢失如何修复?四种快速修复d3dx9_43.dll丢失的方法分享

在我们日常使用电脑的过程中&#xff0c;有时候会遇到一些问题&#xff0c;其中比较常见的一种就是电脑提示 d3dx9_43.dll 丢失。对于这种情况&#xff0c;我们该如何解决呢&#xff1f;本文将详细介绍 d3dx9_43.dll 文件的相关信息&#xff0c;以及解决 d3dx9_43.dll 丢失的四…

[hello,world]这个如何将[ ] 去掉

[hello,world]这个如何将[ ] 去掉&#xff1f; 你可以使用编程语言中的字符串处理函数来去掉方括号。以下是一个示例代码&#xff0c;使用Python的strip()函数去掉方括号&#xff1a; text "[hello,world]" text text.strip("[]") print(text)输出为&a…

CVPR:使用完全交叉Transformer的小样本目标检测

关注并星标 从此不迷路 计算机视觉研究院 公众号ID&#xff5c;ComputerVisionGzq 学习群&#xff5c;扫码在主页获取加入方式 论文地址&#xff1a; https://openaccess.thecvf.com/content/CVPR2022/papers/Han_Few-Shot_Object_Detection_With_Fully_Cross-Transformer_CVPR…

多元共进|支持多元梦想,创造包容文化环境

谷歌致力于推动多元、平等、共融 鼓励每个人赞扬自己取得的成就 了解自我展示的重要性 一起了解 2023 Google 开发者大会上 谷歌如何支持企业创造多元共融的文化 打造包容性的工作场所 为每个人创造更加温暖的环境 多元、平等、共融 (DEI)&#xff0c;三个板块之间互相联系&…

[H5动画制作系列] Sprite及Text Demo

参考代码: sprite.js: var canvas, stage, container; canvas document.getElementById("mainView"); function init() {stage new createjs.Stage(canvas);createjs.Touch.enable(stage);var loader new createjs.LoadQueue(false);loader.addEventListener(&q…

2023年深度测评对比两款大热SaaS平台,国内SaaS是否已经跑出独角兽?

什么是SaaS平台&#xff1f;SaaS平台是否已经形成了自己的核心竞争力&#xff1f;SaaS平台是否在国内跑出独角兽&#xff1f;本篇&#xff0c;我们将为大家测评国内最热的两款SaaS平台&#xff0c;全文干货&#xff0c;请大家安心食用。 一、SaaS平台是什么&#xff1f; SaaS…

青创智通亮相上海GAF 2023全球数字化智能装配工程与装备技术大会

​​​​​​FMEA软件-智能扭矩系统-智能测量系统-青创智通 9月13日-15日由螺丝君主办的“聚焦技术、引领创新”为主题的GAF2023数字化智能装配工程与装备技术大会&#xff0c;将在上海汽车会展中心开幕&#xff0c;北京青创智通携带SunTorque智能扭矩系统、智能扭矩小车亮相此…

MPLAB X IPE安装新版本之后打不开了,提示:Warning - could not install some modules:

FAE的踩坑之路——4、MPLAB X IPE 打不开&#xff0c;提示&#xff1a;Warning - could not install some modules: - 大大通(简体站) (wpgdadatong.com.cn) 我是怎么出现这个问题的呢&#xff1f;以前一直使用的老版本v5.45开发环境&#xff0c;然后想体验一下新版本 v6.10&am…

SuperMap iClient3D 11i (2023) SP1 for Cesium之移动实体对象

作者&#xff1a;nannan 目录 前言 一、代码思路 1.1 绘制面实体对象 1.2 鼠标左键按下事件 1.3 鼠标移动事件 1.4 鼠标左键抬起事件 二、运行效果 三、注意事项 前言 SuperMap 官网三维前端范例 编辑线面&#xff0c;可以对面实体对象的节点进行增加、删除以及修改位置…

PYTHON学习-文件管理,打工人必备~

&#x1f308;write in front&#x1f308; &#x1f9f8;大家好&#xff0c;我是Aileen&#x1f9f8;.希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流. &#x1f194;本文由Aileen_0v0&#x1f9f8; 原创 CSDN首发&#x1f412; 如…

秒杀场景下用Redis分布式锁解决超卖问题

前言 超卖问题通常出现在多用户并发操作的情况下&#xff0c;即多个用户尝试购买同一件商品&#xff0c;导致商品库存不足或者超卖。解决超卖问题的方法有很多&#xff1a;乐观锁、Redis分布式锁、消息队列等。 分布式锁是一种多节点共享的同步机制&#xff0c;通过在多个节点…

《TCP/IP网络编程》阅读笔记--getsockopt和setsockopt的使用

目录 1--Socket的多种可选项 2--getsocketopt() 3--setsockopt() 4--代码实例 1--Socket的多种可选项 Socket 拥有多种可选项&#xff0c;其可分为 SOL_SOCKET 层&#xff0c;IPPROTO_IP 层和IPPROTO_TCP 层等&#xff0c;一般通过 getsocketopt() 和 setsockopt() 函数进行…

【Cpolar内网穿透】公网SSH远程连接Termux – 电脑使用安卓Termux

目录 前言 1.安装ssh 2.安装cpolar内网穿透 3.远程ssh连接配置 4.公网远程连接 5.固定远程连接地址 前言 使用安卓机跑东西的时候&#xff0c;屏幕太小&#xff0c;有时候操作不习惯。不过我们可以开启ssh&#xff0c;使用电脑PC端SSH远程连接手机termux。 本次教程主要…

C#,《小白学程序》第二十课:大数(BigInteger)的四则运算之一,加法

大数的&#xff08;加减乘除&#xff09;四则运算、阶乘运算。 乘法计算包括小学生算法、Karatsuba和Toom-Cook3算法。 重复了部分 19 课的代码。 1 文本格式 using System; using System.Linq; using System.Text; using System.Collections.Generic; /// <summary>…

软件架构设计(十一) 软件产品线

1、基本概念 软件产品线是从DSSA(特定领域架构)扩展而来的,我们从事软件开发,当您有了多年的经验之后,往往会沉淀到某一个特定领域,然后做这个领域的行业软件。 此时,您会把一些共性的东西开发出来,稳定下来。如果以后再开发同类型的系统时,做一些小修小改或者应用层…

《Python趣味工具》——自制emoji(1)绘制爱心应援牌❤️

项目目标&#xff1a; 本项目旨在学习如何自制emoji&#xff0c;学习内容主要分为3个方面&#xff0c;分3次来讲&#xff0c;根本在于了解了turtle的使用&#xff01; 1.完成一个爱心应援牌 2.完成静态的emoji 3.让emoji动起来&#xff01; 今天我们来学习第一个部分。 文章目录…

推荐一个图像生成开源项目——Fooocus

目录 什么是Fooocus&#xff1f; 项目地址 性能消耗 如何安装 效果对比 总结 什么是Fooocus&#xff1f; Fooocus是一款图像生成软件&#xff0c;但它不同寻常&#xff0c;是对稳定扩散&#xff08;Stable Diffusion&#xff09;和Midjourney的设计理念的巧妙重新思考。本…