【STL详解 —— list的模拟实现】

news2024/11/20 13:25:14

STL详解 —— list的模拟实现

  • list接口总览
  • 结点类的模拟实现
    • 构造函数
  • 迭代器类的模拟实现
    • 迭代器类的模板参数说明
    • 构造函数
    • ++运算符的重载
    • --运算符的重载
    • ==运算符的重载
    • !=运算符的重载
    • * 运算符的重载
    • -> 运算符的重载
  • list的模拟实现
    • 默认成员函数
        • 构造函数
        • 拷贝构造函数
        • 赋值运算符重载函数
        • 析构函数
    • list iterator(迭代器)
        • begin和end
    • list element access(访问容器相关函数)
        • front和back
    • list modifiers(修改)
        • insert
        • erase
        • push_back和pop_back
        • push_front和pop_front
        • size
        • empty
        • swap
  • 总览

list接口总览

namespace qq
{
	//模拟list中的节点类
	template<class T>
	struct ListNode
	{
		//成员变量
		ListNode<T>* _next;
		ListNode<T>* _prev;
		T _data;

		//成员函数
		ListNode(const T& x = T())
			:_next(nullptr)
			, _prev(nullptr)
			, _data(x)
		{}
	};

	//模拟实现list迭代器
	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)
		{}


		//各种运算符重载函数
		Ref operator*();
		Ptr operator->();
		self& operator++();		//++it
		self& operator++(int);	//it++
		self& operator--();		//--it
		self& operator--(int);	//it--
		bool operator!=(const self& it);
		bool operator==(const self& it);
	};

	//模拟实现list
	template<class T>
	class list
	{
	public:
		typedef ListNode<T> Node;
		typedef ListIterator<T, T&, T*> iterator;
		typedef ListIterator<T, const T&, const T*> const_iterator;
		

		//list iterator(迭代器)
		const_iterator begin()const;
		const_iterator end()const;
		iterator begin();
		iterator end();


		void clear();					

		//默认成员函数
		list();
		list(const list<T>& lt);		
		list<T>& operator=(list<T> lt);	
		~list();
		void empty_init();


		//list element access(访问容器相关函数)		注意:List不支持operator[]											
		T& front();
		const T& front()const;

		T& back();
		const T& back()const;
	

		// list modifiers(修改)
		void swap(list<T>& lt);		
		void push_back(const T& x);
		void push_front(const T& x);
		void pop_back();
		void pop_front();
		void insert(iterator pos, const T& val);
		iterator erase(iterator pos);
		size_t size()const;
		bool empty()const;

	private:
		Node* _head;
		size_t _size;
	};
}

在上面的代码中,模拟实现STL std::list 通过三个主要的类进行封装:ListNodeListIterator,和 list。这样的封装提供了清晰的职责分离,并模仿了 STL 的设计哲学,每个类都具有特定的功能和目的。下面详细解释每个类的作用及其重要性:

  1. ListNode
    这个类代表链表的节点。链表是由一系列节点组成,每个节点包含数据和指向链表中前一个节点和后一个节点的指针。在 ListNode 中,成员变量 _next 和 _prev 分别是指向下一个和上一个节点的指针,而 _data 存储节点的值。这种设计允许链表在插入和删除操作中提供高效的性能,因为不需要重新排列整个数据结构,只需要修改指针。

  2. ListIterator
    这个类是链表的迭代器,它提供了遍历链表的机制。迭代器是一个重要的抽象,使得链表可以使用类似于数组的方式进行访问和修改。迭代器通过重载操作符(如 ++ 和 --)来前进和后退,通过解引用操作符 (* 和 ->) 来访问节点的数据。通过提供标准迭代器接口,list 类可以与标准算法(如 std::sort, std::find 等)一起工作,增加了其通用性和灵活性。

  3. list
    这是一个容器类,提供对链表的高级管理。这个类封装了对链表的所有操作,如添加和删除元素、访问元素、清空列表、获取列表大小等。它使用 ListNode 来存储数据,使用 ListIterator 来提供对元素的迭代访问。此外,list 还负责管理资源,包括节点的创建和销毁,确保程序的正确性和效率。

通过将不同的功能封装在不同的类中,代码更加模块化,易于理解和维护。例如,ListNode 关心节点的表示和链接,ListIterator 关心如何遍历这些节点,而 list 管理整个链表的结构。


