C++ vector模拟实现

news2025/1/12 20:58:55

建议将vector的模拟实现写在头文件中,测试使用部分写在.cpp文件中

vector是类模板,被封装在命名空间中

部分源码:(删除某些内容后)

vector模拟实现的代码:

#include<assert.h>
namespace djx
{
	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;
		}

		vector()//一定要写,因为我们已经写了拷贝构造了,编译器不会生成默认的构造函数,当要使用无参的构造时如果我们没写,就没有,会报错
		{}

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

		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++;
			}
		}

		void swap(vector<T>& v)
		{
			std::swap(_start, v._start);
			std::swap(_finish, v._finish);
			std::swap(_endofstorage, v._endofstorage);
		}

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

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

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

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

		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;
				_endofstorage = _start + n;
			}
		}

		void push_back(const T& val)
		{
		/*	if (_finish == _endofstorage)
			{
				reserve(capacity() == 0 ? 4 : capacity() * 2);
			}

			*_finish = val;
			_finish++;*/

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

		void resize(size_t n,const T&val=T())
		{
			if (n <= size())
			{
				_finish = _start + n;
			}
			else
			{
				reserve(n);
				while (_finish < _start + n)
				{
					*_finish = val;
					_finish++;
				}
			}
		}

		void insert(iterator pos, const T& val)
		{
			assert(pos >= _start);
			assert(pos <= _finish);
			if (_finish == _endofstorage)
			{
				size_t len = pos - _start;
				reserve(capacity() == 0 ? 4 : capacity() * 2);
				pos = _start + len;
			}

			iterator end = _finish-1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				end--;
			}

			*pos = val;
			_finish++;
		}

		iterator erase(iterator pos)
		{
			assert(pos >= _start);
			assert(pos < _finish);

			iterator begin = pos + 1;
			while (begin < _finish)
			{
				*(begin - 1) = *begin;
				begin++;
			}
			_finish--;
			return pos;
		}

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

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

	private:
		iterator _start = nullptr;//都会走构造,拷贝构造的初始化列表
		iterator _finish = nullptr;//给缺省值,就不用我们在构造,拷贝构造函数的初始化列表给值
		iterator _endofstorage = nullptr;
	};
}

vector的构造函数:

1 无参的构造

        vector()
		{}

每个在对象中的成员都要走初始化列表(它们定义的地方),对于内置类型的成员编译器不做处理,所以我们需要手动给值去初始化,但是每个构造函数要是都在初始化列表的地方给这三个成员:_start  _finish  _endofstorage 初始值,重复写太麻烦了,所以可以直接在成员们声明的地方给缺省值,在它们走初始化列表的时候编译器会用上缺省值去初始化它们

无参的构造即使没有写什么内容也一定要显示出来,否则因为我们写了其他的构造,拷贝构造函数,编译器不再生成默认的构造函数,当要调用无参的构造函数时,又没有就会报错

2 拷贝构造(深拷贝)

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

 假设我们要用v去拷贝构造生成v2

 v2的空间要和v一样大,数据元素要一样,但是各自独立

 复用push_back:push_back插入数据分方式是赋值,自定义类型的数据它们的赋值重载函数都是深拷贝,所以完美契合,但是push_back在空间不够的情况下还会自动扩容,若是我们全部使用push_back完成扩容+插入数据,所以会导致最终v2的空间大小和v不一样

解决方法:用我们实现的reserve提前一次性开和v一样大的空间,再复用push_back就仅仅是插入数据了

3 用n个val初始化

        vector(size_t n, const T& val = T())
		{
			reserve(n);//提前开n个空间
			for (size_t i = 0; i < n; i++)
			{
				push_back(val);
			}
		}

1 val可以是内置类型如int,double,也可以是自定义类型如string,vector<int>等

val给缺省值:匿名对象调用构造函数初始化,匿名对象再拷贝构造生成val(实现的拷贝构造是深拷贝)

cosnt会延长匿名对象的生命周期,当val销毁,匿名对象生命周期结束

如果给val传参,val就是实参的拷贝,不传参,val就是匿名对象的拷贝(val初始化过了)

