C++:一次性搞定vector模拟实现中必须关注的细节

vector模拟实现的细节

  • 1. vector的模拟实现源码
  • 2. 重要接口注意事项
    • 2.1 const修饰
    • 2.2 begin()和end()
    • 2.3 构造函数
      • (1)迭代器区间初始化
      • (2)构造函数冲突问题
        • 发现​问题
        • 分析问题
        • ​解决问题
      • (3)特殊的构造函数的实现
    • 2.4 insert
      • 注意点
      • 分析原因
      • 图解
    • 2.5 push_back
    • 2.6 reserve
      • 发现问题
      • 分析问题
      • 解决问题
      • 图解
    • 2.7 resize
    • 2.8 swap
  • 3. vector的迭代器失效
    • 3.1 insert造成的迭代器失效
      • 案例:往容量已满的vector容器中插入值(实现时默认为4个空间)
      • 分析问题
      • 解决方案
    • 3.2 erase造成的迭代器失效
      • 案例:删除值为偶数的位置
      • 图解
      • 分析问题
      • 解决问题
      • 总结

1. vector的模拟实现源码

vector本质是顺序表,因此其迭代器可以理解为原生指针。
我们通过原生指针来模拟实现vector.

namespace B
{
	template<class T>
	class vector
	{
	public:
		typedef T* iterator;
		typedef const T* const_iterator;

		//Iterators
		iterator begin()
		{
			return _start;
		}

		iterator end()
		{
			return _finish;
		}

		const_iterator begin() const
		{
			return _start;
		}

		const_iterator end() const
		{
			return _finish;
		}

		//constructor and destructor
		vector()
			:_start(nullptr), _finish(nullptr), _endofstorage(nullptr)
		{ }

		vector(initializer_list<T> il)
		{
			//const T* it = il.begin();
			//while (it != il.end())
			//{
				//push_back(*it);
				//it++;
			//}

			//il本质是一个类,有begin()和end(),因此也可以使用迭代器
			reserve(il.size());
			for (auto& e : il)
			{
				push_back(e);
			}
		}

		vector(size_t n, const T& val = T())
		{
			//_start = new T[n];
			//_finish = _endofstorage = _start + n;

			//复用reserve
			reserve(n);
			for (size_t i = 0; i < n; i++)
				push_back(val);
		}

		vector(int n, const T& val = T())
		{
			reserve(n);
			for (size_t i = 0; i < n; i++)
				push_back(val);
		}

		template<class InputIterator>
		vector(InputIterator first, InputIterator last)
		{
			while (first != last)
			{
				push_back(*first);
				first++;
			}
		}

		vector(const vector<T>& v)
		{
			reserve(v.capacity());
			for (auto& e : v)
			{
				*_finish = e;
				_finish++;
			}
		}

		//现代写法
		vector<T>& operator= (vector<T> v)
		{
			swap(v);
			return *this;
		}

		~vector()
		{
			delete[] _start;
			_start = _finish = _endofstorage = nullptr;
		}

		//capacity
		bool empty() const
		{
			return _finish == _start;
		}

		size_t size() const
		{
			return _finish - _start;
		}

		size_t capacity() const
		{
			return _endofstorage - _start;
		}

		void resize(size_t n , const T& val = T())
		{
			if (n > size())
			{
				reserve(n);
				for (size_t i = size(); i < n; i++)
					push_back(val);
			}
			else
			{
				//for (int i = size(); i > n; i--)
				//	pop_back();

				//我们可以直接改变指针
				_finish = _start + n;
			}

		}

		void reserve(size_t n)
		{
			//不会缩容

			//错误写法
			//错因:我们的_start改变了 但是我们的_finsh和_endofstorage还没有改变
			//在调用size()时会出错 因为我们size()函数求大小时使用了_finish
			//if (n > capacity)
			//{
			//	T* tmp = new T[n];
			//	memcpy(tmp, _start, sizeof(T) * size());
			//	delete[] _start;

			//	_start = tmp;
			//	_finish = _start + size();
			//	_endofstorage = _start + n;
			//}

			//正确
			if (n > capacity())
			{
				size_t oldSize = size();
				T* tmp = new T[n];
				//memcpy(tmp, _start, sizeof(T) * size());
				//如果使用memcpy,对于类似vector<string>这样的类,就会出现浅拷贝的现象从而报错
				for (size_t i = 0; i < oldSize; i++)
				{
					tmp[i] = _start[i];
				}
				delete[] _start;

				_start = tmp;
				_finish = _start + oldSize;
				_endofstorage = _start + n;
			}
		}