并且,这里的 list 与之前模拟实现的 vectorstring 有一些显著的不同。后两者都是在连续的物理空间上进行操作,类似于数组,这使得它们可以通过简单的指针运算快速访问任意位置的元素。相比之下,list不是在连续的物理空间中存储数据,而是由一系列分散的节点组成,每个节点通过指针与前一个和后一个节点相连接。

在这里插入图片描述

在这里插入图片描述

因此,对于 vectorstring,它们的迭代器基本上是对原生指针的轻量级封装,直接指向元素的存储位置。这使得迭代器可以直接通过指针运算来访问或修改元素,从而提供类似数组的效率。

然而,list 的存储结构要求其迭代器必须能够处理非连续的节点。因此,list 的迭代器不是简单的原生指针,而是一个更复杂的对象,它包含指向当前节点的指针。这种迭代器通过重载 ++--等操作符来移动到相邻的节点,而不是通过简单的地址运算。此外,迭代器需要通过解引用操作访问节点内部的数据(例如,通过 _data 成员),这进一步区别于基于连续内存存储的容器。

这种设计使得list的插入和删除操作可以在任何位置高效进行,因为这些操作只涉及到指针的重新指向,而不需要移动多个元素。这使得list在需要频繁插入和删除的场景下表现得更优越。然而,这也意味着list在随机访问方面的性能不如基于数组的容器,如 vector string

结点类的模拟实现

list 在底层来看,他是一个带头双向循环链表,如下图:
在这里插入图片描述

所以,一个节点包含三个成员变量前驱指针(_next) 后驱指针 ( _prev) 数据(_data)

成员函数只用提供一个构造函数即可。
而析构函数是因为 ListNode 类中的数据成员决定了是否需要一个显式的析构函数。

  1. 简单数据成员:如果 ListNode 的 _data 成员是内置类型(如 int, double, char 等),或者是一些简单的、不需要特殊资源管理的自定义类型(例如不涉及动态内存管理的类),那么编译器生成的默认析构函数足以正确清理 ListNode 对象。在这种情况下,节点的内存管理(创建和销毁节点)由 list 类通过其构造函数和析构函数来处理。

  2. 复杂数据成员:如果 _data 成员是一个复杂的类,如那些拥有动态内存分配或其他资源(如文件句柄、网络连接等)的类,则这个类需要自己的析构函数来正确释放这些资源。然而,在 ListNode 类中,即便 _data 是复杂类型,其析构也应由 _data 类型自身负责。ListNode 类本身只需关心其指针成员 _next 和 _prev 的链接关系,而这些成员也不需要特殊的资源释放逻辑。

  3. 资源管理:关于 ListNode 的 next 和 prev 指针,它们通常只是指向其他 ListNode 对象,不需要在 ListNode 的析构函数中进行特殊处理。资源的分配和释放(比如 new 和 delete 操作)通常在 list 类的其他成员函数中处理,如插入、删除元素的函数。

总结来说,ListNode 类不需要显式定义析构函数,是因为其成员自动调用它们各自的析构函数,无需额外逻辑来释放资源。list 的析构函数负责遍历所有节点并删除它们,从而管理整个链表的生命周期。

构造函数

结点类的构造函数直接根据所给数据构造一个结点即可,构造出来的结点的数据域存储的就是所给数据,而前驱指针和后继指针均初始化为空指针即可。

//构造函数
ListNode(const T& x = T())
			:_next(nullptr)
			,_prev(nullptr)
			,_data(x)
		{}

注意
使用 T() 表示如果在构造 ListNode 对象时没有提供参数,构造函数会自动创建一个 T 类型的临时对象(使用 T 的默认构造函数)。这使得在创建 ListNode 时可以省略参数,构造函数会使用 T 类型的默认值。

迭代器类的模拟实现

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)
		{}
		
		//*it
		Ref operator*()

		{
			return _node->_data;
		}

		//it->
		Ptr operator->()
		{
			return &_node->_data;
		}
		//++it
		self& operator++()
		{
			_node = _node->_next;
			return *this;
		}

		//it++
		self& operator++(int)
		{
			self tmp(*this);
			_node = _node->_next;
			return tmp;
		}

		//--it
		self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}

		//it--
		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 Ref,class Ptr>