2 复用push_back插入数据

  push_back插入数据使用赋值,对于自定义类型如string的数据,赋值重载都是深拷贝

  push_back负责扩容+插入数据

  注意:push_back我们实现的是空间不够就扩至原来的两倍

          (若是第一次插入,空间为0,就扩容至4个空间)

             所以如果全部依靠push_back插入数据的自动扩容,就会存在空间可能多于n个情况

 所以我们一开始就用reserve一次性开n个空间,那么push_back就只有插入数据的作用了

3 内置类型理论上没有默认构造函数 ,但是随着模板的引入,内置类型必须要有,因为模板是针对泛型,所以内置类型其实也有构造函数,实例:

void test()
{
	int i = 0;
	int j(1);//构造
	int k = int(2);//匿名对象先调用构造再拷贝构造k
	cout << i << " " << j << " " << k << endl;
}

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

 重载另一个版本是因为当有以下代码时会报错:

	djx::vector<int> v(10, 0);

我们的本意是用10个0去初始化v 

上一个版本的n是size_t类型,T可以推导为int类型 

但是若是我们没有写这个版本,且存在用迭代器区间初始化的构造函数时,它的两个形参类型都可以推导成int,所以编译器会调用这个构造函数,会报错

当我们写了此版本之后,编译器会调用这个版本,因为只用推导T的类型为int,两个形参类型都是int

4 用一段迭代器区间初始化 

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

设计成模板,vector<int>存int类型的数据,可以用存储int类型的对象的迭代器去初始化,也可以用string类对象的迭代器区间去初始化

测试:

void test()
{
	djx::vector<string> v(10, "xxx");
	for (auto e : v)
	{
		cout << e << endl;
	}

	djx::vector<int> v2;
	v2.push_back(10);
	v2.push_back(20);
	v2.push_back(30);
	v2.push_back(40);

	djx::vector<int> v3(v2.begin(), v2.end());
	for (auto e : v3)
	{
		cout << e << " ";
	}
	cout << endl;

	string str("hello");
	djx::vector<int> v4(str.begin(), str.end());
	for (auto e : v4)
	{
		cout << e << " ";
	}
	cout << endl;
}

vector的析构函数:

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

push_back:

	    void push_back(const T& val)
		{
			if (_finish == _endofstorage)//扩容
			{
				reserve(capacity() == 0 ? 4 : capacity() * 2);//第一次插入数据就使用push_back,此时的capacity()==0,括4个,不能是capacity()*2,因为还是0
			}

			*_finish = val;//赋值
			_finish++;

			//insert(end(), val);//或者复用insert
		}

 获取size,capacity:

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

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

迭代器:

        typedef T* iterator;//非const对象的迭代器
		typedef const T* const_iterator;//const对象的迭代器

		iterator begin()//非const对象调用
		{
			return _start;
		}

		iterator end()//非const对象调用
		{
			return _finish;
		}

		const_iterator begin()const//const对象调用
		{
			return _start;
		}

		const_iterator end()const//const对象调用
		{
			return _finish;
		}

[]运算符重载:

        T& operator[](size_t pos)//非const对象调用 读/写
		{
			assert(pos < size());
			return _start[pos];
		}

		const T& operator[](size_t pos) const//const对象调用,只读
		{
			assert(pos < size());
			return _start[pos];
		}

测试1:

void test1()
{
	djx::vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);

	for (size_t i = 0; i < v.size(); i++)
	{
		cout << v[i] << " ";
	}
	cout << endl;

	djx::vector<int>::iterator it = v.begin();
	while (it != v.end())
	{
		*it *= 10;
		cout << *it << " ";
		it++;
	}
	cout << endl;

	for (auto e : v)//有迭代器就可以使用范围for
	{
		cout << e << " ";
	}
	cout << endl;
}

 resize:

        void resize(size_t n,const T&val=T())
		{
			if (n <= size())//删除效果,保留前n个
			{
				_finish = _start + n;
			}
			else
			{
				reserve(n);//如果要扩容就扩,空间足够就不会扩
				while (_finish < _start + n)
				{
					*_finish = val;
					_finish++;
				}
			}
		}

reserve:

        void reserve(size_t n)
		{
			if (n > capacity())//如fesize复用reserve时,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;
				_endofstorage = _start + n;
			}
		}

