深入篇【C++】手搓模拟实现list类(详细剖析底层实现原理)模拟实现正反向迭代器【容器适配器模式】

news2024/9/23 7:32:21

深入篇【C++】手搓模拟实现list类(详细剖析底层实现原理)&& 模拟实现正反向迭代器【容器适配器模式】

  • Ⅰ.迭代器实现
      • 1.一个模板参数
      • 2.两个模板参数
      • 3.三个模板参数
  • Ⅱ.反向迭代器实现
      • 1.容器适配器模式
  • Ⅲ.list模拟实现
      • 1.定义结点
      • 2.封装结点
      • 3.构造/拷贝
      • 4.迭代器
      • 5.插入/头尾插
      • 6.删除/头尾删
      • 7.析构/清理
  • Ⅳ.整代码

Ⅰ.迭代器实现

1.一个模板参数

在模拟实现list之前,我们要理解list中的迭代器是如何实现的。
在vector中迭代器可以看成一个指针,指向vector中的数据。它的解引用会访问到具体的数据本身,++会移动到下一个数据位置上去,这些都是因为vector具有天生的优势:空间上是连续的数组,这样指针就是一个天生完美的迭代器。而list与vector不同,list在空间上并不连续,指针解引用访问到的也不是具体的数据,而是结点本身,指针++也不会移动到下一个结点位置,这些问题都说明list的迭代器不能简单是只是原生指针就可以完成。
正确的实现方法是:
将指向结点的原生指针封装起来,构造出一个自定义类型的迭代器。在这个迭代器里我们通过运算符重载,来改变原生指针的一些行为,比如解引用运算符重载,++运算符重载。这样我们就可以构造出一个满足预期的迭代器了。

使用原生指针作为迭代器不符合需求,迭代器的解引用和++都不符合list迭代器的要求
所以这里将原生指针进行封装,然后使用运算符重载达到我们想要的效果
所以list的迭代器是一个自定义类型,这个自定义类型里存着原生指针

    template <class T>//将这个迭代器搞成模板,适用于各种类型
	struct _list_iterator
	{
		typedef listNode<T> Node;//因为结点类被搞成了模板,所以名字很长,我们这里将其重命名为Node

		_list_iterator( Node* node)//用原生指针初始化
			:_node(node)
		{}
	   T& operator*()//重载*运算符因为原生指针中的解引用不符合list迭代要求
		{
			return _node->val;//要求解引用要访问到结点的的数据,而不是结点
		}
		_list_iterator<T>& operator++()//重载++运算符
		{
			_node = _node->next;//要求++要挪动到下一个结点的位置上去
			return *this;
		}
        T* operator->()
		{
			return &_node->val;
		}
		bool operator!=(const _list_iterator<T>& it)
		{
			return _node != it._node;//用原生指针比较即可
		}
		Node* _node;//底层封装的是原生指针
	};

2.两个模板参数

迭代器基本上已经完成,可以正常使用了。不过list中的迭代器实现并没有这么简单,它的模板参数实现给了三个参数,这里我们才有一个参数,接下来我会一一增加上去。第二个模板参数是什么呢?
第二个模板参数是为了实现const迭代器而设计的,const迭代器要求指向的内容不能被修改,而迭代器本身是可以进行修改的。那如何做到呢?
解引用访问到的就是数据本身,而const迭代器要求指向的数据不能被修改,所以直接让解引用运算符重载函数的返回值加上const修饰即可。

template <class T>//将这个迭代器搞成模板,适用于各种类型
	struct _list_iterator
	{
		typedef listNode<T> Node;

		_list_iterator( Node* node)//用原生指针初始化
			:_node(node)
		{}
	   const T& operator*()//const迭代器访问时,返回的是const修饰的数据无法被修改。
		{
			return _node->val;
		}
		_list_iterator<T>& operator++()//重载++运算符
		{
			_node = _node->next;
			return *this;
		}
        T* operator->()
		{
			return &_node->val;
		}
		bool operator!=(const _list_iterator<T>& it)
		{
			return _node != it._node;
		}
		Node* _node;//底层封装的是原生指针
	};

