【C++】——vector深度剖析模拟实现

news2024/9/28 19:22:18

低头赶路,敬事如仪


目录

1、模拟vector

1.1底层结构

1.2构造析构

1.3尾插扩容

1.4迭代器

1.5增删查改

1.6模拟中的注意事项

2、vector模拟补充

2.1迭代器区间构造问题

2.2memcpy深浅拷贝问题

2.3动态二维数组的模拟及遍历


1、模拟vector

想要模拟实现自己的vector,得知其所以然!

分析一下STL源码里的vector底层成员变量

可以看到是三个迭代器类型成员变量,迭代器类型是什么呢?

经过typedef的底层指针,而T类型是模版类的参数。

大致框架如图:

1.1底层结构

根据我们刚才所查看的源码,我们要使用三个迭代器,要使用迭代器,我们可以使用指针进行模拟。

namespace xc
{
	//设置成模板 兼容各种参数 但是不能分离编译否则链接错误
	template<class T>
	class vector
	{
	public:
		typedef T* iterator;

	private:
		iterator _start = nullptr;//指向容器开始
		iterator _finish = nullptr;//指向有效数据下一个位置
		iterator _end_of_storage = nullptr;//指向空间容量的下一个位置
	};
}

写出三个迭代器(指针)后,我们可以接着实现构造函数:需要初始化三个迭代器,所以我们给予初始值nullptr。然后进行开辟空间。

1.2构造析构

实现常用的构造析构以及赋值重载

/*	vector()
	{}*/
	//C++11 强制生成默认构造
	vector() = default;

	//优先匹配构造函数,防止非法的间接寻址问题
	vector(size_t n, const T& val = T())
	{
		reserve(n);
		for (size_t i=0; i < n; i++)
		{
			push_back(val);
		}
	}
	vector(int n, const T& val = T())
	{
		reserve(n);
		for (int i=0; i < n; i++)
		{
			push_back(val);
		}
	}
	vector(long long n, const T&val = T())
	{
		reserve(n);
		for (long long i=0; i < n; i++)
		{
			push_back(val);
		}
	}

	//类模板的成员函数还可以继续是函数模板
	template<class InputIterator>  //写成模板是为了兼容所有容器
	vector(InputIterator first, InputIterator last)
	{
		while(first != last)
		{
			push_back(*first);
			++first;
		}
	}

	// v1(v)
	vector(const vector<T>& v)
	{
		reserve(v.size());
		for (auto& e : v)
		{
			push_back(e);
		}
	}

	void clear()
	{
		_finish = _start;
	}

	 v1=v
	//vector<T>& operator=(const vector<T>&v)
	//{
	//	if (this != &v)
	//	{
	//		clear();
	//		reserve(v.size());
	//		for (auto& e : v)
	//		{
	//			push_back(e);
	//		}
	//	}
	//	return *this;
	//}

	void swap(vector<T>&v)
	{
		std::swap(_start, v._start);
		std::swap(_finish, v._finish);
		std::swap(_end_of_storage, v._end_of_storage);
	}
	//现代写法 V1=v
	vector<T>& operator=(const vector<T> v)
	{
		swap(v);
		return *this;
	}

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

1.3尾插扩容

想要实现尾插的操作,根据之前的经验,可以知道需要复用一些常用的简单接口(size() capacity() reserve() 等)

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

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

bool empty()const { _start == _finish; }

//void reserve(size_t n)
//{
//	if (n > capacity())
//	{
//		T* tmp = new T[n];
//		memcpy(tmp, _start, sizeof(T));
//		delete[]_start;

//		_start = tmp;
//		_finish = _start + size();//这是错误的  _start 已经是扩容后的 start了 而size调用的是 finish - start
//									//解决方法 可以先更新 finish 或者记录一下 size的值
//		_end_of_storage = _start + n;
//	}
//}

