【C++】-vector的模拟实现(以及memcpy如何使用)

news2024/9/22 21:32:13

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
💖作者:小树苗渴望变成参天大树🎈
🎉作者宣言:认真写好每一篇博客💤
🎊作者gitee:gitee✨
💞作者专栏:C语言,数据结构初阶,Linux,C++ 动态规划算法🎄

如 果 你 喜 欢 作 者 的 文 章 ,就 给 作 者 点 点 关 注 吧!

文章目录

  • 前言
  • 一、源码理解
  • 二、模拟实现
    • 2.1成员属性
    • 2.2计算大小相关的函数
    • 2.3resize和reserve函数
    • 2.4operator[]函数
    • 2.5insert和erase函数
    • 2.6push_back和pop_back函数
    • 2.7构造函数和析构函数
    • 2.8swap函数
    • 2.9赋值运算符函数
  • 三、全部代码
  • 四、总结


前言

今天来带大家看看vector的模拟实现,思路来说还是比较简单的,vector本身也是一个动态可变的顺序表,常见功能大家还是熟悉的,接下来我先通过源码来带大家看看,然后再进行模拟实现,这样大家就会理解很多。


一、源码理解

我们看一个类首先看他的属性再看成员函数:
在这里插入图片描述
首先我圈出来的部分大家还是可以看懂的吧,我们vector不是像顺序表一样的通过size,capacity的这种方式来访问大小和容量了,而是通过两个指针与起始位置的差值来访问大小和容量了,为了适应任意类型,采取模板的形式,也将迭代器类型重命名出来了,我们来看看书上怎么说的

在这里插入图片描述

在这里插入图片描述

接下来我们看看他的构造函数
在这里插入图片描述
也是有四个,到时候模拟实现也是按照这四个进行模拟的,还有一个析构函数

计算大小和容量的一些函数
在这里插入图片描述
改变有效大小和容量函数:
在这里插入图片描述
插入删除函数:
在这里插入图片描述

大致就这些函数,大家大致框架应该可以看懂,里面涉及到一些空间配置器的问题(内存池),大家目前不需要理解,我们模拟实现的是使用new就行了,接下来我们来看看模拟实现吧

二、模拟实现

根据源码我们需要把源码定义出来,为了和库里面的vector发生命名冲突,我将模拟实现的vector放在一个命名空间域中,再定义两个文件:(模板的定义和声明不能分离)
在这里插入图片描述

2.1成员属性

#include<iostream>
#include<vector>
using namespace std;
#include<assert.h>
#include<string.h>

namespace xdh
{	
	template <class T>
	class vector {
	public:
		typedef T* iterator;
		typedef const T* const_iterator;
	private:
		iterator start=nullptr;//指向目前使用空间的开头
		iterator finish=nullptr;//指向目前使用空间的结尾
		iterator end_of_storage=nullptr;//指向目前可用空间的尾
	};
}

在这里插入图片描述

2.2计算大小相关的函数

iterator begin() { return start; }
iterator end() { return finish; }
const_iterator cbegin()const { return start; }
const_iterator cend() const { return finish; }
size_t size()const { return finish - start; }
size_t capacity()const { return end_of_storage - start; }
bool empty() { return begin() == end(); }

这些函数大家一眼就能看懂,我就不做具体讲解了

2.3resize和reserve函数

这两个函数要先实现reserve函数,因为resize函数可能会涉及到扩容,要复用reserve函数

	void reserve(size_t n)
		{
			if (n > capacity())//其他情况也要扩容,所以要检查
			{
				size_t oldsize = size();//记录原数组有效位置到其实位置的距离
				T* tmp = new T[n];
				if (start)//原数组不为空,将原数组的数据拷贝到新数组上
				{
					//思考:使用memcpy(tmp,start,sizeof(T)*size())可不可以
					for(size_t i = 0; i < oldsize; i++)
					{
						tmp[i] = start[i];
					}
					delete[] start;//将原来的空间先进行释放

				}
				start = tmp;//改变其实位置
				finish = start + oldsize;//改变有效位置
				end_of_storage = start + n;//改变最终位置
			}
		}