有的人会选择拷贝一份封装的迭代器,然后其他都相同,就解引用运算符重载函数返回类型不同,普通迭代器,返回值是T&,const迭代器返回类型是constT&.但这样做实在太冗余了,大佬是再添加一个模板参数来控制这里的迭代器返回类型的。
所以第二个模板参数是为了控制解引用运算符重载函数的返回值类型的。

有的人会这样做:直接给迭代器前面加上const,这样的做法是不对的!
我们要求的const迭代器是:1.迭代器指向的内容不能被修改2.迭代器本身可以修改
1.const T* iterator 2.T* const iterator 这两种情况应该是第一种情况满足要求。
typedef const _list_iterator const_iterator;这种情况是第二种情况const修饰的是这个封装的迭代器,说明是这个迭代器不能被修改,而不是它指向的内容不能修改,

   
	template <class T,class Ref>//定义两个模板参数,第二个Ref是为了控制*运算符重载函数的返回值,以达到传使用const迭代器,返回const T&   使用正常迭代器返回T&
	struct _list_iterator
	{
		typedef listNode<T> Node;//将结点重命名
		typedef _list_iterator<T, Ref> Self;//将迭代器重命名为Self
		//为什么要重命名?因为加上模板后太长了,重命名一个简洁的名字
		_list_iterator(Node* node)
			:_node(node)
		{}
		Ref operator*()//将这里的返回值改成模板Ref,这样就可以通过模板参数来控制返回值不同了
		{
			return _node->val;
		}
		T* operator->()
		{
			return &_node->val;
		}
		Self& operator++()//重载++运算符//这里的Self就是迭代器_list_iterator<T, Ref>
		{
			_node = _node->next;
			return *this;
		}
		Self& operator++(int)//后置++运算符//这里的Self就是迭代器_list_iterator<T, Ref>
		{
			Node* temp(this);

			_node = _node->next;
			return *temp;
		}
		bool operator!=(const Self& it)//这里的Self就是迭代器_list_iterator<T, Ref>
		{
			return _node != it._node;
		}
		Node* _node;//原生指针
	};

3.三个模板参数

迭代器的第三个模板参数是什么呢?它又是干什么的呢?我们首先要模拟一个场景,一个类的成员变量是允许我们访问的(公有的),那迭代器类似一个指针,指针就可以通过->来访问到指针指向的内容。那这样我们就可以通过迭代器的->访问到数据,而不是解引用。

        T* operator->()
		{
			return &_node->val;
		}

不过这里的箭头运算符重载就有点奇怪了,it->val 就是等于it.operator->().返回的是T* ,T*是怎么访问到val的呢?这里明显少了一个箭头,正确的写法应该是这样it->->val,但编译器觉得太挫了,将一个箭头省略了,编译器为了简洁好看,会自动忽略这个问题。

list<int>::iterator it = lt.begin();
		while (it != lt.end())
		{
			cout << (it->val) << " ";
			++it;
			//因为it是自定义类型,自定义类型++就会去调用它的运算符重载
		}
		cout << endl;

第三个模板参数也是为了应对const迭代器而设计的,只不过这个模板参数控制的是箭头运算符重载函数的返回值类型,因为const迭代器使用->运算符后,返回的应该是const T类型,而正常迭代器使用->运算符后,返回的应该是T

template <class T, class Ref,class Ptr>//三个模板参数,第二个控制解引用运算符重载函数的返回值类型
//第三个参数用来控制箭头运算符重载函数的返回值类型。这样就可以通过用户传什么类型的迭代器,模板自动生成什么样子的迭代器给他使用。
	struct _list_iterator
	{
		typedef listNode<T> Node;//将结点重命名
		typedef _list_iterator<T, Ref,Ptr> Self;//将迭代器重命名,不重命名的话,加上模板,太长了。
		_list_iterator(Node* node)
			:_node(node)
		{}
		Ref operator*()//重载*运算符因为原生指针中的解引用不符合list迭代要求
		{
			return _node->val;
		}
		Ptr operator->()  //const对象使用 返回的应该是const T *, 正常对象使用返回的是T*
		{
			return &_node->val;
		}
		Self& operator++()//重载++运算符//Self就是迭代器_list_iterator<T, Ref,Ptr>
		{
			_node = _node->next;
			return *this;
		}
		Self& operator++(int)//后置++运算符
		{
			Self* temp(this);

			_node = _node->next;
			return *temp;
		}
		Self& operator--()//重载--运算符
		{
			_node = _node->prev;
			return *this;
		}
		Self& operator--(int)//后置--运算符
		{
			Self* temp(this);

			_node = _node->prev;
			return *temp;
		}
		bool operator!=(const Self& it)
		{
			return _node != it._node;
		}
		Node* _node;//原生指针
	};