注意1:reserve开空间因为是重新找一块空间,拷贝数据后释放原空间,所以_start,_finish,_endofstorage全部都要更新

_start=tmp,它指向新空间的起始位置

_finish = _start+size(),原本应该是要这样写的,但是会先调用size(),执行_finish-_start得到原空间的数据个数,但是现在_start已经指向新空间了,旧空间的_finish-新空间的_start是得不到旧空间的数据个数的,所以在_start改变指向之前,要先保留一下旧空间的数据个数size()

注意2:拷贝数据不能使用memcpy,因为是浅拷贝,当vector里面的数据是string类型时,新空间的数据和旧空间的数据之间,分别共用同一块资源空间,释放旧空间,那么这些共享的资源空间也一并释放了

所以可以使用赋值运算符重载,完成深拷贝

 测试2:

void test2()
{
	djx::vector<int> v;
	v.resize(10);
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;

	djx::vector<int*> v2;
	v2.resize(10);

	for (auto e : v2)
	{
		cout << e << " ";
	}
	cout << endl;

	djx::vector<string> v3;
	v3.resize(10,"xxx");
	for (auto e : v3)
	{
		cout << e << " ";
	}
	cout << endl;
}

 insert:

        void insert(iterator pos, const T& val)
		{
			assert(pos >= _start);
			assert(pos <= _finish);
			if (_finish == _endofstorage)//检查是否需要扩容
			{
				size_t len = pos - _start;
				reserve(capacity() == 0 ? 4 : capacity() * 2);
				pos = _start + len;//扩容后需要更新pos
			}

			iterator end = _finish-1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				end--;
			}

			*pos = val;
			_finish++;
		}

insert涉及迭代器失效的问题,因为insert在插入数据之前要检查是否需要扩容,一旦使用reserve扩容就会释放原有空间,pos是指向原有空间某个数据位置的迭代器,释放原有空间后,pos指向的原有空间的某个位置就不可以在访问了,它是野指针

解决方法:pos在扩容后更新,_start _finish _endofstorage都已经在新空间有所指向,pos也要指向新空间的某个位置

但是pos是传值传参,所以外面的pos并没有被真正改变,所以不可以再次使用外面的pos,它已经失效了

实例:

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

	auto it = v.begin() + 2;
	v.insert(it,20);//插入第5个数,先扩容,it失效
	//v.insert(it, 30);//不可以再次使用it,使用就崩了
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}

 测试3:

void test3()
{
	djx::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);
	v.push_back(7);

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

	djx::vector<int>::iterator it = v.begin() + 2;
	v.insert(it, 20);
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;

	v.insert(v.begin() + 3, 30);
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}

erase:

        iterator erase(iterator pos)
		{
			assert(pos >= _start);
			assert(pos < _finish);

			iterator begin = pos + 1;
			while (begin < _finish)
			{
				*(begin - 1) = *begin;
				begin++;
			}
			_finish--;
			return pos;//返回删除数据的下一个位置
		}

erase之后迭代器pos也失效了,如果迭代器失效就不能再使用这个迭代器,如果使用了,结果是未定义的

如利用erase (没有返回值的erase)删除所有的偶数:

以下代码程序崩溃:

若数组数据是 1 2 3 4 5 :正常

                       1 2 3 4 5 6 崩溃

                       2 2 3 4 5    结果不对

void test()
{
	djx::vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(5);
	v.push_back(6);


	auto it = v.begin();
	while (it != v.end())
	{
         //vs2019强制检查,erase之后认为it失效了,不能再访问,访问就报错
		if (*it % 2 == 0)
		{
			v.erase(it);
		}
		it++;
	}
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}

解决方案:使用前给it重新赋值

 库中的erase有返回值,返回的是删除数据的下一个位置

改进以上删除偶数的代码:


void test()
{
	djx::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);

	auto it = v.begin();
	while (it != v.end())
	{
		if (*it % 2 == 0)
		{
			it = v.erase(it);
		}
		else
		{
			it++;
		}
	}
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}

 

测试4:

void test4()
{
	djx::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);
	v.push_back(7);

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

	v.erase(v.begin());
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;

	v.erase(v.begin() + 3);
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}

赋值重载:

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

tmp是实参拷贝构造生成的,拷贝构造是深拷贝