		//Element access
		T& operator[](size_t pos)
		{
			return *(_start + pos);
		}

		const T& operator[](size_t pos) const
		{
			return *(_start + pos);
		}

		//Modify
		void push_back(const T& x)
		{
			//扩容
			//if (_finish == _endofstorage)
			//{
			//	size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
			//	reserve(newcapacity);
			//}
			//*_finish = x;
			//_finish++;

			//复用insert()
			insert(end(), x);
		}

		void pop_back()
		{
			assert(!empty());
			//_finish--;

			//复用erase()
			erase(end() - 1);
			
		}

		void swap(vector<T>& v)
		{
			//注意:不能直接写成swap(_start,v._start);
			//swap(_start, v._start);
			std::swap(_start, v._start);
			std::swap(_finish, v._finish);
			std::swap(_endofstorage, v._endofstorage);
		}

		iterator insert(iterator pos, const T& x)
		{
			assert(pos >= begin() && pos <= end());
			size_t ret = pos - _start;
			if (_finish == _endofstorage)
			{
				size_t len = pos - _start;
				size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newcapacity);

				//必须要更新pos--->迭代器失效
				pos = _start + len;
			}
			iterator cur = _finish;

			//移动数据
			// _start         pos                 _finish(tmp)
			//    1      2     3     4      5
			while (cur > pos)
			{
				*cur = *(cur - 1);
				cur--;
			}
			*cur = x;
			_finish++;

			return _start + ret;
		}

		iterator erase(iterator pos)
		{
			assert(pos >= begin() && pos < end());
			//移动数据
			// _start         pos                 _finish
			//    1      2     3     4      5
			size_t len = pos - _start;
			iterator cur = pos;
			while (cur < _finish - 1)
			{
				*cur = *(cur + 1);
				cur++;
			}
			_finish--;
			return pos;
		}

		void PrintVector()
		{
			for (auto& e : *this)
			{
				cout << e << " ";
			}
			cout << endl;
		}

	private:
		iterator _start = nullptr;
		iterator _finish = nullptr;
		iterator _endofstorage = nullptr;
	};
}

2. 重要接口注意事项

2.1 const修饰

对于不修改成员变量的函数,如size(),capacity(),需要加上const,否则非const成员不能使用。

2.2 begin()和end()

注意对于常量型也必须写成begin()和end(),不能写成cbegin()和cend(),因为它们是C++11之后为了完整性新增加的,但是范围for迭代器只能替换begin()和end()。

实现代码:

		//Iterators
		iterator begin()
		{
			return _start;
		}

		iterator end()
		{
			return _finish;
		}

		const_iterator begin() const
		{
			return _start;
		}

		const_iterator end() const
		{
			return _finish;
		}

2.3 构造函数

(1)迭代器区间初始化

迭代器区间初始化,使用模板的目的是为了不只是vector类的迭代器区间可以使用,string等其他容器的迭代器区间也可以使用。通过模板,无论我们传入的是哪一个容器的迭代器,都可以进行初始化,比如使用库中的list等。

示例:
我们使用string的迭代器来对vector进行初始化。
在这里插入图片描述

实现代码:

template<class InputIterator>
vector(InputIterator first, InputIterator last)
{
	while (first != last)
	{
		push_back(*first);
		first++;
	}
}

(2)构造函数冲突问题

发现​问题

我们在使用如下代码想要初始化时,编译器报错。

void testvector4()
{
	//会报错
	vector<int> v(10, 1);
	v.PrintVector();
	
	//不会报错
	//vector<int> v(10u, 1);
	//v.PrintVector();
}

在这里插入图片描述

分析问题

我们想要调用的是第一个构造函数,但是编译器调用了第二个,调用到了我们所写的模板函数中去,由于int是内置类型,push_back时无法进行解引用,因此报错。
在这里插入图片描述
如果我们在10后面加上u,仅仅是传参的类型改变,但是却不会报错,程序正确执行,那么因此我们可以确定是类型引发的问题。

10和1这种内置类型如果我们没有额外说明,那么默认都为int类型,因此两个int类型,符合模板构造函数,而我们想要调用的构造函数由于n的类型为size_t,虽然两个int类型也可以调用,但是需要类型提升,比较麻烦,因此编译器在有两个函数参数类型相同的情况下,选择了调用函数模板实例化的函数。

​解决问题