Ⅱ.反向迭代器实现

1.容器适配器模式

反向迭代是如何实现的呢?设计的原理就是适配器模式,将正向迭代器封装起来,然后通过函数重载来完成反向迭代器的一系列操作,这个模式牛逼之处就在于你传任何一个类型的正向迭代器,它都会给你适配出它的反向迭代器,就这么牛。反向迭代器的一些操作与正向迭代器相反,比如反向迭代器的++就是调用正向迭代器的–,反向迭代器的–就是调用正向迭代器的++。而解引用应该是一样的,都是访问具体的数据。
不过要注意的是反向迭代器的解引用操作与正向迭代不同,这是因为标准库里采用了一种镜像对称的方案。
什么意思呢?
正常来说,begin()是指向第一个位置的迭代器,end()是指向最后一个数据下一个位置的迭代器。
rbegin()指向的是最后一个数据位置的迭代器,rend()是指向第一个数据前面的位置的迭代器。
在这里插入图片描述
而镜像对称就是要让正迭代器和反迭代器的位置是对称的。在这里插入图片描述
这样反向迭代器的rbegin()就可以直接用正向迭代器的end()初始化,rend()就可以直接用正向迭代器begin()初始化。而弄成这样的代价就是让解引用运算符重载函数来承担了,解引用的不是当前位置的数据,而是前一个位置上的数据的,这样就可以让反向迭代器正确的解引用到数据了。


template <class Iterator,class Ref,class Ptr>

struct ReserveIterator
{
	typedef ReserveIterator<Iterator, Ref, Ptr> Self;
	ReserveIterator(Iterator it)
		:_it(it)
	{}//用正向迭代器初始化
	Ref operator*()//解引用,解的是前一个位置,保持对称
	{
		Iterator tmp = _it;
		return *(--tmp);
		//正向迭代器的模板类型是  class<T ,T& ,T*>
	}
	Self& operator++()
	{
		--_it;
		return *this;
	}
	Self& operator--()
	{
		++_it;
		return *this;
	}
	bool operator!=(Self it)
	{
		return _it != it._it;
	}
	Iterator _it;//底层封装一个正向迭代器

};

Ⅲ.list模拟实现

首先先搭出一个简易的list类模型,再一步一步完善。

1.定义结点

链表是由一个个的结点连接而成,所以第一步先构造出一个结点类。
这个结点包含前后指针和对应的数据。

template <class T>
	struct listNode
	{
		listNode<T>* next;
		listNode<T>* prev;
		T val;
		listNode(const T& val = T())
			:next(nullptr)
			, prev(nullptr)
			, val(val)
		{}
	};

2.封装结点

接下来就是将结点封装到list类里面,用一个个结点来实现list的各个功能了。
我们实现的list是一个带头双向循环链表,所以第一步链表的构造就必须是双向循环的模式。
因为封装的结点是一个类模板,所以我们习惯给它用typedef重命名为一个简洁的名字。

3.构造/拷贝

template <class T>
	class list//带头双向循环列表
	{
	public:
		typedef listNode<T> Node;

		list()//构造函数
		{
			_head = new Node;
			//首先给头结点开辟空间
			_head->prev = _head;
			//让这个头结点的前后指针都指向自己,构造出一个循环链表
			_head->next = _head;
			sz = 0;
		}
		list(const list<T>& lt1)//拷贝构造--->深拷贝
		{
			_head = new Node;//首先还是构造出一个循环链表模型
			_head->prev = _head;
			_head->next = _head;
			sz = 0;

			for (auto e : lt1)//然后将要拷贝的对象的一个个数据全部尾插进来
			{
				push_back(e);//push_back这里还没有实现,在下面会实现知道原理即可
			}
		}
		void swap( list<T>& lt1)
		{
			std::swap(_head, lt1._head);
			std::swap(sz, lt1.sz);
		}
		list<T>& operator=( list<T>&lt1)//赋值运算符重载---现代写法
		{
			swap(lt1);
			return *this;
		}
		private:
		Node* _head;//封装的是一个指向结点的指针
		//封装的是指向头结点的指针
		size_t sz;//大小
};