再用swap交换*this对象和tmp对象,tmp销毁时调用析构函数,释放*this原本的的空间+资源空间

测试5:

void test7()
{
	djx::vector<int> v;
	v.push_back(1);
	v.push_back(1);
	v.push_back(1);
	v.push_back(1);

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

	djx::vector<int> v2(v);
	for (auto e : v2)
	{
		cout << e << " ";
	}
	cout << endl;

	djx::vector<int> v3;
	v3.push_back(10);
	v3.push_back(20);
	v3.push_back(30);
	v3.push_back(40);
	v = v3;
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}

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

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

相关文章

【Git分支操作---讲解二】

Git分支操作---讲解二 查看分支创建分支切换分支修改分支切换分支合并分支合并分支【冲突】(只会修改主分支不会修改其他分支)什么时候会有冲突&#xff1f; 查看分支 创建分支 切换分支 修改分支 切换分支 合并分支 合并分支【冲突】(只会修改主分支不会修改其他分支) 什么时…

国产精品:讯飞星火最新大模型V2.0

大家好&#xff0c;我是爱编程的喵喵。双985硕士毕业&#xff0c;现担任全栈工程师一职&#xff0c;热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。…

FL Studio2023最新版本21.1中文水果音乐编曲工具

虚拟乐器和真实乐器的区别&#xff1f;真实乐器指的是现实中需要乐手演奏的乐器&#xff0c;而虚拟乐器是计算机音乐制作中编曲师使用的数字乐器。FL Studio虚拟乐器插件有哪些&#xff1f;下文将给大家介绍几款FL Studio自带的强大虚拟乐器。 一、虚拟乐器和真实乐器的区别 …

JDK21真的来了:虚拟线程正式发布及十多项新特性!

点击下方“JavaEdge”&#xff0c;选择“设为星标” 第一时间关注技术干货&#xff01; 免责声明~ 任何文章不要过度深思&#xff01; 万事万物都经不起审视&#xff0c;因为世上没有同样的成长环境&#xff0c;也没有同样的认知水平&#xff0c;更「没有适用于所有人的解决方案…

Docker容器与虚拟化技术:Docker-Compose单机编排工具

目录 一、理论 1.Docker-Compose 二、实验 1. Docker Compose 安装部署 2.Docker Compose撰写nginx 镜像 3.Docker Compose撰写tomcat 镜像 三、问题 1.Docker Compose 和 Dockerfile 的区别 四、总结 一、理论 1.Docker-Compose &#xff08;1&#xff09;使用场景…

渗透率超90%!智能座舱赛道迎来「存量」替代升级大周期

智能座舱赛道&#xff0c;正在迎来新一轮芯片替代潮。 相比于智能驾驶领域&#xff0c;座舱主机芯片市场并不「性感」&#xff0c;但巨大的存量替代升级机会&#xff0c;也不容小视。 高工智能汽车研究院监测数据显示&#xff0c;2023年1-6月中国市场&#xff08;不含进出口&am…

通用语言模型蒸馏-GLMD

文章目录 GLMD一、PPT内容论文背景P1 BackgroundP2 Approach 相关知识P3 知识蒸馏P4 语言建模词预测逻辑 方法P5 两阶段词汇预测蒸馏P6P7 词汇压缩 实验结果P8 results 二、论文泛读2.1 论文要解决什么问题&#xff1f;2.2 论文采用了什么方法&#xff1f;2.4 论文达到什么效果…

C++学习笔记---- 引用

1、作用 给变量起别名 基本语法&#xff1a;数据类型 &别名 原名 示例&#xff1a; #include <iostream> using namespace std;int main() {int a 1;int &b a;cout << "a " << a << endl;cout << "b " <…

关于Map的理解

Shuffle中进行了分组聚合,而Reduce对分组聚合后的数据进行重新计算. 切片对应的是MapTask 分区对应的是ReduceTask 也可以通过设定reduce数量来调整分区数 分区规则: 设定为1时, 根本不走自定义分区器,而是Hash 如果分区数大于规则 空 如果分区数小于规则(分区数不为1) 报错 …

《金字塔原理》(表达的逻辑)