==​因为编译器对于同名函数且参数类型相同的函数调用的规则是:有现有的函数调用现有的,没有现有的函数调用函数模板实例化出来的函数。==因此我们可以进行函数重载,重载一份n为int类型的函数,这样的话编译器就会先调用已经实现的函数啦。

实现代码:

vector(int n, const T& val = T())
{
	reserve(n);
	for (size_t i = 0; i < n; i++)
		push_back(val);
	}

vector(size_t n, const T& val = T())
{
	reserve(n);
	for (size_t i = 0; i < n; i++)
		push_back(val);
}

template<class InputIterator>
vector(InputIterator first, InputIterator last)
{
	while (first != last)
	{
		push_back(*first);
		first++;
	}
}

(3)特殊的构造函数的实现

​我们在OJ中经常能看到这种用法:

vector<int> v = { 1, 2, 3, 4 ,5};

而我们自己模拟实现的vector则不支持,会报错。

在这里插入图片描述
通过查阅文档,了解到这种用法是在C++11以后新增加的用法。
其中initializer list是一个类模板。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
通过该构造函数进行初始化,在内部使用其迭代器进行初始化。
在这里插入图片描述
实现代码:

vector(initializer_list<T> il)
{
	//方法一:
	//const T* it = il.begin();
	//while (it != il.end())
	//{
		//push_back(*it);
		//it++;
	//}
	
	//方法二:
	//il本质是一个类,有begin()和end(),因此也可以使用迭代器
	reserve(il.size());
	for (auto& e : il)
	{
		push_back(e);
	}
}

2.4 insert

注意点

如果扩容,不更新pos的位置,会造成内部的迭代器失效问题。

分析原因

如果空间容量不足,那么会进行扩容,而扩容之后由于reserve内部实现了_start 和 _finish的更新,但是pos的迭代器位置错误造成迭代器失效仍然是一个问题。

图解

在这里插入图片描述

代码实现:

iterator insert(iterator pos, const T& x)
{
	assert(pos >= begin() && pos <= end());
	size_t ret = pos - _start;
	if (_finish == _endofstorage)
	{
		size_t len = pos - _start;
		size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
		reserve(newcapacity);

		//必须要更新pos--->迭代器失效
		pos = _start + len;
	}
	iterator cur = _finish;

	//移动数据
	// _start         pos                 _finish(tmp)
	//    1      2     3     4      5
	while (cur > pos)
	{
		*cur = *(cur - 1);
		cur--;
	}
	*cur = x;
	_finish++;

	return _start + ret;
}

2.5 push_back


注意点:

函数参数要加const,否则会因为权限放大而报错。
​vector<int> v;
​v.push_back(1);//1为常量,具有const属性,如果参数不加const则无法使用,会报错

实现代码:

void push_back(const T& x)
{
	//原始写法
	//扩容
	//if (_finish == _endofstorage)
	//{
	//	size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
	//	reserve(newcapacity);
	//}
	//*_finish = x;
	//_finish++;

	//复用insert()
	insert(end(), x);
}

2.6 reserve

注意事项:

1.memcpy是按照字节数量进行拷贝的---->在使用时会出现问题 如果是有深拷贝的类 会出错
2.扩容的时候提前保留size--->如果后面_finish直接使用_start + size(),size()在计算时使用的是已经释放空间的_finish,而_start指向的是新空间的开始,因此会报错。
3._endofStorge改变时,不能直接让其等于n,因为类型不同

发现问题

如果对于vector< string >这样的容器,由于string类内部也有申请空间,如果还是使用memcpy就会报错。

分析问题

我们在reserve()的时候重新new空间只是对于vector进行了深拷贝,但是由于string也是一个需要深拷贝的类,我们直接使用memcpy拷贝的是字节,属于浅拷贝,即新旧空间中的str指向同一块地址,而旧空间free之后会把那里的空间释放掉,因此程序会出错。

解决问题

直接使用循坏,对于每一个值都采取赋值构造,因为string的赋值构造内部是已经写好的深拷贝,我们不需要关心其内部是如何实现的。在采取赋值构造时它会自己进行深拷贝。

图解

在这里插入图片描述
实现代码:

void reserve(size_t n)
{
	//不会缩容

	//错误写法
	//错因:我们的_start改变了 但是我们的_finsh和_endofstorage还没有改变
	//在调用size()时会出错 因为我们size()函数求大小时使用了_finish
	//if (n > capacity)
	//{
	//	T* tmp = new T[n];
	//	memcpy(tmp, _start, sizeof(T) * size());
	//	delete[] _start;

	//	_start = tmp;
	//	_finish = _start + size();
	//	_endofstorage = _start + n;
	//}

	//正确
	if (n > capacity())
	{
		size_t oldSize = size();
		T* tmp = new T[n];
		//memcpy(tmp, _start, sizeof(T) * size());
		//如果使用memcpy,对于类似vector<string>这样的类,就会出现浅拷贝的现象从而报错
		for (size_t i = 0; i < oldSize; i++)
		{
			tmp[i] = _start[i];
		}
		delete[] _start;

		_start = tmp;
		_finish = _start + oldSize;
		_endofstorage = _start + n;
	}
}

2.7 resize

注意事项:

1.分情况
(1)n < size()时
	由于底层是原生指针因此我们可以直接改变指针的值,不需要循环pop_back()函数,可以直接_start + n
(2)n > size()时
	复用reserve,因为reserve内部对于空间足够的情况不会处理,只有空间不足时reserve内部才会重新分配空间
2.参数类型的缺省值如何给:匿名对象

实现代码:

void resize(size_t n , const T& val = T())
{
	if (n > size())
	{
		reserve(n);
		for (size_t i = size(); i < n; i++)
			push_back(val);
	}
	else
	{
		//删除
		//for (int i = size(); i > n; i--)
		//	pop_back();

		//我们可以直接改变指针
		_finish = _start + n;
	}
}

2.8 swap

​注意:

1.我们想要调用库的swap函数,因此必须指明std命名空间来调用。

因为编译器在搜索时会先搜索到我们实现的vector类中的swap,然后编译器会报错参数不匹配。

调用代码:

void swap(vector<T>& v)
{
	//错误写法
	swap(_start, v._start);
	swap(_finish, v._finish);
	swap(_endofstorage, v._endofstorage);
}
void testvector4()
{
	vector<int> v(10, 1);
	v.PrintVector();
	vector<int> v1(20, 2);
	v1.swap(v);
}

程序报错:
在这里插入图片描述

实现代码:

void swap(vector<T>& v)
{
	//注意:不能直接写成swap(_start,v._start);
	//swap(_start, v._start);
	std::swap(_start, v._start);
	std::swap(_finish, v._finish);
	std::swap(_endofstorage, v._endofstorage);
}

3. vector的迭代器失效

3.1 insert造成的迭代器失效

在vector中insert导致的迭代器失效多为扩容导致的问题。

案例:往容量已满的vector容器中插入值(实现时默认为4个空间)

void testvector6()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	vector<int>::iterator it = v.begin();
	cout << *it << endl;
	v.insert(it, 40);
	cout << *it << endl;
}

输出结果:

在这里插入图片描述

分析问题

造成这种问题的原因是因为在insert函数执行时进行了扩容操作,原有的空间被释放,但是it仍然指向被释放的空间,因此打印出来为随机值。

解决方案

错误思路:

可能会有人说,在函数参数部分传引用,这样就可以让迭代器自动更新,但是如果传引用的话,则无法将begin(),end()直接作为参数传递,因为它们的返回值是临时变量,具有常性,如果需要接收则需要在参数部分增加const来修饰,但是增加const之后就不能修改。

正确思路:

在这里插入图片描述

​stl库在设计insert函数时增加返回值,从而便于使用返回值来更新迭代器。更新迭代器

3.2 erase造成的迭代器失效

案例:删除值为偶数的位置

场景一:

//场景:删除值为偶数的节点
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);

vector<int>::iterator it = v.begin();
while (it != v.end())
{
	if (*it % 2 == 0)
		v.erase(it);
	it++;
}
v.PrintVector();

输出结果:正确
在这里插入图片描述

场景二:

//场景:删除值为偶数的节点
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(4);
v.push_back(5);

vector<int>::iterator it = v.begin();
while (it != v.end())
{
	if (*it % 2 == 0)
		v.erase(it);
	it++;
}
v.PrintVector();

输出结果:结果不符合预期,有跳过
在这里插入图片描述

场景三:

//场景:删除值为偶数的节点
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
v.push_back(6);

vector<int>::iterator it = v.begin();
while (it != v.end())
{
	if (*it % 2 == 0)
		v.erase(it);
	it++;
}
v.PrintVector();

输出结果:编译器报错
在这里插入图片描述
我们从画图的过程来理解为什么相似的几段代码但是结果却不相同。

图解

场景一:
在这里插入图片描述

场景二:
在这里插入图片描述