4.迭代器

迭代器我们上面已经实现完毕,这里就可以直接使用了。

       typedef _list_iterator<T, T&,T*> iterator;//普通迭代器		
	typedef _list_iterator<T, const T&,const T*> const_iterator;//const迭代器
	
	typedef ReserveIterator<iterator, T&, T*> reserve_iterator;//反向迭代器
    typedef ReserveIterator<const_iterator, T&, T*> reserve_iterator;//反向const迭代器

		reserve_iterator rbegin()//反向迭代器要保持镜像对称,用正向迭代器的end()构造rbegin()
		{
			return reserve_iterator(end());
		}
		reserve_iterator rend()//用正向迭代器的begin()构造rend()
		{
			return reserve_iterator(begin());
		}
		
		reserve_iterator rbegin()const
		{
			return reserve_iterator(end());
		}
		reserve_iterator rend()const 
		{
			return reserve_iterator(begin());
		}
		
		iterator begin()
		{
			return _head->next;
			//return _list_iterator(_head->next)
			//单参数的构造支持隐式类型转化
		}
		iterator end()
		{
			return _head;
		}
		const_iterator begin()const
		{
			return _head->next;
			//return _list_iterator(_head->next)
			//单参数的构造支持隐式类型转化
		}
		const_iterator end()const
		{
			return _head;
		}

5.插入/头尾插

      iterator insert(iterator pos,const T& x)
		{
             //最好还是转化成结点的指针,这里访问迭代器不方便,这里就体现了为什么要用struct来定义迭代器让其成员共有
			Node* cur = pos._node;
			Node* prev = cur->prev;

			Node* newnode = new Node(x);

			prev->next = newnode;
			newnode->prev = prev;
			
			newnode->next = cur;
			cur->prev = newnode;
			sz++;
			return newnode;//返回新插入结点的位置
		}
		void push_back(const T& x)
		{
			尾插首先需要找到尾
			//Node* tail = _head->prev;
			找到尾部后将新结点连接
			//Node* newnode = new Node(x);
			//tail->next = newnode;
			//newnode->prev = tail;

			//_head->prev = newnode;
			//newnode->next = _head;
			insert(end(), x);//可以直接复用insert
		}
		void push_front(const T& x)
		{
			insert(begin(),x);//可以直接复用insert
		}

6.删除/头尾删

        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;
			sz--;
			return next;
		}
		void pop_back()
		{
			erase(--end());//直接复用erase删除尾部位置
		}
		void pop_front()//直接复用erase删除第一个数据
		{
			erase(begin());
		}

7.析构/清理

	
		void clear()//清空数据,但不清哨兵位
		{
			iterator it = begin();
			while (it != end())
			{
				it=erase(it);//删除完后会返回下一个位置的迭代器
			
			}
			sz = 0;
		}
		~list()//析构,全部清除
		{
			clear();
			delete _head;
			_head = nullptr;
		}

Ⅳ.整代码

#pragma once
#include<iostream>
#include <stdio.h>
#include <assert.h>
#include "ReserveIterator.h"
using namespace std;
namespace tao
{

	template <class T>
	struct listNode
	{
		listNode<T>* next;
		listNode<T>* prev;
		T val;
		listNode(const T& val = T())
			:next(nullptr)
			, prev(nullptr)
			, val(val)
		{}
	};
	template <class T, class Ref,class Ptr>
	struct _list_iterator
	{
		typedef listNode<T> Node;
		typedef _list_iterator<T, Ref,Ptr> Self;
		_list_iterator(Node* node)
			:_node(node)
		{}
		Ref operator*()//重载*运算符因为原生指针中的解引用不符合list迭代要求
		{
			return _node->val;
		}
		Ptr operator->()  //const对象使用 返回的应该是const T *
		{
			return &_node->val;
		}
		Self& operator++()//重载++运算符
		{
			_node = _node->next;
			return *this;
		}
		Self& operator++(int)//后置++运算符
		{
			Self* temp(this);

			_node = _node->next;
			return *temp;
		}
		Self& operator--()//重载--运算符
		{
			_node = _node->prev;
			return *this;
		}
		Self& operator--(int)//后置--运算符
		{
			Self* temp(this);

			_node = _node->prev;
			return *temp;
		}
		bool operator!=(const Self& it)
		{
			return _node != it._node;
		}
		Node* _node;//原生指针
	};