void reserve(size_t n)
{
	if (n > capacity())
	{
		size_t old_size = size();
		T* tmp = new T[n];
		memcpy(tmp, _start, size()*sizeof(T));

		delete[] _start;

		_start = tmp;
		_finish = tmp + old_size;
		_end_of_storage = tmp + n;
	}
}

void reseize(size_t n, T val = T())//内置类型也具有的构造析构等函数行为 但是并没有概念
{
	if (n < size())
	{
		_finih = _start + n;
	}
	else
	{
		reserve(n);
		while (_finsh < _start+n)
		{
			*_finish++ = val;
		}
	}
}

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

注意:如果对象中涉及到资源管理时,千万不能使用memcpy进行对象之间的拷贝,因为memcpy是浅拷贝,否则可能会引起内存泄漏甚至程序崩溃。

1.4迭代器

所需要实现的迭代器其实很简单,对指针 typedef 即可

typedef T* iterator;
typedef const T* const_iterator;

iterator begin() { return _start; }

iterator end() { return _finish; }

const_iterator begin()const { return _start; }

const_iterator end()const { return _finish; }

实现了简单的迭代器,我们可以测试一下 迭代器遍历和范围for遍历

void test_vector1()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(1);
	v.push_back(1);
	v.push_back(1);
	vector<int>::iterator it = v.begin();
	while (it != v.end())
	{
		cout << *it << ' ';
		++it;
	}
	cout << endl;

	for (auto e : v)
	{
		cout << e << ' ';
	}
	cout << endl;
}

这里我们为了后续方便封装一个打印函数

template<class T>
void print_vector(const vector<T>& v)
{
	vector<T>::const_iterator it = v.begin();
	while (it != v.end())
	{
		cout << *it << ' ';
		++it;
	}
	cout << endl;
}

可是这居然报错

为什么呢?没有实例化的类模板里面取东西,编译器是分不清 const_iterator 是静态成员变量还是类型的,在前面加一个 typename 显示告诉编译器是类型即可

template<class T>
void print_vector(const vector<T>& v)
{
	typename vector<T>::const_iterator it = v.begin();
	while (it != v.end())
	{
		cout << *it << ' ';
		++it;
	}
	cout << endl;
}

1.5增删查改

void insert(iterator pos, const T& x)
{
	if (_finish == _end_of_storage)
	{
		reserve(capacity() == 0 ? 4 : capacity() * 2);
	}
	iterator end = _finish;
	while (pos < end)
	{
		*end = *(end - 1);
		--end;
	}
	*pos = x;
	++_finish;
}

看起来没什么问题,但是存在着迭代器失效的风险

pos仍指向原来的空间!记录相对位置,修正一下pos即可

void insert(iterator pos, const T& x)
{
	if (_finish == _end_of_storage)
	{
		size_t len = pos - _start;//记录相对位置
		reserve(capacity() == 0 ? 4 : capacity() * 2);
		pos = _start + len;
	}
	iterator end = _finish;
	while (pos < end)
	{
		*end = *(end - 1);
		--end;
	}
	*pos = x;
	++_finish;
}

值得注意的是STL里的 insert有返回值!

这是为什么呢?还是防止迭代器失效的一种措施。

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

/*	v.insert(v.begin() + 1, 10);
	print_vector(v);*/
	int x;
	cin >> x;// x==2
	auto pos = find(v.begin(), v.end(), x);//没找到返回 last 左闭右开区间
	if (pos != v.end())
	{
		v.insert(pos, 40);//pos 可以理解为下标位置 也可以理解为原来位置数据前面插入一个数据
		print_vector(v);
		(*pos) *= 10;//期望将x乘以10 但是是插入的40*10  并没有指向原来的数据
						//归为迭代器失效一类
	}
	print_vector(v);

}

如果这里还发生了扩容呢??就是将push的5给删掉

这是什么鬼?我们不是已经修正了pos的位置吗?显然这里的p已经失效了!形参是无法改变实参的,我们修正了pos,但是影响不了p

传const 引用过去影响实参。但不是常量引用的话 ,下面代码会报错!