思考:使用memcpy(tmp,start,sizeof(T)*size())可不可以

	void resize(size_t n, const T& val = T())
		{
			if (n <= size())//改变的大小小于原数组的的大小,直接改变有效位置的指针
			{
				finish = start + n;
				return;
			}
			if (n > capacity())//先检查是否需要扩容
			{
				reserve(n);
				iterator it = finish;
				finish = start + n;
				while (it != finish)
				{
					*it = val;//将默认值放在数组的后面
					it++;
				}
			}
		}

我们看到resize需要提供默认值,方便初始化, 我们使用匿名对象,这样初始化就是调用T类型的默认构造,给T()进行初始化,然后通过T类型的赋值运算符给val赋值,就达到初始化任务了,这里面有两个注意的点,我们的自定义类型T必须要有自己的默认构造,可以帮助自己完成默认初始化,而且还必须有默认的赋值运算,涉及到深拷贝问题,赋值运算符还要自己实现才行。

我们是内置类型就会出现const int& val=int();这种情况,C++对于这块给我们做了优化
int a=int() 加相当于int a=0;
int a=int(1) 加相当于int a=1;
int a=int(2) 加相当于int a=3;
其余内置类型也是一样的道理

2.4operator[]函数

我们有两种,一种是const的,一种是非const的,因为我们也重命名了两种迭代器

		T& operator[](size_t pos)
		{
			assert(pos < size());
			return start[pos];
		}
		const T& operator[](size_t pos)const
		{
			assert(pos < size());
			return start[pos];
		}

2.5insert和erase函数

这两个函数可以说是vector比较常用的函数了,也是比较关键的函数,我们再vector使用的时候介绍了有三种insert和两种erase,博主都给实现了一遍:

		iterator insert(iterator position, const T& val)
		{
			assert(position <= finish);//检查下标合理性
			if (finish == end_of_storage)//扩容
			{
				size_t n = position-start;//记录插入位置和起始位置的距离,到时候再新数组好定位到position
				size_t newcapacity= capacity() == 0 ? 4 : capacity() * 2;//一开始为空就扩容4个,不为空就扩容两倍
				reserve(newcapacity);
				//要进行重置,不然pos还是指向原来数组的地址
				position = start + n;
			}
			iterator end = finish-1;//finish是执行那个数组的下一个位置,所以减1指向最后一个数组
			while (end >=position)
			{
				*(end + 1) = *end;//一个一个往后面移,把position位置空出来,给要插入的元素
				--end;
			}
			*position = val;
			++finish;//最终插入了一个位置,finish加1就可以了
			return position;//返回迭代器的值,防止迭代器失效
		}

		iterator insert(iterator position, size_t n, const T& val)
		{
			assert(position <= finish);
			if (finish == end_of_storage)
			{
				size_t n1 = position - start;
				reserve((capacity() == 0) ? 4 : capacity() + n);
				//要进行重置,不然pos还是指向原来数组的地址
				position = start + n1;
			}
			iterator end = finish-1;
			while (end >= position)
			{
				*(end + n) = *end;//一次往往后面移n个位置,从position位置往后空出来n个位置
				end--;
			}
			finish += n;
			while (n--)//再把插入的元素插进来
			{
				*(++end) = val;
			}
			return position;
		}

		template <class Input>
		iterator insert(iterator position, Input first, Input last)
		{
			assert(position <= finish);
			int n = last - first;
			if (size() == capacity())
			{
				size_t n1 = position - start;
				reserve((capacity() == 0) ? 4 : capacity() + n+1);
				//要进行重置,不然pos还是指向原来数组的地址
				position = start + n1;
			}
			iterator end = finish - 1;
			while (end >= position)
			{
				*(end + n) = *end;
				end--;
			}
			finish +=n;
			while (first<last)
			{
				*(position++) = (*first++);
			}
			return position;
		}