在list的模拟实现当中,我们typedef了两个迭代器类型,普通迭代器和const迭代器。

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

ListIterator 类的模板参数列表中,RefPtr 分别指代引用和指针类型。

使用普通迭代器时,编译器会实例化一个普通迭代器对象;而使用常量迭代器时,则会实例化一个常量迭代器对象。

若该迭代器类不设计三个模板参数,将难以有效区分普通迭代器和常量迭代器。

构造函数

//构造函数
ListIterator(Node* node)
    : _node(node)
{}

参数:构造函数接受一个指向 ListNode<T> 类型的指针 node。这个指针指向列表中的一个节点。
功能:构造函数的主要功能是将 _node 成员变量初始化为传入的 node 指针所指向的节点。这样就建立了迭代器与列表节点之间的关联,使得迭代器可以通过指针访问节点的数据。

++运算符的重载

++it 前置递增操作符重载

	//++it
	self& operator++()
	{
		_node = _node->_next;
		return *this;
	}
  • 功能:这个函数实现了前置递增操作符,即 ++it。它使迭代器向前移动到列表中的下一个节点。
  • 操作:将迭代器当前指向的节点 _node指向下一个节点 _next。这样迭代器就指向了列表中的下一个元素。
  • 返回值:返回类型为self&,表示返回一个对自身的引用,以支持链式调用。这样可以使得多次操作可以连续执行。

it++ 后置递增操作符重载

	//it++
	self& operator++(int)
	{
		self tmp(*this);
		_node = _node->_next;
		return tmp;
	}
  • 功能:这个函数实现了后置递增操作符,即 it++。它使迭代器向前移动到列表中的下一个节点,并返回移动前的迭代器。
  • 操作:首先,创建一个临时的迭代器 tmp,它是当前迭代器的副本。然后,将当前迭代器指向下一个节点。最后,返回之前创建的临时迭代器 tmp,表示返回移动前的迭代器。
  • 返回值:返回类型为 self&,表示返回一个对自身的引用,以支持链式调用。因为后置递增操作符应该返回移动前的迭代器的值,而不是移动后的。

–运算符的重载

–的重载思路与++相类似

//--it
		self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}

//it--
		self& operator--(int)
		{
			self tmp(*this);
			_node = _node->_prev;
			return tmp;
		}

==运算符的重载

bool operator==(const self& it)
	{
		return _node == it._node;
	} 
  • 功能:这个函数用于比较两个迭代器是否指向相同的节点。
  • 参数:参数 const self& it 是另一个迭代器对象,表示要与当前迭代器进行比较的对象。
  • 操作:将当前迭代器 _node 指向的节点地址与参数迭代器 it 的 _node 指向的节点地址进行比较。如果它们指向的是同一个节点,则返回 true;否则返回 false。
  • 返回值:返回一个布尔值,表示两个迭代器是否相等。如果它们指向相同的节点,则返回 true;否则返回 false。

!=运算符的重载

bool operator!=(const self& it)
	{
		return _node != it._node;
	}
  • 功能:该函数用于比较两个迭代器是否指向不同的节点。
  • 参数:参数 const self& it 是另一个迭代器对象,表示要与当前迭代器进行比较的对象。
  • 操作:将当前迭代器 _node 指向的节点地址与参数迭代器 it 的 _node 指向的节点地址进行比较。如果它们指向的不是同一个节点,则返回 true;否则返回 false。
  • 返回值:返回一个布尔值,表示两个迭代器是否不相等。如果它们指向不同的节点,则返回 true;否则返回 false。

* 运算符的重载

//*it
	Ref operator*()
	{
		return _node->_data;
	}	
  • 功能:这个函数用于返回迭代器当前指向节点的数据。
  • 操作:它通过返回 _node->_data,即当前节点的数据,来提供对数据的访问。
  • 返回值:返回类型为 Ref,即引用类型,表示返回的是当前节点数据的引用。这样做可以直接操作节点数据,而不需要进行拷贝。

-> 运算符的重载

//it->
	Ptr operator->()
	{
		return &_node->_data;
	}
  • 功能:这个函数用于返回一个指向迭代器当前指向节点数据的指针。
  • 操作:它通过返回 &_node->_data,即指向当前节点数据的指针,来提供对数据的访问。
  • 返回值:返回类型为 Ptr,即指针类型,表示返回的是当前节点数据的指针。这样做使得我们可以通过指针访问节点的数据成员,例如使用箭头运算符(->)。