因为函数调用返回的参数是临时变量,且临时变量具有常性。

而事实上加了const引用里面的pos就无法修正了!我们的思路错了,STL的操作是加一个返回值 (返回的是新插入数据的下标)

iterator insert(iterator  pos, const T& x)
{
	assert(pos <= _finish && pos >= _start);
	if (_finish == _end_of_storage)
	{
		size_t len = pos - _start;//记录相对位置
		reserve(capacity() == 0 ? 4 : capacity() * 2);
		pos = _start + len;
	}
	iterator end = _finish;
	while (pos < end)
	{
		*end = *(end - 1);
		--end;
	}
	*pos = x;
	++_finish;
	return pos;
}

所以 insert后不能再访问find 的 p 想要访问得更新!

增删查改:

iterator insert(iterator  pos, const T& x)
{
	assert(pos <= _finish && pos >= _start);
	if (_finish == _end_of_storage)
	{
		size_t len = pos - _start;//记录相对位置
		reserve(capacity() == 0 ? 4 : capacity() * 2);
		pos = _start + len;
	}
	iterator end = _finish;
	while (pos < end)
	{
		*end = *(end - 1);
		--end;
	}
	*pos = x;
	++_finish;
	return pos;
}

iterator erase(iterator pos)
{
	assert(pos < _finish && pos >= _start);
	iterator it = pos + 1;
	while (it != end())
	{
		*(it - 1) = *it;
		++it;
	}
	--_finish;
	return pos;
}

size_t find(T val = T())
{
	for (auto it = _start; it < _finish; it++)
	{
		if (*it == val)
		{
			return it - _start;
		}
	}
	return -1;
}

T& operator[](size_t i)
{
	assert(i < size());
	return _start[i];
}

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

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

1.6模拟中的注意事项

  • 规定:类模板在没有实例化时,迭代器无法读取!编译器不能区分这里const_iterator是类型还是静态成员变量。要想解决:第一可以在前面加上typename用来证明这里是类型;第二用auto,让编译器判断为类型
  • 内置类型是没有构造函数的概念,但为了兼容模板(T val = T( ) ),可以被视为具有默认构造函数的行为
void reseize(size_t n, T val = T())//内置类型具有的构造析构等函数行为,并没有对应概念
{
	if (n < size())
	{
		_finish = _start + n;
	}
	else
	{
		reserve(n);
		while (_finish < _start+n)
		{
			*_finish++ = val;
		}
	}
}
  •  c++11中有强制生成默认构造的操作,即使类中已有其他构造函数,也能强制生成。eg:vector( ) = default

  • 类里面可以用类名替代类型(特殊化),类外面规定:类名不能代表类型
//类里面可以用类名替代类型(特殊化)
//vector & operator=(vector v)
vector<T>& operator= (vector<T> v)
{
    swap(v);
    return *this;
}
  • 类模板的成员函数,还可继续是函数模板
//类模板的成员函数还可以继续是函数模板
template<class InputIterator>  //写成模板是为了兼容所有容器
vector(InputIterator first, InputIterator last)
{
	while(first != last)
	{
		push_back(*first);
		++first;
	}
}

2、vector模拟补充

2.1迭代器区间构造问题

//C++11 强制生成默认构造
vector() = default;


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

//类模板的成员函数还可以继续是函数模板
template<class InputIterator>  //写成模板是为了兼容所有容器
vector(InputIterator first, InputIterator last)
{
	while(first != last)
	{
		push_back(*first);
		++first;
	}
}

这个是对迭代器区间进行的构造函数,思路很简单,把迭代器区间的数据依次尾插就可以了(这里之所以另外使用一个新的模版,而不是使用vector类的模版,是为了兼容其他容器类型)。这样就可以通过一个现有的类型来构造容器。
但是出乎意料的是出现了一个问题: C2100 非法的间接寻址 (编译层面的问题) 。非法的间接寻址的造成原因有很多:

  1. 空指针引用:当一个指针没有被初始化或者为NULL时,对它进行间接寻址操作会导致非法访问
  2. 野指针引用:当一个指针超出了它所指向的内存范围,或者已经被释放但仍然被引用时,进行间接寻址操作也会导致非法访问。
  3. 类型不匹配:如果试图将指针转换为不兼容的类型进行间接寻址,也会导致非法访问。