iterator erase(iterator position)
		{
			assert(position <= finish);//检查下标合法性
			iterator begin = position;
			while (begin < finish)
			{
				*(begin) = *(begin+1);//从前往后覆盖
				begin++;
			}
			--finish;
			return position;
		}
		iterator erase(iterator first, iterator last)
		{

			assert(first<= finish);//检查合法性
			int n = last - first+1;
			
			iterator begin = first + n;
			while (begin != finish)
			{
				*(begin - n) = *begin;
				begin++;
			}
			finish-=n;
			return first;
		}

注释我已经标识好了,大家不理解的画个图理解一下:对于为什么要使用返回值,就是要考虑迭代器可能会失效,再vector的使用已经讲解过了,可以画两块空间的图,一个新空间,一个就空间,看看再扩容的时候,此时的迭代器指向那个位置。就可以理解了。

2.6push_back和pop_back函数

这是一个尾插和尾删的函数,我们之前实现了insert和erase函数,之间复用就好了

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

2.7构造函数和析构函数

我为什么要把构造函数放在后面呢??原因就是我们要实现四个构造函数,有两个构造函数要复用前面的插入函数,每个构造函数的特点之前介绍过,这里就不做过多解释了

		//1.默认构造器
		vector()
			:start(nullptr)
			,finish(nullptr)
			,end_of_storage(nullptr)
		{
		}

		//2.传个数的初始化
		vector(size_t n, const T& val = T())
			:start(nullptr)
			, finish(nullptr)
			, end_of_storage(nullptr)
		{
			reserve(n);
			while (n--)
			{
				push_back(val);
			}
		}

		template <class InputIterator>
		//3.传其他迭代器进来初始化
		vector(InputIterator first, InputIterator last)
			:start(nullptr)
			, finish(nullptr)
			, end_of_storage(nullptr)
		{
			int n = last - first;
			reserve(n);
			while (n--)
			{
				push_back(*(first++));
			}
		}

		//4.传vector进来构造(拷贝构造)
		vector(const vector<T>& x)
			:start(nullptr)
			, finish(nullptr)
			, end_of_storage(nullptr)
		{
			size_t n = x.size();
			T* tmp = new T[n];
			for (int i = 0; i < n; i++)
			{
				tmp[i] = x[i];
			}
			start = tmp;
			finish = end_of_storage=start+n;
		}
~vector()
		{
			delete[] start;
			start = finish = end_of_storage = nullptr;
		}

我们发现我们写的所有构造里面都有初始化列表,而且都使用的是默认值nullptr,为什么要进行默认值呢,原因是再介绍构造函数那篇博客中介绍过,不写有的编译器不做处理,那么里面就是随机值,再C++11中打了这个补丁,我们再成员变量的时候就给缺省值就可以了,上面这些初始化列表都可以不用写。

我们来看一下第二个构造函数和第三个构造函数
在这里插入图片描述
我们看到我是想通过第二个构造函数进行初始化,但是看到报错信息和第三个构造函数报错了,这是为什么?很简单,我们的(10,2)这个参数再匹配模板的时候都会默认解释成int,说明第三个构造函数和它最匹配,上面图解也解释了,你的模板被解释成int了,而第二个构造函数再进行匹配的时候,第一个参数解释成size_t,第二个参数解释成int,显然没有第三个构造函数全部解释成int好,所有优先匹配了第三个构造函数,因为里面有解引用,所以出现了对非法地址的间接寻址
有两种解决办法
1.再传参的时候将类型规定好(10u,2)因为第三个构造函数只有一个模板,不可能解释成两个不太类型,这样只能匹配到第二个构造函数
2.将第二个构造函数的size_t改成int就可以,读者下来可以自己去看看

注意:这种情况再insert的第二个和第三个函数也出现过,我没有做修改,希望大家可以回过头来看看。

2.8swap函数

实现两个vector之间的交换,只需要交换三个属性就可以了

void swap(vector<T>& v)
		{
			std::swap(start, v.start);
			std::swap(finish, v.finish);
			std::swap(end_of_storage, v.end_of_storage);

		}

2.9赋值运算符函数

	vector<T>& operator=(vector<T>& x)
		{
			vector<T>tmp(x);
			swap(tmp);
			return *this;
		}

		vector<T>& operator=(vector<T> x)
		{
			swap(x);
			return *this;
		}