想想如下场景:
 当list容器当中的每个结点存储的不是内置类型,而是自定义类型,例如日期类,那么当我们拿到一个位置的迭代器时,我们可能会使用->运算符访问Date的成员:

	list<Date> lt;
	Date d1(2021, 8, 10);
	Date d2(1980, 4, 3);
	Date d3(1931, 6, 29);
	lt.push_back(d1);
	lt.push_back(d2);
	lt.push_back(d3);
	list<Date>::iterator pos = lt.begin();
	cout << pos->_year << endl; //输出第一个日期的年份

注意: 使用pos->_year这种访问方式时,需要将日期类的成员变量设置为公有。

对于->运算符的重载,我们直接返回结点当中所存储数据的地址即可。

Ptr operator->()
{
	return &_pnode->_val; //返回结点指针所指结点的数据的地址
}

在这里插入图片描述

这里本来是应该有两个->的,第一个箭头是pos ->去调用重载的operator->返回Date* 的指针,第二个箭头是Date* 的指针去访问对象当中的成员变量_year。

但是一个地方出现两个箭头,程序的可读性太差了,所以编译器做了特殊识别处理,为了增加程序的可读性,省略了一个箭头。

list的模拟实现

默认成员函数

构造函数

list是一个带头双向循环链表,在构造一个list对象时,直接申请一个头结点,并让其前驱指针和后继指针都指向自己即可。
在这里插入图片描述

//构造函数
list()
{
	_head = new Node;			 //申请一个头结点
	_head->_next = _head;		//头结点的后继指针指向自己
	_head->_prev = _head;		//头结点的前驱指针指向自己
		
	_size = 0;					//用于计数,先置为0
}

拷贝构造函数

拷贝构造函数就是根据所给list容器,拷贝构造出一个对象。对于拷贝构造函数,我们先申请一个头结点,并让其前驱指针和后继指针都指向自己,然后将所给容器当中的数据,通过遍历的方式一个个尾插到新构造的容器后面即可。

list(const list<T> &lt)
		{
			//先申请一个头节点
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;

			_size = 0;
			
			//复用push_back 来对这个节点进行尾插。
			//这里和vector的拷贝构造类似,不建议使用memcoy,如果是自定义类型数据,则memcpy将会出错。
			for (auto& e : lt)
			{
				push_back(e);
			}
		}
赋值运算符重载函数

我们这里直接使用现代写法:
这里编译器接收右值的时候自动调用其拷贝构造函数,使用swap()来交换这两个对象,因为值传值传参,故交换的是临时拷贝对象。

list<T>& operator=(list<T> lt)	//编译器接收右值的时候自动调用其拷贝构造函数
	{
		swap(lt);	//交换这两个对象
		return *this;	//支持连续赋值
	}
析构函数

对对象进行析构时,首先调用clear()函数清理容器当中的数据,然后将头结点释放,最后将头指针置空即可。

//析构函数
~list()
{
	clear(); //清理容器
	delete _head; //释放头结点
	_head = nullptr; //头指针置空
}

list iterator(迭代器)

begin和end

在这里插入图片描述

iterator begin()
{
	//返回使用头结点后一个结点的地址构造出来的普通迭代器
	return iterator(_head->_next);
}
iterator end()
{
	//返回使用头结点的地址构造出来的普通迭代器
	return iterator(_head);
}

重载一对用于const对象的begin函数和end函数。

const_iterator begin() const
{
	//返回使用头结点后一个结点的地址构造出来的const迭代器
	return const_iterator(_head->_next);
}
const_iterator end() const
{
	//返回使用头结点的地址构造出来的普通const迭代器
	return const_iterator(_head);
}

list element access(访问容器相关函数)

front和back

frontback函数分别用于获取第一个有效数据和最后一个有效数据,因此,实现frontback函数时,直接返回第一个有效数据和最后一个有效数据的引用即可。

T& front()
{
	return *begin(); //返回第一个有效数据的引用
}
T& back()
{
	return *(--end()); //返回最后一个有效数据的引用
}

当然,这也需要重载一对用于const对象的front函数和back函数,因为const对象调用frontback函数后所得到的数据不能被修改。