为什么报错在迭代器构造呢?因为编译器会寻找最合适的函数,但是这里与我们期望所违背!!

为了解决编译器选择的问题,可以多枚举几个构造函数:

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

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

这样可以做到优先匹配vector(int n,T val = T());,我们的问题也就解决了。 

2.2memcpy深浅拷贝问题

我们测试一下自定义类型的vector(这里以string为例):

void vector_test6() {
	vector<string> v1;
	v1.push_back("11111");
	v1.push_back("22222");
	v1.push_back("33333");
	v1.push_back("44444");
    //再次插入扩容
	v1.push_back("55555");

	print_vector(v1);
}

程序崩溃了

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

结论:如果对象中涉及到资源管理时,千万不能使用memcpy进行对象之间的拷贝,因为memcpy是浅拷贝,否则可能会引起内存泄漏甚至程序崩溃

解决方法:

void reserve(size_t n)
{
	if (n > capacity())
	{
		size_t old_size = size();
		T* tmp = new T[n];
		//memcpy(tmp, _start, size()*sizeof(T));//浅拷贝
		for (size_t i = 0; i < old_size; i++)
		{
			tmp[i] = _start[i];//自定义类型的话 也是会调用自定义类型的赋值重载
		}
		delete[] _start;

		_start = tmp;
		_finish = tmp + old_size;
		_end_of_storage = tmp + n;
	}
}

2.3动态二维数组的模拟及遍历

结构框架图:

//二维数组
vector<int>v(3, 1);
vector<vector<int>>vv(5, v);

vv[2][1] = 2;
//与上述等价  vv.operator[](2).operator[](1) = 2;
for (size_t i = 0; i < vv.size(); i++)
{
	for (size_t j = 0; j < vv[i].size(); j++)
	{
		cout << vv[i][j] << ' ';
	}
	cout << endl;
}
cout << endl;

for (auto it = vv.begin(); it != vv.end(); it++)
{
	for (auto jt = (*it).begin(); jt != (*it).end(); jt++)
	{
		cout << *jt << ' ';
	}
	cout << endl;
}
cout << endl;

auto it1 = vv.begin();
/*auto it2 = (*it1).begin();*/
while (it1 != vv.end())
{
	auto it2 = (*it1).begin();//更新it2 指针在堆上分配,所以可以看见内存不是连续分配的
	while (it2 != (*it1).end())
	{
		cout << *it2 << ' ';
		it2++;
	}
	cout << endl;
	it1++;
}
cout << endl;

for (auto row : vv)
{
	for (auto column : row)
	{
		cout << column << ' ';
	}
	cout << endl;
}
cout << endl;

应用:杨辉三角

// 以杨慧三角的前n行为例:假设n为5
void test2vector(size_t n)
{
	// 使用vector定义二维数组vv,vv中的每个元素都是vector<int>
	vector<vector<int>> vv(n);
	// 将二维数组每一行中的vecotr<int>中的元素全部设置为1
	for (size_t i = 0; i < n; ++i)
		vv[i].resize(i + 1, 1);
	// 给杨慧三角出第一列和对角线的所有元素赋值
	for (int i = 2; i < n; ++i)
	{
			for (int j = 1; j < i; ++j)
			{
				vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];
			}
	}
}

构造一个vv动态二维数组,vv中总共有n个元素,每个元素 都是vector类型的,每行没有包含任何元素,n为5时如下所示:

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

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

相关文章

即梦PixelDance:从追赶到领跑,一跃成为全球AI竞赛的领航者!