这两种都可以,我们按照原始的办法就要开辟空间进行拷贝,但我们发现这个方法拷贝构造已经帮我们实现了,第一种就直接通过拷贝构造来创建一个tmp对象,然后交换就可以了,第二种是再传参的时候就会调用拷贝构造,函数体里面之间交换就可以了,反正改变不了外面的实参


至此我们的模拟实现就到这里结束了,来解决一下刚才的思考:我们再reserve函数里面进行memcpy(tmp,start,sizeof(T)*size()),将原数组的值赋值到新数组上可以不可以??我们来看事例:
在这里插入图片描述
我们发现传自定义类型的参数,程序就报错了了,我只把赋值换成了memcpy函数,说明肯定是memcpy出现了错误,我们来看看为什么会出现错误:
在这里插入图片描述

  1. memcpy是内存的二进制格式拷贝,将一段内存空间中内容原封不动的拷贝到另外一段内存空间中
  2. 如果拷贝的是自定义类型的元素,memcpy既高效又不会出错,但如果拷贝的是自定义类型元素,并且自定义类型元素中涉及到资源管理时,就会出错,因为memcpy的拷贝实际是浅拷贝。

此时再理解这句话是不是容易多了,对于内置类型或者自定义类型中没有涉及到空间问题,那么使用memcpy和赋值没有任何区别,涉及到空间问题,就是有可能指向释放的空间,导致出错,所以大家使用memcpy的使用要注意有没有刚才说的情况。这样才不会出现

三、全部代码

vector.h