const T& front() const
{
	return *begin(); //返回第一个有效数据的const引用
}
T& back()
{
	return *(--end()); //返回最后一个有效数据的引用
}
const T& back() const
{
	return *(--end()); //返回最后一个有效数据的const引用
}

list modifiers(修改)

insert

insert函数可以在所给迭代器之前插入一个新结点。
在这里插入图片描述
这里的逻辑与之前我们用C实现链表数据结构时候的思想差不多,具体看见数据结构 | C语言链表讲解(新手入门).

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

			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;
		}
erase

erase函数可以删除所给迭代器位置的结点。
在这里插入图片描述

iterator erase(iterator pos)
	{
		_size--;
		Node* cur = pos._node;
		Node* prev = cur->_prev;
		Node* next = cur->_next;
		
		prev->_next = next;
		next->_prev = prev;
		delete cur;

		return iterator(next);
	}
push_back和pop_back

push_back和pop_back函数分别用于list的尾插和尾删,在已经实现了insert和erase函数的情况下,我们可以通过复用函数来实现push_back和pop_back函数。

push_back函数就是在头结点前插入结点,而pop_back就是删除头结点的前一个结点。

void push_back(const T& x)
	{
		insert(end(), x);
	}
void pop_back()
		{
			erase(--end());
		}

push_front和pop_front

当然,用于头插和头删的push_front和pop_front函数也可以复用insert和erase函数来实现。

push_front函数就是在第一个有效结点前插入结点,而pop_front就是删除第一个有效结点。

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

_size作为类的成员变量,当每次改变 list 的容量时,_size相应的++ 或者 --

size_t size()const
	{
		return _size;
	}
empty
bool empty()const
	{
		return _size == 0;
	}
swap

swap函数用于交换两个容器,list容器当中存储的实际上就只有链表的头指针,我们将这两个容器当中的头指针交换即可。

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

总览

#include<assert.h>
#include<iostream>
namespace qq
{
	template<class T>
	struct ListNode
	{
		ListNode<T>* _next;
		ListNode<T>* _prev;
		T _data;

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

	/*typedef ListIterator<T, T&, T*> iterator;
	typedef ListConstIterator<T, const T&, const T*> const_iterator;*/

	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)
		{}
		
		//*it
		Ref operator*()

		{
			return _node->_data;
		}

		//it->
		Ptr operator->()
		{
			return &_node->_data;
		}
		//++it
		self& operator++()
		{
			_node = _node->_next;
			return *this;
		}

		//it++
		self& operator++(int)
		{
			self tmp(*this);
			_node = _node->_next;
			return tmp;
		}