场景三:
在这里插入图片描述

分析问题

出现上面的原因就是因为迭代器it没有及时更新还在使用所导致的。

解决问题

在这里插入图片描述
stl库在设计时erase是有返回值的,其返回值为正确的迭代器,因此每次使用后可以通过接收返回值来更新迭代器。

修改后代码:

//场景:删除值为偶数的节点
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(4);
v.push_back(5);
v.push_back(6);

vector<int>::iterator it = v.begin();
while (it != v.end())
{
	if (*it % 2 == 0)
		it = v.erase(it);
	else
		it++;
}
v.PrintVector();

总结

vector的insert和erase操作都可能使迭代器失效。
迭代器失效以后,不要直接使用,如果需要使用应该按照规则重新更新后使用。

以上是本次所有内容,如有可以提升的地方,希望各位佬可以指点,谢谢观看。

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

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

相关文章

LDR6023,大功率OTG快充芯片,引领充电新时代!

在科技日新月异的当下&#xff0c;数据交互方式已经深入我们生活的每一个角落&#xff0c;对日常生活和工作产生着无可替代的影响。然而&#xff0c;在OTG技术诞生之前&#xff0c;数据交互的过程常常显得繁琐且耗时。想象一下&#xff0c;需要将数码相机中的照片导入笔记本电脑…

《科技创新与应用》是什么级别的期刊?是正规期刊吗?能评职称吗?

问题解答&#xff1a; 问&#xff1a;《科技创新与应用》是什么级别期刊&#xff1f; 答&#xff1a;省级&#xff1b;主管单位&#xff1a;黑龙江省科学技术协会&#xff1b;主办单位&#xff1a;黑龙江省创联文化传媒有限公司 问&#xff1a;《科技创新与应用》是核心期刊…

查找链表中的中间结点

略。。。。。 提升题的难度&#xff0c;不知道输入数据节点的个数。 方法一&#xff1a;对链表进行两次遍历。第一次遍历时&#xff0c;我们统计链表中的元素个数 N&#xff1b;第二次遍历时&#xff0c;我们遍历到第 N/2 个元素时&#xff0c;将该元素返回即可。 方法二&am…

粉丝免费福利第一期-海浪型手机支架

&#x1f341; 作者&#xff1a;知识浅谈&#xff0c;CSDN签约讲师&#xff0c;CSDN博客专家&#xff0c;华为云云享专家&#xff0c;阿里云专家博主 &#x1f4cc; 擅长领域&#xff1a;全栈工程师&#xff0c;大模型&#xff0c;爬虫、ACM算法 &#x1f492; 公众号&#xff…

不小心将某个分支的本地代码推到另外一个远程分支上

不小心将某个分支的本地代码推到另外一个远程分支上&#xff0c;是不会产生效果的&#xff0c;若提示了“Everything up-to-date ”&#xff0c;说明没有内容被提交到了远程&#xff0c;某个分支的本地代码仍然在本地仓库。 若想将改动的代码提交到另外一个远程分支上&#xf…

不懂技术的老板,如何避免过度依赖核心技术人员

在这个日新月异、技术驱动的时代&#xff0c;即使作为非技术背景的老板&#xff0c;也深知核心技术人员的价值。然而&#xff0c;过度依赖某几位核心技术人员&#xff0c;不仅可能带来经营风险&#xff0c;还可能限制企业的创新与发展。那么&#xff0c;不懂技术的老板&#xf…

【python基础教程】2. 算法的基本要素与特性

&#x1f388;个人主页&#xff1a;豌豆射手^ &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏 &#x1f917;收录专栏&#xff1a;python基础教程 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff0c;让我们共同学习、…

创建第一个51文件

1.找一个文件目录创建一个main.c 比如我的 F:\my_project\project_of_51\0.first_of_51 什么你不会&#xff1f; 先把这里的文件扩展名打开; 再创建一个文本文件&#xff0c;重新命名为main.c // 修改.c 后弹出一个确认修改的框&#xff0c;选确认即可 2.kei操作: 1&#xf…

学生成绩管理系统的设计与实现|Springboot+ Mysql+Java+ B/S结构(可运行源码+数据库+设计文档)

本项目包含可运行源码数据库LW&#xff0c;文末可获取本项目的所有资料。 推荐阅读100套最新项目持续更新中..... 2024年计算机毕业论文&#xff08;设计&#xff09;学生选题参考合集推荐收藏&#xff08;包含Springboot、jsp、ssmvue等技术项目合集&#xff09; 1. 管理员功…