#pragma once
#include<iostream>
#include<vector>
using namespace std;
#include<assert.h>
#include<string.h>

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

		iterator begin() { return start; }
		iterator end() { return finish; }
		const_iterator cbegin()const { return start; }
		const_iterator cend() const { return finish; }
		size_t size()const { return finish - start; }
		size_t capacity()const { return end_of_storage - start; }
		bool empty() { return begin() == end(); }

		//==============reserse函数的实现=================
		void reserve(size_t n)
		{
			if (n > capacity())//其他情况也要扩容,所以要检查
			{
				size_t oldsize = size();//记录原数组有效位置到其实位置的距离
				T* tmp = new T[n];
				
				if (start)//原数组不为空,将原数组的数据拷贝到新数组上
				{
					for(size_t i = 0; i < oldsize; i++)
					{
						tmp[i] = start[i];
					}
					//memcpy(tmp, start, sizeof(T) * oldsize);
					delete[] start;//将原来的空间先进行释放
					
				}
				start = tmp;//改变其实位置
				finish = start + oldsize;//改变有效位置
				end_of_storage = start + n;//改变最终位置
			}
		}

		//============resize函数的实现=============
		void resize(size_t n, const T& val = T())
		{
			if (n <= size())//改变的大小小于原数组的的大小,直接改变有效位置的指针
			{
				finish = start + n;
				return;
			}
			if (n > capacity())//先检查是否需要扩容
			{
				reserve(n);
				iterator it = finish;
				finish = start + n;
				while (it != finish)
				{
					*it = val;//将默认值放在数组的后面
					it++;
				}
			}
		}
		//==============构造器============
		//1.默认构造器
		vector()
			:start(nullptr)
			,finish(nullptr)
			,end_of_storage(nullptr)
		{
		}

		//2.传个数的初始化
		vector(size_t n, const T& val = T())
			:start(nullptr)
			, finish(nullptr)
			, end_of_storage(nullptr)
		{
			reserve(n);
			while (n--)
			{
				push_back(val);
			}
		}

		template <class InputIterator>
		//3.传其他迭代器进来初始化
		vector(InputIterator first, InputIterator last)
			:start(nullptr)
			, finish(nullptr)
			, end_of_storage(nullptr)
		{
			int n = last - first;
			reserve(n);
			while (n--)
			{
				push_back(*(first++));
			}
		}

		//4.传vector进来构造(拷贝构造)
		vector(const vector<T>& x)
			:start(nullptr)
			, finish(nullptr)
			, end_of_storage(nullptr)
		{
			size_t n = x.size();
			T* tmp = new T[n];
			for (int i = 0; i < n; i++)
			{
				tmp[i] = x[i];
			}
			start = tmp;
			finish = end_of_storage=start+n;
		}

		//============operator=函数的实现==========
		vector<T>& operator=(vector<T>& x)
		{
			vector<T>tmp(x);
			swap(tmp);
			return *this;
		}

		vector<T>& operator=(vector<T> x)
		{
			swap(x);
			return *this;
		}
		//============析构函数=========
		~vector()
		{
			delete[] start;
			start = finish = end_of_storage = nullptr;
		}
		//===========operator[]函数的实现==========
		T& operator[](size_t pos)
		{
			assert(pos < size());
			return start[pos];
		}
		const T& operator[](size_t pos)const
		{
			assert(pos < size());
			return start[pos];
		}


		//==============insert函数的实现============
		iterator insert(iterator position, const T& val)
		{
			assert(position <= finish);//检查下标合理性
			if (finish == end_of_storage)//扩容
			{
				size_t n = position-start;//记录插入位置和起始位置的距离,到时候再新数组好定位到position
				size_t newcapacity= capacity() == 0 ? 4 : capacity() * 2;//一开始为空就扩容4个,不为空就扩容两倍
				reserve(newcapacity);
				//要进行重置,不然pos还是指向原来数组的地址
				position = start + n;
			}
			iterator end = finish-1;//finish是执行那个数组的下一个位置,所以减1指向最后一个数组
			while (end >=position)
			{
				*(end + 1) = *end;//一个一个往后面移,把position位置空出来,给要插入的元素
				--end;
			}
			*position = val;
			++finish;//最终插入了一个位置,finish加1就可以了
			return position;//返回迭代器的值,防止迭代器失效
		}
		void insert(iterator position, int n, const T& val)
		{
			assert(position <= finish);
			if (finish == end_of_storage)
			{
				size_t n1 = position - start;
				reserve((capacity() == 0) ? 4 : capacity() + n);
				//要进行重置,不然pos还是指向原来数组的地址
				position = start + n1;
			}
			iterator end = finish-1;
			while (end >= position)
			{
				*(end + n) = *end;//一次往往后面移n个位置,从position位置往后空出来n个位置
				end--;
			}
			finish += n;
			while (n--)//再把插入的元素插进来
			{
				*(++end) = val;
			}
			
		}

		template <class Input>
		void insert(iterator position, Input first, Input last)
		{
			assert(position <= finish);
			int n = last - first;
			if (size() == capacity())
			{
				size_t n1 = position - start;
				reserve((capacity() == 0) ? 4 : capacity() + n+1);
				//要进行重置,不然pos还是指向原来数组的地址
				position = start + n1;
			}
			iterator end = finish - 1;
			while (end >= position)
			{
				*(end + n) = *end;
				end--;
			}
			finish += n;
			while (first<last)
			{
				*(position++) = (*first++);
			}
			
		}


		//==============erase的函数实现===============
		iterator erase(iterator position)
		{
			assert(position <= finish);//检查下标合法性
			iterator begin = position;
			while (begin < finish)
			{
				*(begin) = *(begin+1);//从前往后覆盖
				begin++;
			}
			--finish;
			return position;
		}
		iterator erase(iterator first, iterator last)
		{

			assert(first<= finish);//检查合法性
			int n = last - first+1;
			
			iterator begin = first + n;
			while (begin != finish)
			{
				*(begin - n) = *begin;
				begin++;
			}
			finish-=n;
			return first;
		}


		//==============push_back和pop_back函数的实现=============
		void push_back(const T& val)
		{
			insert(end(), val);
		}
		void pop_back()
		{
			erase(end());
		}


		//==============swap函数的实现====================
		void swap(vector<T>& v)
		{
			std::swap(start, v.start);
			std::swap(finish, v.finish);
			std::swap(end_of_storage, v.end_of_storage);

		}
	private:
		iterator start;//指向目前使用空间的开头
		iterator finish;//指向目前使用空间的结尾
		iterator end_of_storage;//指向目前可用空间的尾
	};
}

四、总结