前言&#xff1a;在思考和表达上&#xff0c;很多时候显得很混乱&#xff0c;源于不了解结构化思想、表达&#xff0c;如何让话讲得更有逻辑&#xff1f;事做得更有条理&#xff1f;接触到了一本书&#xff1a;《金字塔原理》&#xff0c;通过这本书的学习&#xff0c;希望可以…

数字化营销怎么做?数字化营销系统落地重点一览

如何抓住数字化机遇&#xff0c;企业取得营销突破&#xff1f;如果说数字是一种技术、工具或数据等无形的资源&#xff0c;那么数字化则代表了某种动态的过程、资源和能力&#xff0c;如同企业经营一样始终处于流动的过程之中。当前&#xff0c;由于科技水平的不断发展&#xf…

神经网络入门

前言 本文主要介绍最基础的神经网络&#xff0c;包括其结构&#xff0c;学习方法&#xff0c; C \texttt{C} C 的实现代码。 Python \texttt{Python} Python 的代码可以搜索互联网得到。 前排提示&#xff1a;本人涉及一丁点数学知识。 神经网络的结构 神经网络包括多个层…

【ARM】Day8 中断

1. 思维导图 2. 实验要求&#xff1a; 实现KEY1/LEY2/KE3三个按键&#xff0c;中断触发打印一句话&#xff0c;并且灯的状态取反 key1 ----> LED3灯状态取反 key2 ----> LED2灯状态取反 key3 ----> LED1灯状态取反 key3.h #ifndef __KEY3_H__ #define __KEY3_H__#in…

每日两题 203移除链表元素 104二叉树的最大深度

203 题目 给你一个链表的头节点 head 和一个整数 val &#xff0c;请你删除链表中所有满足 Node.val val 的节点&#xff0c;并返回 新的头节点 。 示例 1&#xff1a; 输入&#xff1a;head [1,2,6,3,4,5,6], val 6 输出&#xff1a;[1,2,3,4,5]示例 2&#xff1a; 输入…

【ES】笔记-迭代器

迭代器概念 迭代器(Iterator)是一种接口&#xff0c;为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Iterator接口&#xff0c;就可以完成遍历操作。 ES6 创造了一种新的遍历命令for…of循环&#xff0c;Iterator接口主要供for…of消费原生具备iterator接口的…

Kubernetes(K8S) kubesphere 介绍

官网地址&#xff1a;https://kubesphere.com.cn/ KubeSphere 是个全栈的Kubernetes容器云PaaS解决方案 KubeSphere 是在目前主流容器调度平台 Kubernetes 之上构建的企业级分布式多租户容器平台&#xff0c;提供简单易用的操作界面以及向导式操作方式&#xff0c;在降低用户使…

数据封装与解封装过程

2.2数据封装与解封装过程(二) 如果网络世界只有终端设备&#xff0c;那么将不能称之为网络。正因为有很多中转设备才形成了今天如此复杂的Internet网络&#xff0c;只不过一贯作为网络用户的我们没有机会感知它们的存在&#xff0c;这都是传输层的“功劳”&#xff0c;由于传输…

【C语言】自定义数据类型:枚举+共用体

前言 之前我们学习了自定义数据类型里的结构体&#xff0c;还顺带着学习了位段 今天&#xff0c;我们就来学习枚举和共用体&#xff0c;我们开始吧 自定义类型&#xff1a;结构体 位段详解 枚举 枚举就是一一列举 枚举类型的定义 枚举类型的创建 enum Day {mon,//此处是…

【前端从0开始】JavaSript——自定义函数

函数 函数是一个可重用的代码块&#xff0c;用来完成某个特定功能。每当需要反复执行一段代码时&#xff0c;可以利用函数来避免重复书写相同代码。函数包含着的代码只能在函数被调用时才会执行&#xff0c;就可以避免页面载入时执行该脚本在JavaScript中&#xff0c;可以使用…

ChatGpt开源项目完美运行配置(ChatGml2)

&#xff08;以下所有软件均可免费在网盘获取。&#xff09; 任务描述 本节任务是安装和配置chatgpt项目所需的软件以及chatgpt项目所需要的python库包&#xff0c;同时编写python代码来完成chatgpt项目的人机对话功能。 实验工具 显卡GTX1070&#xff08;专用内存需要大于等…