Taskflow:条件任务(Conditional Tasking)

基本使用 条件任务评估一组指令&#xff0c;并返回要执行的下一个后续任务的整数索引。 该索引是根据其继任者结构的顺序定义的&#xff0c;首先&#xff0c;创建一个最简单的if else条件模块: #include <taskflow/taskflow.hpp>int main() {tf::Taskflow taskflow;tf:…

暴力破解pdf文档密码

首先安装pdfcrack工具包 apt install pdfcrack 默认密码字典存储在/usr/share/wordlists里&#xff0c;是gz文件&#xff0c;将它解压并copy到pdf目录 然后使用pdfcrack破解 密码在最后一行user-password的单引号里

计算机网络——31数据链路层和局域网引论和服务

数据链路层和局域网 WAN&#xff1a;网络形式采用点到点链路 带宽大&#xff0c;距离远&#xff08;延迟大&#xff09; 贷款延迟积大 如果采用多点连接方式 竞争方式&#xff1a;一旦冲突代价大令牌等协调方式&#xff1a;在其中协调节点的发送代价大 点到点链路的链路层服…

基于SpringBoot和Vue的教学管理系统的设计与实现

今天要和大家聊的是一款基于SpringBoot和Vue的教学管理系统的设计与实现 &#xff01;&#xff01;&#xff01; 有需要的小伙伴可以通过文章末尾名片咨询我哦&#xff01;&#xff01;&#xff01; &#x1f495;&#x1f495;作者&#xff1a;李同学 &#x1f495;&#x1f…

理解游戏服务器架构-逻辑底层架构

目录 前言 什么是逻辑底层架构 逻辑底层架构的职责 1&#xff09;Thread-线程 线程管理 线程通讯 线程安全锁机制 2&#xff09;Network-网络 网络模型 网络消息协议 断线重连 网络安全 防范重复消息 防范篡改消息内容 防范篡改内存数据 网络承载 3&#xff0…

使用nvm管理nodejs版本

文章目录 1、下载NVM2、选择NVM安装3 、查询版本号&常用命令4、nvm命令安装指定版本node4.1 安装指定node4.2 查看是否安装成功4.3 切换node版本到你想要的版本4.4 再次查看nvm版本列表4.5 nvm其他常用命令 这个是每个全能前端经常会用到的&#xff0c;之前用过现在重装了&…

基于SSM的戒烟网站(有报告)。Javaee项目。ssm项目。

演示视频&#xff1a; 基于SSM的戒烟网站&#xff08;有报告&#xff09;。Javaee项目。ssm项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&#xff0c;通过Spring SpringMv…

新能源汽车充电桩常见类型及充电桩站场的智能监管方案

随着新能源汽车市场的迅猛发展&#xff0c;充电桩作为支持其运行的基础设施&#xff0c;也呈现出多样化的类型。这些充电桩不仅在外形和功能上存在差异&#xff0c;更在充电速度、充电方式以及使用场景等方面展现出独特的优势。 一、充电桩类型及区别 1、慢充桩&#xff08;交…

Android 12系统源码_多窗口模式(一)和多窗口模式相关方法的调用顺序

前言 从 Android 7.0 开始&#xff0c;Google 推出了一个名为“多窗口模式”的新功能&#xff0c;允许在设备屏幕上同时显示多个应用&#xff0c;多窗口模式允许多个应用同时共享同一屏幕&#xff0c;多窗口模式&#xff08;Multi Window Supports&#xff09;目前支持以下三种…

在 IntelliJ IDEA 中使用 Terminal 执行 git log 命令后的退出方法

前言 IntelliJ IDEA 是一款广受欢迎的集成开发环境&#xff0c;它内置了强大的终端工具&#xff0c;使得开发者无需离开IDE就能便捷地执行各种命令行操作&#xff0c;包括使用 Git 进行版本控制。在 IDEA 的 Terminal 中执行 git log 命令时&#xff0c;由于该命令会显示项目的…

烫烫烫手的结构体大小计算来咯,很烫哦,慢慢消化。自定义类型(一)

emmm&#xff0c;在这炎热的夏天在宿舍吹着空调写着博客也是一件不错的事呢&#xff0c;今天就来来好好盘一下C语言中的自定义类型。 常常会回顾努力的自己&#xff0c;所以要给自己的努力留下足迹。 为今天努力的自己打个卡&#xff0c;留个痕迹吧 2024.03.29 小闭 目录 …