    template <class Iterator,class Ref,class Ptr>
//反向迭代器
     struct ReserveIterator
   {
	typedef ReserveIterator<Iterator, Ref, Ptr> Self;
	ReserveIterator(Iterator it)
		:_it(it)
	{}//用正向迭代器初始化
	Ref operator*()//解引用,解的是前一个位置,保持对称
	{
		Iterator tmp = _it;
		return *(--tmp);
		//正向迭代器的模板类型是  class<T ,T& ,T*>
	}
	Self& operator++()
	{
		--_it;
		return *this;
	}
	Self& operator--()
	{
		++_it;
		return *this;
	}
	bool operator!=(Self it)
	{
		return _it != it._it;
	}
	Iterator _it;//底层封装一个正向迭代器

    };

	//容器适配器模式 --->反向迭代器--给我正向迭代器我给你适配出反向迭代器,针对任何容器都可以,只要给我正向迭代器就可以适配出反向迭起


	template <class T>
	class list//带头双向循环列表
	{
	public:
		typedef listNode<T> Node;
		//typedef _list_iterator<T> iterator;//将自定义的迭代器名字统一命名为iterator
		//typedef _list_iterator<T,T&> iterator;//普通迭代器
		typedef _list_iterator<T, T&,T*> iterator;//普通迭代器
		//typedef _list_iterator<T, const T&> const_iterator;//const迭代器
		typedef _list_iterator<T, const T&,const T*> const_iterator;//const迭代器
		//const迭代器如何设计?
		//我们要求的const迭代器是:1.迭代器指向的内容不能被修改2.迭代器本身可以修改
		//1.const T* iterator     2.T* const iterator
		//typedef const _list_iterator<T> const_iterator;这种情况是第二种情况const修饰的是这个封装的迭代器,说明是这个迭代器不能被修改,而不是它指向的内容不能修改

		//正确的做法是,解引用时访问到数据,返回时返回const T&类型的数据,这样返回回来的数据就无法再被修改
		//有的人会选择拷贝一份封装的迭代器,然后其他都相同,就解引用运算符重载函数返回类型不同,普通迭代器,返回值是T&,const迭代器返回类型是constT&.
		//但这样做实在太冗余了,大佬是再添加一个模板参数来控制这里的迭代器返回类型的。

		typedef ReserveIterator<iterator, T&, T*> reserve_iterator;

		reserve_iterator rbegin()
		{
			return reserve_iterator(end());
		}
		reserve_iterator rend()
		{
			return reserve_iterator(begin());
		}
		iterator begin()
		{
			return _head->next;
			//return _list_iterator(_head->next)
			//单参数的构造支持隐式类型转化
		}
		iterator end()
		{
			return _head;
		}
		const_iterator begin()const
		{
			return _head->next;
			//return _list_iterator(_head->next)
			//单参数的构造支持隐式类型转化
		}
		const_iterator end()const
		{
			return _head;
		}
		list()
		{
			_head = new Node;
			_head->prev = _head;
			_head->next = _head;
			sz = 0;
		}
		list(const list<T>& lt1)
		{
			_head = new Node;
			_head->prev = _head;
			_head->next = _head;
			sz = 0;

			for (auto e : lt1)
			{
				push_back(e);
			}
		}
		void swap( list<T>& lt1)
		{
			std::swap(_head, lt1._head);
			std::swap(sz, lt1.sz);
		}
		list<T>& operator=( list<T>&lt1)
		{
			swap(lt1);
			return *this;
		}
		void push_front(const T& x)
		{
			insert(begin(),x);
		}
		void pop_front()
		{
			erase(begin());
		}
		void push_back(const T& x)
		{
			尾插首先需要找到尾
			//Node* tail = _head->prev;
			找到尾部后将新结点连接
			//Node* newnode = new Node(x);
			//tail->next = newnode;
			//newnode->prev = tail;

			//_head->prev = newnode;
			//newnode->next = _head;
			insert(end(), x);
		}
		void pop_back()
		{
			erase(--end());
		}
		iterator insert(iterator pos,const T& x)
		{
             //最好还是转化成结点的指针,这里访问迭代器不方便,这里就体现了为什么要用struct来定义迭代器让其成员共有
			Node* cur = pos._node;
			Node* prev = cur->prev;

			Node* newnode = new Node(x);

			prev->next = newnode;
			newnode->prev = prev;
			
			newnode->next = cur;
			cur->prev = newnode;
			sz++;
			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;
			sz--;
			return next;
		}
		size_t size()
		{
			return sz;
		}
		~list()
		{
			clear();
			delete _head;
			_head = nullptr;
		}
		void clear()//清空数据,但不清哨兵位
		{
			iterator it = begin();
			while (it != end())
			{
				it=erase(it);
			
			}
			sz = 0;
		}
	private:
		Node* _head;//封装的是一个指向结点的指针
		size_t sz;
	};
	
};


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

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