		//--it
		self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}

		//it--
		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 ListNode<T> Node;
	public:
		/*typedef ListIterator<T> iterator;
		typedef ListConstIterator<T> const_iterator;*/

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

		const_iterator begin()const
		{
			return ListIterator<T, const T&, const T*>(_head->_next);
		}

		/*iterator end()
		{
			return ListIterator<T>(_head); 
		}*/
		const_iterator end()const
		{
			return _head;
		}


		iterator begin()
		{
			return ListIterator<T, T&, T*>(_head->_next);
		}

		iterator end()
		{
			return _head;
		}

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

			_size = 0;
		}

		list()
		{
			empty_init();
		}


		//lt2(lt1) 
		list(const list<T> &lt)
		{
			empty_init();
			for (auto& e : lt)
			{
				push_back(e);
			}
		}
		void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				it = erase(it);
			}
		}

		void swap(list<T>& lt)
		{
			std::swap(_head, lt._head);
			std::swap(_size, lt._size);
		}
		//lt3 = lt1;
		list<T>& operator=(list<T> lt)
		{
			swap(lt);
			return *this;
		}

		//需要析构,就需要深拷贝
		//没有析构,就不用深拷贝
		~list()
		{
			clear();
			delete _head;
			_head = nullptr;
		}
		/*void push_back(const T& x)
		{
			Node* newnode = new Node(x);
			Node* tail = _head->_prev;

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


		T& front()
		{
			return *begin(); //返回第一个有效数据的引用
		}
		T& back()
		{
			return *(--end()); //返回最后一个有效数据的引用
		}

		const T& front() const
		{
			return *begin(); //返回第一个有效数据的const引用
		}
		T& back()
		{
			return *(--end()); //返回最后一个有效数据的引用
		}
		const T& back() const
		{
			return *(--end()); //返回最后一个有效数据的const引用
		}


		void push_back(const T& x)
		{
			insert(end(), x);
		}

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

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

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



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

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

		iterator erase(iterator pos)
		{

			_size--;
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* next = cur->_next;

			prev->_next = next;
			next->_prev = prev;
			delete cur;

			return iterator(next);
		}

		size_t size()const
		{
			return _size;
		}

		bool empty()const
		{
			return _size == 0;
		}
	private:
		Node* _head;
		size_t _size;
	};

}

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

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

相关文章

SQL语法 case when语句用法讲解

CASE WHEN解释 &#xff1a; SQL中的CASE WHEN语句是一种条件表达式&#xff0c;它允许你根据不同的情况返回不同的值。CASE WHEN通常用于SELECT语句中&#xff0c;用于创建新的列&#xff0c;该列的值取决于其他列的值。CASE WHEN可以用于任何可以使用表达式的地方。 大致概…

顺序表(增删减改)+通讯录项目(数据结构)

什么是顺序表 顺序表和数组的区别 顺序表本质就是数组 结构体初阶进阶 系统化的学习-CSDN博客 简单解释一下&#xff0c;就像大家去吃饭&#xff0c;然后左边是苍蝇馆子&#xff0c;右边是修饰过的苍蝇馆子&#xff0c;但是那个好看的苍蝇馆子一看&#xff0c;这不行啊&a…

UI自动化测试中公认最佳的设计模式-POM

一、概念 什么是POM&#xff1f; POM是PageObjectModule&#xff08;页面对象模式&#xff09;的缩写&#xff0c;其目的是为了Web UI测试创建对象库。在这种模式下&#xff0c;应用涉及的每一个页面应该定义为一个单独的类。类中应该包含此页面上的页面元素对象和处理这些元…

css 实现排行榜向上滚动

使用动画实现无线向上滚动 复制一层dom&#xff0c;使用动画向上滚动&#xff0c;鼠标hover的时候暂停动画 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthd…

Java调用WebServices接口

当拿到一个WebServices接口时&#xff0c;首先用接口测试工具调用一下接口&#xff0c;看是否可以正常发送请求和获取返回接口&#xff0c;确保接口是没有问题的&#xff0c;可以用SoapUI工具进行测试。 下面以一个免费的天气预报接口为例&#xff0c;记录整个接口的调用过程。…

【echarts】支持根据data中最大的数值来动态计算y轴最大数据以及间隔

以这个图例可以看到Y轴最大显示到250ml&#xff0c;如果超过就会出现有一部分数据看不到了&#xff1a; https://echarts.apache.org/examples/zh/editor.html?cmix-line-bar 那么如何根据这个data数据的最大值&#xff0c;来动态计算y轴最大数据以及间隔呢&#xff1f; 那我…

数组(java)

目录 数组的定义和使用&#xff1a; 数组的初始化&#xff1a; 遍历数组&#xff1a; 数组是引用类型 初始JVM的内存分布 再读引用变量 认识null 数组的应用场景 作为函数的参数 作为函数的返回值 数组练习 数组转字符串 排序 冒泡排序 数组逆序 数组求平均…

伺服驱动器算法入门的一些建议和书籍推荐

希望此篇文章对想从事伺服驱动器的研发工作的一些刚刚入门的同学一些建议。 针对伺服驱动器的研发工作涉及的知识和需要掌握的技能主要分为两部分&#xff0c;第一是原理部分、第二是工程实践部分。原理部分的学习在此主要推荐大家查看一些入门书籍&#xff0c;本文章中也对书籍…

【vue】导入组件

先行知识 用vite创建vue项目 1.导入组件 项目结构 App.vue中&#xff1a; 导入后&#xff0c;App.vue是Header.vue和Footer.vue的父组件 参考 https://www.bilibili.com/video/BV1nV411Q7RX

在js中计算两个时间段重叠的时长问题

文章目录 前言一、过程分析二、实现代码(js)总结 前言 最近遇到一个需求&#xff0c;就是在js中计算两段时间的重叠时长问题&#xff0c;这里记录一下。 一、过程分析 两段时间的重叠问题&#xff0c;一般有3中情况 两段时间完全无重叠&#xff0c;也就是无任何交集两段时间…

08 Php学习:if语句、Switch语句

PHP 条件语句 当您编写代码时&#xff0c;您常常需要为不同的判断执行不同的动作。您可以在代码中使用条件语句来完成此任务。 在 PHP 中&#xff0c;提供了下列条件语句&#xff1a; if 语句 - 在条件成立时执行代码 if…else 语句 - 在条件成立时执行一块代码&#xff0c;…

【树哈希】CF1182D Complete Mirror

CF1182D - Complete Mirror Description 给定一个 n n n 个点的无根树&#xff0c;求一个树根 r o o t root root,使得对于任意两个节点 v 1 , v 2 v_1,v_2 v1​,v2​&#xff0c;若满足 d i s t ( v 1 , r o o t ) d i s t ( v 2 , r o o t ) dist(v_1,root)dist(v_2,ro…

【CSS】SVG图片属性及修改颜色

最近的开发中遇到了SVG不能修改颜色的问题&#xff0c;以前是直接用&#xff0c;没有研究过&#xff0c;现在搞个笔记记录下 SVG的属性&#xff1a; width:设置最终SVG图片的宽度height:设置最终SVG图片的高度viewbox&#xff1a;视区&#xff0c;在svg上截取一块&#xff0c…

C++--用list容器处理约瑟夫环问题

约瑟夫环 约瑟夫环问题是一个经典的数学问题&#xff0c;描述如下&#xff1a; 假设有 n 个人站成一圈&#xff0c;编号从 1 到 n。从第一个人开始报数&#xff0c;报到 m 的人出列&#xff0c;然后下一个人继续从 1 开始报数&#xff0c;直到所有人都出列为止。问最后留下的…

013:vue3 Pinia详解使用详解

文章目录 1. Pinia 是什么2. Pinia 功能作用3. 手动添加Pinia到Vue项目4. Pinia基础使用5. getters实现6. action异步实现7. storeToRefs工具函数8. Pinia的调试9. 总结 1. Pinia 是什么 Pinia 是 Vue 的专属的 最新状态管理库是 Vuex 状态管理工具的替代品和 Vuex 一样为 Vue…

ESP-IDF移植lvgl 驱动 ST7789

文章目录 1 前言2 准备3 移植LVGL3.1 工程准备3.2 修改 CMakeLists.txt文件编译 LVGL3.3 编译LVGL 4 编译 ST7789 LCD驱动5 发现问题 1 前言 本教程开始学习 LVGL的&#xff0c;开始之前要把环境配置好&#xff0c;首先就需要移植 lvgl&#xff0c;使用的是 esp32 环境&#xf…

计算机网络——ARP协议

前言 本博客是博主用于复习计算机网络的博客&#xff0c;如果疏忽出现错误&#xff0c;还望各位指正。 这篇博客是在B站掌芝士zzs这个UP主的视频的总结&#xff0c;讲的非常好。 可以先去看一篇视频&#xff0c;再来参考这篇笔记&#xff08;或者说直接偷走&#xff09;。 …

Javaweb监听器(Listener)

一、概念 1.Listener表示监听器。是Javaweb三大组件&#xff08;Servlet、Filter、Listener&#xff09;之一 2.优先级为监听器>过滤器>servlet 3.监听器可以监听就是在application&#xff0c;session&#xff0c;request三个对象创建、销毁或者往其中添加修改删除属性…

MySQ数据库: MySQL数据库的安装配置 ,图文步骤详细,一篇即可完成安装完成! MySQL数据库如何与客户端连接

LiuJinTao&#xff1a; 2024年4月14日 文章目录 MySQL的安装配置1. 下载2. 安装 三、 MySQL 启动与停止1. 第一种 方式&#xff1a;2. 第二种方式&#xff1a; 四、MySQL 客户端连接2. 方式二&#xff1a; MySQL的安装配置 1. 下载 官方下载网址&#xff1a;https://www.mysq…

2024蓝桥杯省赛C++软件算法研究生组题解+游记

A题 给你一个音游的游戏记录log.txt&#xff0c;判断玩家的最高连击数 题解 水题&#xff0c;但是要小心&#xff0c;miss的键需要重置k0&#xff0c;超时但正确的键重置k1 个人答案是9 B题 计算1~2024041331404202中有多少个数x满足x! - x*(x1)/2能被100整除 题解 首先…