我们的vector模拟实现总算讲完了,里面要注意的细节还是非常多,所以说C++没学好,里面的坑还是非常多的,博主自己再实现的时候也出现了很多错误,一定要写一个功能旧测试一个,不然出错了就很痛苦,那我们这篇就说到这里,下篇我们开始介绍list,这个容器难度就上来了,因为涉及到带头双向循环链表的结构,不用担心,跟着博主走,没有难的事。请添加图片描述

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

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

相关文章

Spring-MVC的注解扫描-spring17

包括我们业务层和Dao层&#xff0c;去帮助别人去扫 只扫controller下的注解

00_YS_硬件电路图

1.主控制芯片的型号 STM32F407IGT6&#xff0c;LQFP-176&#xff0c;1MB 内部 FLASH&#xff0c;192KB RAM USART3 RS485 通信&#xff0c;芯片使用 SP3072EEN; UART5 RS232 通信&#xff0c; CAN 1 路&#xff0c;型号 SN65HVD230 USB 支持 …

Python应用实例(二)数据可视化(三)

数据可视化&#xff08;三&#xff09; 1.使用Plotly模拟掷骰子1.1 安装Plotly1.2 创建Die类1.3 掷骰子1.4 分析结果1.5 绘制直方图1.6 同时掷两个骰子1.7 同时掷两个面数不同的骰子 1.使用Plotly模拟掷骰子 本节将使用Python包Plotly来生成交互式图表。需要创建在浏览器中显示…

代码随想录算法训练营第十九天 | 动态规划系列5,6,7,8

动态规划系列5,6,7,8 377 组合总和 Ⅳ未看解答自己编写的青春版重点代码随想录的代码我的代码(当天晚上理解后自己编写) 爬楼梯进阶322 零钱兑换未看解答自己编写的青春版写完这道题后的感受重点代码随想录的代码动态规划&#xff0c;也要时刻想着剪枝操作。我的代码(当天晚上理…

异常执行结果随笔

前段时间有朋友问我异常执行顺序问题&#xff0c;这里简单记录下哈。 伪代码描述&#xff0c;当j0和j1&#xff0c;输出结果分别是什么&#xff1f; int i 0; int j 0或1; try {j i / j&#xff1b;System.out.println(i);return i; } catch (Exception e) {System.out.pri…

win10如何使用wsl配置Ubuntu并使用vscode连接

文章目录 0. 前置资料1. 下载wsl2. 下载Ubuntu3. vscode连接wsl 0. 前置资料 wsl为适用于 Linux 的 Windows 子系统&#xff0c;可参考以下微软的官方文档 https://learn.microsoft.com/zh-cn/windows/wsl/ 1. 下载wsl 点击屏幕左下角的放大镜&#xff0c;直接在输入框键入P…

第 354 场LeetCode周赛