相关文章

【Python】Web学习笔记_flask(1)——模拟登录

安装flask pip3 install flask 第一部分内容&#xff1a; 1、主页面输出hello world 2、根据不同用户名参数输出用户信息 3、模拟登录 from flask import Flask,url_for,redirectappFlask(__name__)app.route(/) def index():return hello worldapp.route(/user/<uname…

linux_进程状态

目录 一. 概念铺设 状态是什么&#xff1f; 传统操作系统的状态转换图 二. 传统操作系统状态 1. 运行 2. 阻塞 3. 挂起 三. linux 中的进程状态 1. 总体介绍 2. R 3. S 4. D kill -9 D vs S 5. T kill T vs S 6. Z 什么是僵尸状态&#xff1f; 僵尸进程的危害 …

hadoop部署配置

端口名称 Hadoop2.x Hadoop3.x NameNode内部通信端口 8020 / 9000 8020 / 9000/9820 NameNode HTTP UI 50070 9870 MapReduce查看执行任务端口 8088 8088 历史服务器通信端口 19888 19888 端口名称Hadoop2.xHadoop3.xNameNode内部通信端口8020 / 90008020 / 9000/9820NameNode…

延长周末,获得高质量休息:工作与学习党的生活策略

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

【Linux】多线程的补充

1 线程安全的单例模式 1.1 什么是单例模式 单例模式是一种 "经典的, 常用的, 常考的" 设计模式. 1.2 什么是设计模式 IT行业这么火, 涌入的人很多. 俗话说林子大了啥鸟都有. 大佬和菜鸡们两极分化的越来越严重. 为了让菜鸡们不太拖大佬的后腿, 于是大佬们针对一些…

从源码角度配合网络编程函数API 分析下 三握手四挥手都做了什么

首先我们先说下网络编程API&#xff1a; 数据在网络上通信&#xff0c;通信的双方一个是 客户端&#xff0c; 一个是 服务器 更具体来说&#xff0c;不是 客户端和服务器这两个机器在 经由互联网 进行通信&#xff0c; 而是 客户端上的某一进程 与 服务器端的某一进程 进…

Vue2 第七节 Vue监测数据更新原理

&#xff08;1&#xff09;Vue会监视data中所有层次的数据 &#xff08;2&#xff09;如何监测对象中的数据 通过setter实现监视&#xff0c;且要在new Vue时传入要监测的数据对象中后追加的属性&#xff0c;Vue默认不做响应式处理如果要给后添加的属性做响应式&#xff0c;使…

【笔记】PyTorch DDP 与 Ring-AllReduce

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhang.cn] 文内若有错误&#xff0c;欢迎指出&#xff01; 今天我想跟大家分享的是一篇虽然有点老&#xff0c;但是很经典的文章&#xff0c;这是一个在分布式训练中会用到的一项技术&#xff0c; 实际上叫ringallreduce。 …

Hyper-v 设置静态IP 搭建集群

背景 最近想在本机WIN11上创建几个Centos用于做几个试验&#xff0c;之前一直用VMWare&#xff0c;需要安装额外的软件&#xff0c;正好win自带虚拟机功能&#xff0c;只需要在功能中安装Hyper-v就可以使用。 新建虚拟机 虚拟机交换器 Hyper-V 虚拟交换机是基于软件的第 2 层…

P5691 [NOI2001] 方程的解数

