【c++】:STL中vector的模拟使用及模拟实现
文章目录
- 前言
- 一.使用库中vector常用接口
- 二.vector的模拟实现
- 总结
前言
上一篇我们讲解了STL中的string的使用和模拟实现,这次我们就来讲解STL中的vector,vector相对于string来说模拟实现会难一些,难点在于迭代器失效问题和深浅拷贝问题。
首先介绍一下vector:
一、使用库中的vector的常用接口
1.首先是对象的创建,使用vector必须确定类型否则会报错,push_back就是尾插一个数据,size()是求一个对象的长度与string相同,我们实现过string知道string会多开一个空间放\0,但是vector的capacity就是实际空间。

2.vector的迭代器与string的使用方法相同,如下图:

3.reserve与string中的一样,只开空间改变capacity
4.resize与string一样,改变空间并且初始化,如果空间小于原先的size就删除多余的数据:

5.pop_back是尾删

6.insert 通常是配合find去使用

int main()
{vector<int> v;v.push_back(1);v.push_back(2);v.push_back(3);v.push_back(4);vector<int>::iterator pos = find(v.begin(), v.end(), 2);if (pos != v.end()){v.insert(pos, 50);}vector<int>::iterator it = v.begin();while (it != v.end()){cout << *it << " ";it++;}cout << endl;return 0;
}

上面我们是用find函数找到2的位置,然后将50插入到2的位置。
7.erase是删除一个数据,与insert一样要搭配使用


我们修改刚刚的代码将插入改为删除,就成功删除了2这个数据,当然也可以删除多个需要迭代器实现:

以上就是一些常用的vector的接口,vector的接口不再像string那样太冗余,我们可以通过下图看到库中vector的接口也不是很多:

二、vector的模拟实现
vector的模拟实现与string一样,我们都会按照库中的源码进行实现但也不是完全的模仿源码,因为源码的封装性太强而我们并不需要像源码那样封装。

首先源码中的开头是禁止有人将源码售卖等并且有改动的话需要公开,对于源码来说我们只看最核心的那部分,至于其他的编译条件语句什么的我们并不关心。

上图是源码中的成员变量,最重要的是iterator,这里还不容易看出来,我们再看下一张图:

相信大家已经看出来了吧,实际上库中的vector底层就是原生指针,只不过通过封装将指针名重命名为value_type,然后又将指针重命名为iterator,我们可以看到一共三个指针,_start,_finish,_end_of_storage通过名称我们可以辨认它们三个作用,再结合size()和capacity()的实现我们就能知道start指向起始位置,finish指向最后一个数据的后一个位置,因为只有这样指针相减才是size大小,而end很明显就是容量了。如下图所示:

知道了底层是如何实现的我们就可以来模拟实现了,首先为了防止命名冲突我们将要模拟的vector写到我们自己的命名空间里。
namespace sxy
{template<class T>class vector{public:typedef T* iterator;private:iterator _start;iterator _finish;iterator _end_of_storage;};
}
在这里我们用模板的目的是创造一个任意类型的指针,因为我们的vector是可以放任意类型的。首先我们typedef一个iterator来代表T*的指针,然后私有成员写出和源码一样的三个成员变量。
然后我们先写出size()和capacity()函数,如下:
size_t size() const{return _finish - _start;}size_t capacity() const{return _end_of_storage - _start;}
我们之前说过,当函数内不涉及修改成员变量时就加上const,这样普通对象和const对象就都能调用这个函数了。我们在实现一个判断是否为空的函数:
bool empty() const{return _start == _finish;}
要判断是否为空很简单,只需要看start是否等于finish,因为如果有数据finish会指向最后一个数据的下一个位置。
然后我们再实现一下[]符号的重载,因为遍历数组最方便的就是[]:
T& operator[](size_t pos){assert(pos < size());return _start[pos];}const T& operator[](size_t pos) const{assert(pos < size());return _start[pos];}
需要注意的是我们返回的对象是T类型的,前面我们说过vector是一个模板任何类型都能够使用所以我们不能只单单返回一个int类型,【】访问需要实现const版本的,因为有的静态对象也需要访问数据,这里我们在string中都讲解过就不再赘述。
我们先实现一个简单的构造函数,能完成初始化指针就可以:
vector():_start(nullptr),_finish(nullptr),_end_of_storage(nullptr){}
初始化我们能在初始化列表初始化就在初始化列表初始化。
下面开始进入正题了,我们先实现push_back接口,对于这个接口我们需要考虑的是扩容问题:
void push_back(const T& x){if (_finish==_end_of_storage){reserve(capacity() == 0 ? 4 : 2 * capacity());}*_finish = x;++_finish;}
当finish等于end_of_storage时就需要扩容了,扩容的时候要注意不能直接扩容2倍的空间,因为刚开始capacity是有可能为0的,所以我们用一个三目操作符来解决这个问题,当容量为0时就开4个空间,否则就是2倍空间。当成功开完空间后我们就将要插入的值放入finish的位置然后让finish自加。下面我们来讲解扩容的问题:
void reserve(size_t n){if (n > capacity()){size_t sz = size();T* tmp = new T[n];if (_start){for (size_t i = 0; i < sz; i++){tmp[i] = _start[i];}delete[] _start;}_start = tmp;_finish = _start + sz;_end_of_storage = _start + n;}}
在我们实现string的时候扩容什么的接口我们可以用memcpy等函数直接搞定那是因为string中的类型是char是内置类型,而vector是一个针对任意类型的模板,如果我们还用memcpy这样的浅拷贝的话是一定会出问题的,比如vector<string>里面存放的是string,如果用memcpy的话就导致vector里的每一个string对象都指向同一块空间,如下图:

这样在我们析构的时候释放空间就会将原来的空间释放两次程序就崩溃了,更具体的深浅拷贝问题我们在文章的最后面会更详细的讲解, 现在的实现大家只需要记住让对象一个一个去赋值即可。
reserve的实现与string中的一样,只有在重新开的空间大于原先的空间的时候我们才进行扩容,先用一个T类型的指针tmp开一个和原先capacity一样的空间,如果原先是有空间的不是空指针我们就将原先空间的值依次赋值给新空间,然后再将旧空间释放掉,在这里大家会有疑问,为什么要用一个变量提前记录size()的大小呢?我们画个图给大家解释一下:

我们实现size()函数的时候使用finish-start的,而当我们开新空间后,size()的计算还是用旧空间上的指针来计算的,当我们将新空间给start后,这个时候的size()是计算不出来的如下图:

我们很明显的看到两个指针都不在同一块空间上,计算出来的size()不知道有多大,也就是说将新空间交给start后原先的finish失效了计算不出来真实的size()了,这个时候我们就需要提前记录size(),由于finish的位置是start+size()所以我们提前记录size()。而end_of_storage的位置也变了,在这里一定要注意end_of_storage = _start + n而不是end_of_storage = _start + capacity,因为我们扩容空间已经将空间改变了。
实现了push_back后我们就可以实现pop_back了,pop_back就非常的简单了:
void pop_back(){assert(!empty());--_finish;}
尾删我们只需要让finish--向前移动一个位置即可,这个时候我们写的empty就派上用场了,我们尾删一定不可以一直让finish--,所以只有当不为空时才可以尾删。
下面我们实现一下迭代器,迭代器的实现与string没有很大的差别:
namespace sxy
{template<class T>class vector{public: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;}private:iterator _start;iterator _finish;iterator _end_of_storage;};
}
同样我们要实现普通迭代器和const迭代器,这里与string无任何差别就不在介绍了。
接下来是resize接口,resize与string中的一样,当要开的空间小于capacity,就会将多余的数据删掉,当要开的空间大于capacity的时候,就要开空间并且初始化。
void resize(size_t n, T val = T()){if (n < size()){_finish = _start + n;}else{if (n > capacity()){reserve(n);}while (_finish != _start + n){*_finish = val;++_finish;}}}
当开的空间小于size()的时候我们就删掉多余的空间,这里为什么是小于size()而不是小于capacity呢?我们看下图:

我们可以看到如果开的空间小于capacity就删除数据就会有这样的问题,finish往后移动了而实际的数据不仅没删还多出了空白空间,而用size()就不会发生这个问题,当开的空间小于size()的时候我们直接将finish移到start+n的位置,如下图:

当要开的空间大于capacity的时候我们就扩容,扩容后从finish的位置开始填要初始化的数据,这里要初始化的数据不可以给0,因为很有可能vector里面放的自定义类型,自定义类型必须用其构造函数进行初始化,所以大家可以看到val的缺省值我们给的是T(),这个我们之前讲过这是一个匿名对象。
接下里介绍vector的重点insert接口和erase,为什么是重点呢?因为这两个接口会引起经典的迭代器失效问题。
iterator insert(iterator pos, const T& val){assert(pos >= _start);assert(pos <= _finish);if (_finish == _end_of_storage){size_t len = pos - _start;reserve(capacity() == 0 ? 4 : 2 * capacity());//如果扩容pos指针将会失效所以需要重置pos的位置pos = _start + len;}iterator end = _finish - 1;while (end >= pos){*(end + 1) = *end;--end;}*pos = val;++_finish;return pos;}
我们实现的insert只有从pos位置插入一个值即可,当然插入是有条件的不能随便插入,只有当要插入的位置大于等于start位置,小于等于finish位置才可以插入,等于start位置是头插,等于finish位置是尾插,当finish等于end_of_storage的时候我们就扩容,这里先不解释为什么要记录len等会我们画图解释一下,插入很简单,我们只需要让一个指针指向最后一个数据的位置,当end大于等于pos位置我们就把end位置上的数据放在end+1的位置上,然后让end--,当所有数据移完后我们就将要插入的数据放到pos位置,然后让finish++即可。如下图:

下面我们讲解失效问题,从上面代码大家应该也看出来了,失效只出现在扩容后如下图:

当我们扩容后start finish end_of_storage这三个指针都到了新空间上,而pos指针还指向旧空间,如果这个时候我们再往这个pos位置插入数据呢?答案显而易见,原先的那块空间都已经释放了再插入数据不就非法访问了吗,所以我们再扩容前先用len变量记录了pos的位置,然后在扩容后重置pos位置。
大家看下面这段代码的结果:
void test6(){vector<int> v1;v1.push_back(1);v1.push_back(2);v1.push_back(3);v1.push_back(4);v1.push_back(5);for (auto ch : v1){cout << ch << " ";}cout << endl;v1.insert(v1.begin(), 0);auto pos = find(v1.begin(), v1.end(), 3);if (pos != v1.end()){v1.insert(pos, 30);}for (auto ch : v1){cout << ch << " ";}cout << endl;(*pos)++;for (auto ch : v1){cout << ch << " ";}cout << endl;}

我们的目的是在3的位置++,按道理应该是0 1 2 30 4 4 5才对,为什么加的是30这个位置呢?这是因为形参不能改变实参,我们在insert函数里面虽然已经更新了pos位置,但是insert函数内部的pos是形参是一份临时拷贝根本不会影响到test6()中的pos位置,这个pos位置还是指向原先那个位置的。那么如何解决这个问题呢?我们只需要让其insert多一个返回值返回pos位置即可。那么我们既然想要函数内改变引起函数外的改变为什么不用引用传pos呢?如果用引用这里是会报错的,iterator我们设计的时候是传值返回,传值返回会临时拷贝一个对象,临时对象具有常性是不能用引用的。
下面我们讲解erase接口:
iterator erase(iterator pos){assert(pos >= _start);assert(pos < _finish);iterator end = pos + 1;while (end != _finish){*(end - 1) = *end;++end;}--_finish;return pos;}
erase的接口实现起来很简单,首先判断要删除的位置是否大于等于start并且小于finish。然后我们用一个指针指向pos+1的位置,只要end不等于finish我们就将end位置的值放入end-1的位置,移完数据后让finish--即可,这里要向insert一样返回pos位置,如下图:

那么在什么情况下erase会导致迭代器失效失效呢?如下图:
通过上面的图片我们应该了解了迭代器失效问题,下面我们讲几个简单的接口:
void swap(vector<T>& v){std::swap(_start, v._start);std::swap(_finish, v._finish);std::swap(_end_of_storage, v._end_of_storage);}
与某个vector<T>类型的对象交换,我们传参传的是引用,直接用库中的swap即可,将指针交换指向即可。
析构函数:
~vector(){delete[] _start;_start = _finish = _end_of_storage = nullptr;}
析构我们直接delete掉start的空间,然后将三个指针置为空。
拷贝构造函数:
vector(const vector<T>& v){_start = new T[v.capacity()];for (size_t i = 0; i < v.size(); i++){_start[i]=v._start[i];}_finish = _start + v.size();_end_of_storage = _start + v.capacity();}
对于拷贝构造,我们直接给start开capacity大小的空间,然后将v中的数据依次挨个赋值给start,将数据都传到start后我们要将finish放在start+ size()的位置,然后将end_of_storage的位置放在start+capacity()的位置。
拷贝构造我们实现了后赋值就非常简单了。
赋值运算符重载:
vector<T>& operator=(vector<T> v){swap(v);return *this;}
我们直接传值传参,这样就会调用拷贝构造函数生成一份临时拷贝,然后用这份临时拷贝和*this交换,交换后*this就拥有了与v同样的数据,然后我们直接返回*this即可,这里既然我们要swap就一定不要穿引用传参,因为传引用会改变v的值,而赋值操作符是不会改变右边操作数的值的。
用n个val初始化的构造函数:
vector(int n, const T& val = T()):_start(nullptr), _finish(nullptr), _end_of_storage(nullptr){reserve(n);for (size_t i = 0; i < n; i++){push_back(val);}}
首先我们在初始化列表将三个指针初始化为空指针,然后提前开n个大小的空间,然后依次将初始化的valpush_back到空间中,这里我们用的依旧是匿名对象做缺省值。
迭代器构造函数:
template<class InputIterator>vector(InputIterator first, InputIterator last){while (first != last){push_back(*first);++first;}}
迭代器的构造函数为什么我们用重新弄一个模板呢?这是因为我们用迭代器初始化不是一定要用vector的迭代器去初始化,我们还可能用string的迭代器去初始化,所以这里为了能使用各种迭代器我们直接弄一个迭代器模板,当first不等于last我们就push_back数据,然后让first++即可。
下面我们再来详细的讲解一下深拷贝问题:

如上图所示的代码,如果我们用memcpy浅拷贝去扩容,去拷贝构造会发生什么呢?

这个时候虽然vector的两个对象不在同一块空间,但是vector的每个string对象却指向了同一块空间,那么为什么我们用memcpy不行,用挨个赋值的方式就可以呢?这是因为string中对于赋值的运算符重载是深拷贝,是会给被赋值的对象先开好空间再将数据依次放入空间中,而我们的vector在扩容,拷贝构造函数中都用依次赋值的方法,只有我们将赋值运算符重载了变成深拷贝了才能解决vector的浅拷贝问题。下图是挨个赋值后的样子:

这样我们就将vector模拟实现完成了。
总结
vector的模拟实现相对于string来说会稍微难一些,不知道大家会不会有疑问,为什么string没有迭代器失效问题呢?因为我们访问string是采用下标的形式,而vector用的是指针。对于vector,我们学的重点就是迭代器失效问题以及深浅拷贝问题,这里尤其是深浅拷贝问题一不小心就会发现程序莫名其妙崩溃,对于vector的学习建议大家一定要多练习并且配合画图来理解。
相关文章:
【c++】:STL中vector的模拟使用及模拟实现
文章目录 前言一.使用库中vector常用接口二.vector的模拟实现总结前言 上一篇我们讲解了STL中的string的使用和模拟实现,这次我们就来讲解STL中的vector,vector相对于string来说模拟实现会难一些,难点在于迭代器失效问题和深浅拷贝问题。 首…...
C++ STL:vector的使用方法及模拟实现
目录 一. vector概述 二. vector接口函数的使用方法和模拟实现 2.1 vector类模板的成员变量 2.2 构造函数的使用和模拟实现 2.2.1 构造函数的使用方法 2.2.2 构造函数的模拟实现 2.3 析构函数的模拟实现 2.4 赋值运算符重载函数的使用和模拟实现 2.4.1 函数的使用 2.…...
naive UI 的upload组件自定义手动上传图片的base64位
<template><n-upload ref"uploadRef" action"#" :default-upload"false" :custom-request"myUpload"><n-button>点击选择文件</n-button></n-upload><n-button click"submitUpload"> 上…...
信创办公–基于WPS的PPT最佳实践系列(表格和图标常用动画)
信创办公–基于WPS的PPT最佳实践系列(表格和图标常用动画) 目录应用背景操作步骤图表常用动画效果:擦除效果表格常用动画效果:轮子效果应用背景 文不如表,表不如图。在平时用ppt做总结时,我们会经常用到图…...
Spring Bean实例化和初始化的过程
承接上文Spring Bean生命周期应用程序在运行过程中能否去读取当前系统的环境变量或系统属性?这里涉及到一个非常重要的接口Environment,System.getenv,System.getProperties都是获取当前系统环境变量,Environment接口的实现类AbstractEnviro…...
WorkTool企微机器人接入智能问答
一、前言 最新版的企微机器人已经集成 Chat ,无需开发可快速搭建智能对话机器人。 从官方介绍看目前集成版本使用模型为 3.5-turbo。 二、入门 创建 WorkTool 机器人 你可以通过这篇快速入门教程,来快速配置一个自己的企微机器人。 实现的流程如图&…...
C导入正则库问题
环境 操作系统:win11 专业版 gcc: gcc (x86_64-posix-seh-rev0, Built by MinGW-W64 project) 8.1.0 编辑器:vscode 要求 在c中使用正则表达式 遇到的问题以及解决思路 C标准中并没有正则表达式库 从其他地方下载正则表达式库即可。 http://gnuwin32.sourcefo…...
尚融宝05-Node.js入门
目录 一、Node.js的概念 1、JavaScript引擎 2、什么是Node.js 二、下载和安装 1、下载和安装 2、查看安装是否成功 三、初始Node.js程序 1、运行一个程序 常见问题 2、文件的读取 3、服务器端程序 三、Node.js的作用 1、Node.js的应用场景 2、BFF 解决什么问题 …...
「SAP ABAP」OPEN SQL(八)【WHERE语句大全】
💂作者简介: THUNDER王,一名热爱财税和SAP ABAP编程以及热爱分享的博主。目前于江西师范大学本科在读,同时任汉硕云(广东)科技有限公司ABAP开发顾问。在学习工作中,我通常使用偏后端的开发语言A…...
Ribbon负载均衡的原理(源码分析)
SpringCloud底层其实是利用了一个名为Ribbon的组件,来实现负载均衡功能的。1)LoadBalancerIntercepor可以看到这里的intercept方法,拦截了用户的HttpRequest请求,然后做了几件事:1.request.getURI():获取请…...
用sql计算两个经纬度坐标距离(米数互转)
目录 一、sql示例(由近到远) 二 、参数讲解 三、查询效果 - 距离(公里 / 千米) 四、查询效果 - 距离(米) 五、距离四舍五入保留后2位小数(java) 一、sql示例(由近到远…...
C语言详解KMP算法
如果给你一个字符串 和 该字符串的一个子字符串 你能否快速找出该子字符串的所在位置我猜 这里会有一群杠精 说可以找到 真的吗 那下面这个字符串你可以一眼看出来吗你能找出来吗 如果能 算你眼神好 如果不能 那就看看接下来我怎么做你有想到暴力求解法吗?——来自百…...
redis在window上安装与自启动
需求: 客户window服务器使用redis,需要配置成在window服务中,并且可以随着电脑自启动服务。 下载 https://github.com/tporadowski/redis/releases打开上面的下载地址,这里我们下载zip压缩版本。 解压到待安装目录下ÿ…...
字符串匹配【BF、KMP算法】
文章目录:star:BF算法代码实现BF的改进思路:star:KMP算法🚩next数组🚩代码实现优化next数组最终代码⭐️BF算法 BF算法,即暴力(Brute Force)算法,是普通的模式匹配算法,BF算法的思想就是将主串S的第一个字符与模式串P…...
Leetcode.1616 分割两个字符串得到回文串
题目链接 Leetcode.1616 分割两个字符串得到回文串 Rating : 1868 题目描述 给你两个字符串 a和 b,它们长度相同。请你选择一个下标,将两个字符串都在 相同的下标 分割开。由 a可以得到两个字符串: aprefix和 asuffix,…...
剑指 Offer II 033. 变位词组
题目链接 剑指 Offer II 033. 变位词组 mid 题目描述 给定一个字符串数组 strs,将 变位词 组合在一起。 可以按任意顺序返回结果列表。 注意:若两个字符串中每个字符出现的次数都相同,则称它们互为变位词。 示例 1: 输入: strs [“eat”,…...
spring-cloud-sentinel ---流控算法---review
计数器算法 计数器算法,限定每个固定时间能处理的请求总数,例如1分钟100,如果在第一个一分钟,总共请求60次,接着第二个一分钟,counter又会从0 开始技术,如果在1.5分钟的时候,达到了…...
1.浅析NIO 多路复用器selector
一:IO基本介绍 Java共支持3种网络编程IO模式:BIO,NIO,AIO 0.Java对BIO、NIO、AIO的支持: Java BIO : 同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端…...
Day920.结构化日志业务审计日志 -SpringBoot与K8s云原生微服务实践
结构化日志&业务审计日志 Hi,我是阿昌,今天学习记录的是关于结构化日志&业务审计日志的内容。 1、什么是结构化日志 结构化日志(Structured Logging)是一种将日志信息组织为结构化数据的技术。 传统的日志通常是一些文…...
前端代码复用学习笔记:整洁架构与清晰架构
基础代码的复用往往比较简单,但是业务代码的复用通常是困难的,如果没有特殊的手段去治理项目会逐渐发展为难以维护的巨石应用,按照维基百科记载,代码的复用形式主要有三种,程序库,应用框架,设计…...
linux之kylin系统nginx的安装
一、nginx的作用 1.可做高性能的web服务器 直接处理静态资源(HTML/CSS/图片等),响应速度远超传统服务器类似apache支持高并发连接 2.反向代理服务器 隐藏后端服务器IP地址,提高安全性 3.负载均衡服务器 支持多种策略分发流量…...
K8S认证|CKS题库+答案| 11. AppArmor
目录 11. AppArmor 免费获取并激活 CKA_v1.31_模拟系统 题目 开始操作: 1)、切换集群 2)、切换节点 3)、切换到 apparmor 的目录 4)、执行 apparmor 策略模块 5)、修改 pod 文件 6)、…...
Qt Widget类解析与代码注释
#include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this); }Widget::~Widget() {delete ui; }//解释这串代码,写上注释 当然可以!这段代码是 Qt …...
在四层代理中还原真实客户端ngx_stream_realip_module
一、模块原理与价值 PROXY Protocol 回溯 第三方负载均衡(如 HAProxy、AWS NLB、阿里 SLB)发起上游连接时,将真实客户端 IP/Port 写入 PROXY Protocol v1/v2 头。Stream 层接收到头部后,ngx_stream_realip_module 从中提取原始信息…...
土地利用/土地覆盖遥感解译与基于CLUE模型未来变化情景预测;从基础到高级,涵盖ArcGIS数据处理、ENVI遥感解译与CLUE模型情景模拟等
🔍 土地利用/土地覆盖数据是生态、环境和气象等诸多领域模型的关键输入参数。通过遥感影像解译技术,可以精准获取历史或当前任何一个区域的土地利用/土地覆盖情况。这些数据不仅能够用于评估区域生态环境的变化趋势,还能有效评价重大生态工程…...
select、poll、epoll 与 Reactor 模式
在高并发网络编程领域,高效处理大量连接和 I/O 事件是系统性能的关键。select、poll、epoll 作为 I/O 多路复用技术的代表,以及基于它们实现的 Reactor 模式,为开发者提供了强大的工具。本文将深入探讨这些技术的底层原理、优缺点。 一、I…...
Python 包管理器 uv 介绍
Python 包管理器 uv 全面介绍 uv 是由 Astral(热门工具 Ruff 的开发者)推出的下一代高性能 Python 包管理器和构建工具,用 Rust 编写。它旨在解决传统工具(如 pip、virtualenv、pip-tools)的性能瓶颈,同时…...
Fabric V2.5 通用溯源系统——增加图片上传与下载功能
fabric-trace项目在发布一年后,部署量已突破1000次,为支持更多场景,现新增支持图片信息上链,本文对图片上传、下载功能代码进行梳理,包含智能合约、后端、前端部分。 一、智能合约修改 为了增加图片信息上链溯源,需要对底层数据结构进行修改,在此对智能合约中的农产品数…...
LOOI机器人的技术实现解析:从手势识别到边缘检测
LOOI机器人作为一款创新的AI硬件产品,通过将智能手机转变为具有情感交互能力的桌面机器人,展示了前沿AI技术与传统硬件设计的完美结合。作为AI与玩具领域的专家,我将全面解析LOOI的技术实现架构,特别是其手势识别、物体识别和环境…...
tomcat指定使用的jdk版本
说明 有时候需要对tomcat配置指定的jdk版本号,此时,我们可以通过以下方式进行配置 设置方式 找到tomcat的bin目录中的setclasspath.bat。如果是linux系统则是setclasspath.sh set JAVA_HOMEC:\Program Files\Java\jdk8 set JRE_HOMEC:\Program Files…...