今年2月份&#xff0c;OpenAI发布了全新的文生视频模型Sora&#xff0c;那一次的发布&#xff0c;成功开启了AI视频生成的ChatGPT时刻。 看到Sora的发布视频&#xff0c;我相信有很多小伙伴和我一样被深深得震撼了&#xff01; 只需一个指令就可以生成一段逼近真实的视频&…

NBOUND 2024:与AI 一起推动业务增长

在2024年&#xff0c;我们的企业有幸参加了INBOUND大会&#xff0c;这是HubSpot为期三天的动态会议&#xff0c;专注于探讨营销、销售和人工智能领域的最新趋势和策略。作为HubSpot的合作伙伴&#xff0c;我们在这里不仅学习和分享&#xff0c;更见证了如何通过AI推动增长的无限…

【DAY20240926】智能时隙选择器

文章目录 要点元模型和本地模型的关系lstm模型更新Q-learning选择策略&#xff1a;ϵ-greedy动作空间&#xff08;&#x1d44e;&#x1d461;&#x1d456;−1∈{add,stay,minus}&#xff09;: 要点 The intelligent time slot selector is composed of a meta model on the …

提升并行效率的Python多处理模块指南

在Python中&#xff0c;multiprocessing 模块提供了强大的多进程支持&#xff0c;能够帮助我们充分利用多核CPU资源来实现并行计算。相比传统的多线程模式&#xff0c;Python的多进程模式更加高效&#xff0c;因为它可以避免Python的全局解释器锁&#xff08;GIL&#xff09;对…

一个超强大的Python数据探索工具

在数据分析的过程中&#xff0c;快速掌握数据集的基本特征是必不可少的一步。虽然 Pandas 提供了方便的 df.describe() 方法来生成数据摘要&#xff0c;但随着数据类型和分析需求的多样化&#xff0c;这一方法的局限性逐渐显现。Skimpy 作为一个新兴的 Python 包&#xff0c;旨…

基于mybatis-plus创建springboot,添加增删改查功能,使用postman来测试接口出现的常见错误

1 当你在使用postman检测 添加和更新功能时&#xff0c;报了一个500错误 查看idea发现是&#xff1a; Data truncation: Out of range value for column id at row 1 通过翻译&#xff1a;数据截断&#xff1a;表单第1行的“id”列出现范围外值。一般情况下&#xff0c;出现这个…

什么是Agent智能体?

你好&#xff0c;我是三桥君 近期&#xff0c;从各大厂商的年度大会到多个大型AI峰会&#xff0c;三桥君明显感受到行业风气的转变。这些会议不仅展示了众多AI Agent的实际应用案例&#xff0c;还有专家们对未来发展的预测。一时间&#xff0c;“Agent”这个词成为了热门词汇&…

Linux(含麒麟操作系统)如何实现多显示器屏幕采集录制

技术背景 在操作系统领域&#xff0c;很多核心技术掌握在国外企业手中。如果过度依赖国外技术&#xff0c;在国际形势变化、贸易摩擦等情况下&#xff0c;可能面临技术封锁和断供风险。开发国产操作系统可以降低这种风险&#xff0c;确保国家关键信息基础设施的稳定运行。在一…

多态的遗留问题以及C++中杂项,C++面试题

C面试题 1.什么是虚函数&#xff1f;什么是纯虚函数 虚函数&#xff1a;在类的继承中&#xff0c;基类中的函数前加virtual声明的函数就是虚函数。 虚函数实现了运行的多态&#xff0c;同一函数调用在不同对象中表现出不同的行为 纯虚函数&#xff1a;在基类中声明但没有实…

树莓派4B+UBUNTU20.04+静态ip+ssh配置

树莓派4B+UBUNTU20.04+静态ip+ssh配置 1.烧录Ubuntu镜像1.1选择pi 4b1.2选择ubuntu server (服务器版,无桌面)20.041.3选择sd卡1.4 点击右下角 NEXT ,编辑设置,输入密码,wifi选CN, 开启ssh1.5 烧录,依次点击“是”,等待完成2 烧录完成后装入树莓派,上电,等待系统完成配…