题目 思路 暴搜显然会TLE&#xff0c;所以这时候就应该请出DFS的伙伴——折半搜索&#xff08;meet in the middle&#xff09;了 折半搜索的思路就是先搜完后一半后&#xff0c;借助这一半的数据来搜索前一半&#xff0c;效率是原来的2倍 这个题怎么才能折半搜索呢&#xff1…

链表OJ题目1 (移除链表元素)

力扣&#xff08;链接放这里喽&#xff09; 先贴代码再做讲解&#xff1a; struct ListNode* removeElements(struct ListNode* head, int val) {struct ListNode* cur head;struct ListNode* tail NULL;while(cur){if(cur->val val){if(cur head){head head->next…

【点云处理教程】01如何创建和可视化点云

一、说明 本文是系列教程&#xff0c;专门介绍点云处理的全流程&#xff0c;是一个入门工具。“点云处理”教程对初学者友好&#xff0c;我们将在其中简单地介绍从数据准备到数据分割和分类的点云处理管道。 第1条&#xff1a;点云处理简介文章2&#xff1a;在Python中从深度图…

Python入门【可变参数、lambda表达式和匿名函数、eval()函数、递归函数、嵌套函数(内部函数)、 nonlocal关键字】(十二)

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是爱敲代码的小王&#xff0c;CSDN博客博主,Python小白 &#x1f4d5;系列专栏&#xff1a;python入门到实战、Python爬虫开发、Python办公自动化、Python数据分析、Python前后端开发 &#x1f4e7;如果文章知识点有错误…

怎么对XMind思维导图加密?

在现代社会中&#xff0c;信息安全变得越来越重要。对于那些使用XMind思维导图来组织和管理重要信息的人来说&#xff0c;保护思维导图中的内容免受未经授权的访问变得至关重要。本文将介绍如何加密XMind思维导图&#xff0c;以确保您的信息安全。 什么是XMind思维导图&#x…

Qt中postevent造成内存泄漏问题的通用解决方案

在Qt中由QCoreApplication统一管理Qt事件的收发和销毁,其中sendEvent为阻塞式发送,用于单线程的事件发送;postevent为非阻塞式发送,构造事件的线程和接受事件的线程可以为两个线程。 最近在做一个个人项目ShaderLab 需要绘制OpenGL实时渲染的图像,由于OpenGL渲染基本都放…

Flowable-服务-消息任务

文章目录 定义图形标记XML内容集成Rabbitmq引入pom包配置rabbitmq 操作界面 定义 Mq 任务不是 BPMN 2.0 规范定义的官方任务&#xff0c;在 Flowable 中&#xff0c;Mq 任务是作为一种特殊的服务 任务来实现的&#xff0c;主要做Mq消息发送。 图形标记 由于 Mq 任务不是 BPM…

openGauss学习笔记-25 openGauss 聚集函数

文章目录 openGauss学习笔记-25 openGauss 聚集函数25.1 sum(expression)25.2 max(expression)25.3 min(expression)25.4 avg(expression)25.5 count(expression)25.6 count(*)25.7 delta25.8 mode() within group (order by value anyelement) openGauss学习笔记-25 openGauss…

Vue『卡片拖拽式课程表』

Vue『卡片拖拽式课程表』 概述 在本篇技术博客中&#xff0c;我们将介绍一个使用Vue实现的『卡片拖拽式课程表』。这个课程表允许用户通过拖拽课程卡片来安排不同的课程在时间表上的位置。我们将逐步讲解代码实现&#xff0c;包括课程表的布局、拖拽功能的实现&#xff0c;以…

mac使用brew切换node的版本号

借用的是这篇文章 》 mac 使用brew切换node版本 # 查看node的版本号 brew search node# 安装其他版本. 选择 node16版本 brew install node16# 如果你是第一次安装node的话&#xff0c;执行下面这个操作 brew link --overwrite --force node16# 如果你是第二次安装node的话&am…

一文教会你单向链表

目录 一、什么是链表&#xff1f; 1.链表的定义 2.链表的实现 2.1链表的定义 2.2创建一个链表 二、链表的各个接口 1.创建节点 2.头插(将新创建的节点作为头插入到链表中) 3.打印链表 4.尾插(将新创建的节点插入到链表的末端) 5.头删 6.尾删 7.查找 8.删除指定节点位…