A 特殊元素平方和 模拟 class Solution { public:int sumOfSquares(vector<int> &nums) {int res 0;int n nums.size();for (int i 0; i < n; i)if (n % (i 1) 0)res nums[i] * nums[i];return res;} };B 数组的最大美丽值 差分数组: n u m s [ i ] nums[…

cmake处理参数时的一些问题说明

cmake处理参数时的一些问题说明 函数传参空格和分号的坑函数转发的坑demo 函数传参遇到不平衡方括号的坑 函数传参空格和分号的坑 我们在处理函数和宏的时候不过不小心会遇到很多坑例如: someCommand(a b c) someCommand(a b c)因为cmake中使用空格或者分号分隔符所以上面代…

Django实现接口自动化平台(十一)项目模块Projects序列化器及视图【持续更新中】

相关文章&#xff1a; Django实现接口自动化平台&#xff08;十&#xff09;自定义action names【持续更新中】_做测试的喵酱的博客-CSDN博客 本章是项目的一个分解&#xff0c;查看本章内容时&#xff0c;要结合整体项目代码来看&#xff1a; python django vue httprunner …

LangChain 本地化方案 - 使用 ChatYuan-large-v2 作为 LLM 大语言模型

一、ChatYuan-large-v2 模型 ChatYuan-large-v2是一个开源的支持中英双语的功能型对话语言大模型&#xff0c;与其他 LLM 不同的是模型十分轻量化&#xff0c;并且在轻量化的同时效果相对还不错&#xff0c;仅仅通过0.7B参数量就可以实现10B模型的基础效果&#xff0c;正是其如…

自动化测试-selenium环境搭建

文章目录 1. 什么是自动化2. 自动化测试分类3. selenium的环境搭建4. 测试selenium 1. 什么是自动化 自动化是指使用软件工具、脚本或程序来执行一系列的任务、操作或流程&#xff0c;而无需人工干预或指导。 自动化测试&#xff1a;使用自动化工具和脚本来执行测试用例&#x…

浅谈端口 | 80端口和8080端口是2个不同的端口吗?有何区别?

目录 写在前面 端口及其特点 端口号的范围和分类 在HTTP超文本传输协议中80端口有正式的身份 写在前面 首先&#xff0c;确定以及肯定的是他们俩是完全不同的端口。一般80作为网页服务器的访问端口&#xff0c;比如一个网站的ip地址是119.119.119.119&#xff0c;我们在浏…

如何将SAP数据抽取到Azure数据湖平台?

经过多年的发展&#xff0c;SNP Glue 在全球已成为值得信赖且广为人知的解决方案&#xff0c;支持客户将其 SAP 数据与现代化的平台集成。SNP Glue 打破了数据孤岛&#xff0c;向数据科学家开放了 SAP&#xff0c;支持基于企业 SAP 数据的多个新用例。 随着时间的推移&#xff…

MySQL数据库之事物

一、MySQL事务的概念 &#xff08;1&#xff09;事务是一种机制、一个操作序列&#xff0c;包含了一组数据库操作命令&#xff0c;并且把所有的命令作为一个整体一起向系统提交或撤销操作请求&#xff0c;即这一组数据库命令要么都执行&#xff0c;要么都不执行。 &#xff08…

【基础理论】描述性统计基本概念

一、说明 统计学是数学的一个分支&#xff0c;涉及数据的收集、解释、组织和解释。本博客旨在回答以下问题&#xff1a; 1. 什么是描述性统计&#xff1f;2. 描述性统计的类型&#xff1f;3. 集中趋势的度量&#xff08;平均值、中位数、模式&#xff09; 4. 散布/离差度量&…

每天一道C语言编程(6):委派任务

题目描述 某侦察队接到一项紧急任务&#xff0c;要求在A、B、C、D、E、F六个队员中尽可能多地挑若干人&#xff0c;但有以下限制条件&#xff1a; 1)A和B两人中至少去一人&#xff1b; 2)A和D不能一起去&#xff1b; 3)A、E和F三人中要派两人去&#xff1b; 4)B和C都去或都不去…

使用CatBoost和SHAP进行多分类完整代码示例

CatBoost是顶尖的机器学习模型之一。凭借其梯度增强技术以及内置函数&#xff0c;可以在不做太多工作的情况下生成一些非常好的模型。SHAP (SHapley Additive exPlanation)是旨在解释具有独特视觉效果和性能价值的机器学习模型的输出。CatBoost和SHAP结合在一起构成了一个强大的…

《动手学深度学习》(pytorch版本)中`d2lzh_pytorch`包问题

《动手学深度学习》&#xff08;pytorch版本&#xff09;中d2lzh_pytorch包问题

vue3-03 todo-新增任务

步骤&#xff1a; todoStorage.js&#xff1a;定义获取任务列表的方法、将任务存储到localStorage的方法、后续需要用到的获取随机id的方法。useTodoList.js&#xff1a;用一个响应式变量todosRef接收任务列表的数据&#xff1b;使用watchEffect方法将新增的任务保存到localSt…

2023/7/16周报

摘要 论文阅读 1、题目和现有问题 2、工作流程 3、图神经网络模块 4、注意力网络 5、实验结果和分析 深度学习 1、GNN和GRU的融合 2、相关公式推导 总结 摘要 本周在论文阅读上&#xff0c;对基于图神经网络和改进自注意网络的会话推荐的论文进行了学习&#xff0c;…