软件无线电3-微相E316和HackRF实现FM调制解调

前面介绍了基于Matlab、矢量信号器和HackRF One实现射频下的FM调制解调&#xff0c;今天分享的内容是用微相E316替代矢量信号器完成发射工作。注意本文仅用于科研和学习&#xff0c;私自搭建电台属于违法行为。 1.概述 微相E316和HackRF One实现FM调制解调测试框图如1所示&am…

离散化 ---( 求区间和)

什么是离散化&#xff1f; 离散化是将连续的数值范围映射到有限的、离散的数值集合的过程。在许多情况下&#xff0c;数据可能会存在多个重复值或范围较大的连续值。为了简化处理&#xff0c;尤其是处理区间查询和增量问题时&#xff0c;我们可以将这些值转换为一组有限的、唯一…

【重学 MySQL】四十、SQL 语句执行过程

【重学 MySQL】四十、SQL 语句执行过程 select 语句的完整结构select 语句执行顺序SQL 语句执行原理 select 语句的完整结构 SELECT 语句是 SQL&#xff08;Structured Query Language&#xff09;中用于从数据库表中检索数据的核心语句。一个完整的 SELECT 语句结构可以包括多…

Linux系统部署Mysql8.x修改密码并且设置远程连接

配置yum仓库配置yum仓库 # 更新密钥 rpm import https:repo.mysql.com/RPM-GPG-KEY- mysql-2023 # 安装Mysql8.x版本 yum库 rpm -Uvh https:dev.mysql.com/get/mysql80-community-release-el7-2.noarch.rpm 由于MySQL并不在CentOS的官方仓库中&#xff0c;所以我们通过上述rp…

跟李沐学AI:自注意力和位置编码

自注意力 自注意力机制&#xff08;Self-Attention Mechanism&#xff09;&#xff0c;也被称为内部注意力&#xff08;Intra-attention&#xff09;或并行注意力&#xff08;Parallel Attention&#xff09;&#xff0c;是一种在深度学习模型中用于处理序列数据的机制。它允许…

新版pycharm如何导入自定义环境

我们新的版本的pycharm的ui更改了&#xff0c;但是我不会导入新的环境了 我们先点击右上角的add interpreter 然后点击添加本地编译器 先导入这个bat文件 再点击load 我们就可以选择我们需要的环境了

解决毕业论文难题!推荐7款AI自动生成论文工具网站

在当今学术研究和写作领域&#xff0c;AI论文写作工具的出现极大地提高了写作效率和质量。这些工具不仅能够帮助研究人员快速生成论文草稿&#xff0c;还能进行内容优化、查重和排版等操作。以下是七款值得推荐的AI自动生成论文工具网站&#xff0c;特别推荐千笔-AIPassPaper。…

arthas简单应用

背景说明 项目上某个接口响应时间过长&#xff0c;需要查看方法耗时情况进行优化 安装配置 访问下载页进行下载&#xff1a;下载 | arthas 调整文件位置进行解压缩 - 查看arthas帮助命令&#xff08;非必须&#xff0c;官网文档更详细&#xff09; C:\tools\arthas\4.0.1\b…

移动技术开发:HandlerAsyncTask

1 实验名称 Handler&AsyncTask 2 实验目的 掌握使用Handler消息传递机制和AsyncTask处理后台线程周期性的改变Activity中界面控件的属性 3 实验源代码 布局文件代码&#xff1a; &#xff08;1&#xff09;HandlerTest <?xml version"1.0" encoding&quo…

《深度学习》—— ResNet 残差神经网络

文章目录 一、什么是ResNet&#xff1f;二、残差结构&#xff08;Residual Structure&#xff09;三、Batch Normalization&#xff08;BN----批归一化&#xff09; 一、什么是ResNet&#xff1f; ResNet 网络是在 2015年 由微软实验室中的何凯明等几位大神提出&#xff0c;斩获…