C++ vector 模拟实现

news2024/11/17 7:54:29

vector的底层也是一个动态数组,他与 string 的区别就是,string 是专门用来存储字符类数据的,为了兼容C语言,使用C语言的接口,在string的动态数组内都会都开一块空间用来存 \0  ,而vector则不会。

首先我们要知道的就是,vector是一个类模板,他的内存管理是使用空间配置器,我们就简单点直接使用new和delete来管理内存。其他的一些接口什么的,vector与string差别不大,vector有的string基本都有,实现起来我们也挑一些重点来实现。而对于vector的成员变量,我们则不是采用string的一个指针两个整型的实现,而是使用三个迭代器,_start,_finish,_end_of_storage,这是参考Linux的库的实现,而我们实现vector时迭代器就直接使用的原生的指针。

_start 迭代器是数组的开头,_finish是最后一个数据的下一个位置,_end_of_storage指向的是申请的内存块的下一个位置。

使用三个迭代器的实现,在后续需要挪数据的时候会很方便。

无参构造函数与析构

无参构造不用说,很简单也没什么含量,都初始化为空指针就行了

		vector()
			:_start(nullptr)
			,_finish(nullptr)
			,_end_of_storage(nullptr)
		{
		}

完成了无参构造函数之后,我们先去实现其他的一些基本的函数,最后再来解决其他的三个构造

析构函数就是释放_start指向的空间

        //析构
		~vector()
		{
			delete[]_start;
			_start = _finish = _end_of_storage = nullptr;
		}

size与capacity和判空

size与capacity的实现其实很简答,我们之前学过数组中同类型指针相减,得到的是这两个指针之间的该类型的数据的个数,于是我们就可以用这三个迭代器来得到size和capacity

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

		//capacity
		size_t capacity()const
		{
			return _end_of_storage - _start;
		}
        
        //判空
	    bool empty()const
		{
			return _start == _finish;
		}

方括号的重载与at

vector由于还是动态数组实现的,所以我们还是支持直接是用下标访问的方式,所以方括号重载的实现还是有必要的,方括号重载的越界检查我们直接使用assert暴力检查,库里面实现的就是assert。同时at的功能虽然和方括号一样,但是他和方括号的区别在于 : 1 方括号是运算符重载,而at是普通的成员函数。2 越界的检查不一样,方括号的越界检查使用assert,而at是抛异常,我们可使用vs的库试一下

由于我们还没学习异常。也直接用assert判断就行了。同时,由于有普通对象和const对象的访问,返回的引用的权限不一样,所以我们需要实现两个版本

		//普通对象  [ ]
		reference operator[](size_t pos)
		{
			assert(pos < size());
			return _start[pos];
		}

		//const对象 [ ] 
		const_reference operator[](size_t pos)const
		{
			assert(pos < size());
			return _start[pos];
		}

		//普通对象 at
		reference at(size_t pos)
		{
			assert(pos < size());
			return _start[pos];
		}
		//const对象 at
		const_reference at(size_t pos)const
		{
			assert(pos < size());
			return _start[pos];
		}

对于下标为pos位置的数据的访问,我们可以直接使用 _start[pos]来访问,等价于*(_start+pos)

由于vector也就是普通的数组的比较是没有意义的,所以一般只重载[ ] 和=

swap

为什么要实现swap呢?

首先vector的库里面是有swap的,用于交换两个对象的数据,但是算法库里面也有swap,

他们的区别在于,算法库里的swap有三次拷贝构造,如果要交换的是两个自定义类型的对象,代价很大,而我们的类里面的成员函数swap,我们可以使用算法库的swap对对象里的成员变量进行交换,避免了深拷贝。


		//swap
		void swap(rvector<T>& v)
		{
			//要使用算法库里面的swap要指定命名空间域,否则会优先匹配当前命名空间的swap函数
			std::swap(_start, v._start);
			std::swap(_finish, v._finish);
			std::swap(_end_of_storage, v._end_of_storage);
		}

同时我们实现swap也是为了下面的拷贝构造和赋值重载做准备

赋值重载

vector的赋值重载显然是深拷贝,我们有一种很简洁的代码写法来完成这个深拷贝,就是利用拷贝构造,我们可以传值传参,将我们的this与生成的临时的形参进行交换

而当v2不为空时也是一样的,无非就是将这临时对象与当前对象的空间换了一下,出作用域之后,临时对象指向的空间就被释放了,不会影响外面等号右边的变量。

        //赋值重载
		vector<T>& operator=(vector<T> v)
		{
			swap(v);
			return *this;
		}

push_back和pop_back

尾插的实现就很简单了,首先检查是否扩容,然后直接在 _finish位置插入数据并且更新 _finish就行了。而尾删的操作则是判断是否为空,然后直接 -- _finish 就行了。

		void push_back(const_reference x)
		{
			if (_finish == _end_of_storage)
			{
				size_t newcapacity = capacity() == 0 ? 4 : 2 * capacity();
				reserve(newcapacity);
			}
			*_finish = x;;
			++_finish;

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

		void pop_back()
		{
			assert(size()>0);
			--_finish;
		}

拷贝构造

我们需要考虑当 T 为需要深拷贝的自定义类型这种情况,要做深层次的深拷贝。

如果数据只是内置类型或者只需要浅拷贝的自定义类型的话,我们就只需要memcpy就行了,但是如果是需要深拷贝的数据的话,比如 vector<vector<int>> ,数据类型是 vector<int> ,需要深拷贝,如果只是简单的memcpy

	//如果只是内置类型或者浅拷贝自定义类型
			//先扩容
			reserve(v.capacity());//函数内部会修改_end_of_storage
			//直接内存拷贝
			memcpy(_start,v._start,v.size()*sizeof(T));
			_finish = _start + v.size();

我们发现这时候两个对象虽然是不同的空间,但是里面的 vector<int> 数据指向的空间是同一块,这其实还没有达到我们想要的深拷贝。

对于我们来说目前也没有什么更好的解决方法,可以利用上面实现的赋值重载,赋值重载去对每一个数据都深拷贝,


		//拷贝构造
		vector(const vector<T>& v)
			:_start(nullptr)
			,_finish(nullptr)
			,_end_of_storage(nullptr)
		{
			//先扩容
			reserve(v.capacity());
			//或者先扩size,再使用赋值重载进行深拷贝
			_finish = _start + v.size();
			for (size_t i = 0; i < size(); ++i)
			{
				_start[i] = v[i];
			}
		}

同时,拷贝构造完成之后,不管再多层,他们都有对应的拷贝构造,但是可能有人会对这里的赋值重载产生疑惑,认为赋值重载的前提是实现拷贝构造,而我们这里的拷贝构造又用了赋值重载,会不会递归死循环了,其实是不会的,就比如这里的vector<vector<int>> ,会生成 vector<int> 和vector<vector<int>>的模板,而在vector<vector<int>>中拷贝构造的赋值重载是属于 vector<int>实例出来的赋值重载,而这个赋值重载调用的是 int 的拷贝构造,也就是内置类型的传值调用,所以这里的逻辑是没有问题的,拷贝构造和赋值重载的互相调用会在内置类型(或者浅拷贝的自定义类型)停下来,然后回归。总之就是 赋值重载的深拷贝是调用 T 类型的拷贝构造

reserve

reserve函数与string一样,只会扩容,不会缩容,同时不会改变size。但是扩容之后需要解决将原空间的数据拷贝到新空间的问题,我们可以直接使用指针来进行边界控制。

但是同时,我们也要考虑 数据类型是需要深拷贝的自定义类型的情况,比如 vector<vector<int>> ,如果只是单纯的memcpy,也会造成浅拷贝的问题,也就是原空间的 vector<int>对象与新空间的vector<int>对象指向相同的内存空间,当时放掉原空间之后,新空间指向的内存也被释放掉了。

解决方法还是跟上面的深拷贝一样,使用数据自身的赋值重载来拷贝,虽然效率低了点,但是能确保不会出问题。

迭代器相关

		//迭代器
		iterator begin()
		{
			return _start;
		}

		iterator end()
		{
			return _finish;
		}

		const iterator cbegin()
		{
			return _start;
		}

		const iterator cend()
		{
			return _finish;
		}

insert和erase

		iterator insert(iterator pos, const_reference x=val_type())//缺省值
		{
			//检查越界
			assert(pos >= _start);
			assert(pos <= _finish);//第一次插入pos==_finish  同时要支持尾插
			
			//检查是否扩容
			if (_finish == _end_of_storage)
			{
				//扩容之后pos就失效了,所以要记录pos的相对位置
				size_t pos_index = pos - _start;
				size_t newcapacity = (capacity() == 0 ? 4 : 2 * capacity());
				reserve(newcapacity);

				//更新pos
				pos = _start + pos_index;
			}
			
			//挪动数据 从后往前,往后挪数据
			iterator end = _finish;
			while (end != pos)
			{
				*end = *(end - 1);
				--end;
			}
			//更新_finish
			++_finish;

			//插入新数据
			*pos = x;

			//返回插入的数据的迭代器
			return pos;
		}

		iterator erase(iterator pos)
		{
			assert(pos >= _start);
			assert(pos < _finish);
			//挪数据覆盖
			iterator begin = pos+1;
			while (begin < _finish)
			{
				*(begin - 1) = *begin;
				begin++;
			}
			//返回删除的下一个数据
			return pos;
		}

insert和erase都要注意迭代器失效的问题,比如insert的时候,如果扩容了,那么传过来的pos指向的空间就可能已经被释放了,所以要更新pos指向新空间的该元素的位置,同时,正常来说insert之后,函数外面的pos就不能用了,因为我们是传值的,函数内更新了pos不会影响函数外的pos,这时候如果我们要更新函数外面的pos,可以insert返回心得pos的值。  erase也是一样的,因为erase可能是删除最后一个数据,这时候pos的位置就是_finish 了,虽然pos没有出我们开辟的内存空间,但是在我们看来,它指向finish就已经算是越界了,不能继续使用,同样,要在函数外面更新pos也是用erase的返回值更新。

resize

		//resize
		void resize(size_t n, val_type x = val_type())//缺省参数
		{
			//扩容检查
			if (n > capacity())
			{
				reserve(n);
			}
			//是否需要加数据
			if (n > size())
			{
				while (_finish < _start + n)
				{
					*_finish=x;
					++_finish;
				}
			}
		}

剩下的两个构造函数

一个是迭代器区间来构造初始化。为什么这里的构造函数是模板呢? 因为我们可能不是使用vector<T>的数据结构来初始化我们的对象,也可能是用其他的数据结构比如链表等的数据来初始化vector,这也是可以的,只要传迭代器区间就可以

		template<typename InputIterator>
		vector(InputIterator begin, InputIterator end)
		{
			//push_back就行了
			while (begin != end)
			{
				push_back(*begin);
				++begin;
			}
		}

还有n个val初始化,这里我们需要注意的是,我们不能做到跟库里一样,n的参数类型要用int,为什么呢? 

		//n个val初始化
		vector(int n, T val)
		{
			for (int i = 0; i < n; ++i)
			{
				push_back(val);
			}
		}

比如我们可以这样构造 vector<int> v(10,5) 这时候,如果我们的n的类型是size_t 的话,由于编译器会把 10 和5 识别为 int 类型,这时候传给构造函数的两个参数都是 int ,如果去匹配这个构造函数的话, n 还需要进行整型提升。而如果去匹配上面的迭代器区间的模板,因为模板的只有一个参数类型,两个迭代器区域间都是一样的类型,所以不用进行隐式类型转换,因此他与迭代器区间的构造函数更加匹配,编译器就会去调用迭代器区间的构造函数,这时候就会出问题。 有两种解决方法,一种就是我们上面写的,但是这样做就与库不一致了 。另外一种就是我们不要使用原生的指针作为迭代器的类型,这种方法我们目前还不会用

完整代码


namespace MY_vector
{
	template<typename T>
	class vector
	{
	public:
		typedef T val_type;
		typedef T& reference;
		typedef const T& const_reference;
		typedef T* iterator;

		//无参构造
		vector()
			:_start(nullptr)
			,_finish(nullptr)
			,_end_of_storage(nullptr)
		{
		}

		//析构
		~vector()
		{
			delete[] _start;
			_start = _finish = _end_of_storage=nullptr;
 		}

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

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

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

		//尾插
		void push_back(val_type x)
		{ /*
			//检查扩容
			if (_finish== _finish)
			{
				size_t newcapacity = (capacity() == 0 ? 4 : 2 * capacity());
				reserve(newcapacity);
			}
			//插入数据
			*_finish = x;
			++_finish;*/
			insert(begin(), x);
		}

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

		iterator insert(iterator pos, const_reference x=val_type())//缺省值
		{
			//检查越界
			assert(pos >= _start);
			assert(pos <= _finish);//第一次插入pos==_finish  同时要支持尾插
			
			//检查是否扩容
			if (_finish == _end_of_storage)
			{
				//扩容之后pos就失效了,所以要记录pos的相对位置
				size_t pos_index = pos - _start;
				size_t newcapacity = (capacity() == 0 ? 4 : 2 * capacity());
				reserve(newcapacity);

				//更新pos
				pos = _start + pos_index;
			}
			
			//挪动数据 从后往前,往后挪数据
			iterator end = _finish;
			while (end != pos)
			{
				*end = *(end - 1);
				--end;
			}
			//更新_finish
			++_finish;

			//插入新数据
			*pos = x;

			//返回插入的数据的迭代器
			return pos;
		}

		iterator erase(iterator pos)
		{
			assert(pos >= _start);
			assert(pos < _finish);
			//挪数据覆盖
			iterator begin = pos+1;
			while (begin < _finish)
			{
				*(begin - 1) = *begin;
				begin++;
			}
			//返回删除的下一个数据
			return pos;
		}

		//[ ] 重载
		reference operator[](size_t pos)
		{
			assert(pos < size());
			return _start[pos];
		}

		const_reference operator[](size_t pos)const
		{
			assert(pos < size());
			return _start[pos];
		}
		
		//at
		reference at(size_t pos)
		{
			assert(pos<size());
			return _start[pos];
		}

		const_reference at(size_t pos)const
		{
			assert(pos < size());
			return _start[pos];
		}

		//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);
		}

		//= 重载
		vector<T>& operator=(vector<T> v)
		{
			swap(v);
			return *this;
		}

		//扩容 reserve
		void reserve(size_t n)
		{
			if (n > capacity())
			{
				iterator tmp = new val_type[n];
				//记录原来的数据个数,用来更新_finish
				size_t oldsize = size();
				//判断是否需要挪数据
				if(_start) //_start!=nullptr
				{
					for (size_t i = 0; i < oldsize; ++i)
					{
						tmp[i] = _start[i];
					}
					//释放原空间 
					delete[]_start;
				}
				//更新迭代器
				_start = tmp;
				_finish = _start + oldsize;
				_end_of_storage = _start + n;
			}
		}

		//拷贝构造
		vector(const vector<T>& v)
			:_start(nullptr)
			,_finish(nullptr)
			,_end_of_storage(nullptr)
		{
			//先扩容
			reserve(v.capacity());
			//或者先扩size,再使用赋值重载进行深拷贝
			_finish = _start + v.size();
			for (size_t i = 0; i < size(); ++i)
			{
				_start[i] = v[i];
			}
		}

		//resize
		void resize(size_t n, val_type x = val_type())//缺省参数
		{
			//扩容检查
			if (n > capacity())
			{
				reserve(n);
			}
			//是否需要加数据
			if (n > size())
			{
				while (_finish < _start + n)
				{
					*_finish=x;
					++_finish;
				}
			}
		}

		//迭代器
		iterator begin()
		{
			return _start;
		}

		iterator end()
		{
			return _finish;
		}

		const iterator cbegin()
		{
			return _start;
		}

		const iterator cend()
		{
			return _finish;
		}

		template<typename InputIterator>
		vector(InputIterator begin, InputIterator end)
		{
			//push_back就行了
			while (begin != end)
			{
				push_back(*begin);
				++begin;
			}
		}

		//n个val初始化
		vector(int n, T val)
		{
			for (int i = 0; i < n; ++i)
			{
				push_back(val);
			}
		}

	private:
		iterator _start;
		iterator _finish;
		iterator _end_of_storage;
	};



	//测试一维
	void test1()
	{
		vector<int>v1;
		v1.push_back(1);
		v1.push_back(2);
		v1.push_back(3);
		v1.push_back(4);
		v1.push_back(5);
		vector<int> v2(v1);
		vector<int>v3;
		v3.resize(8);
		v3 = v1;
		v3.reserve(10);
		v2.pop_back();
		v2.pop_back();
		v2.pop_back();
		v2.pop_back();
		//v2.pop_back();
		//v2.pop_back();
		//v2.pop_back();
		v3.insert(v3.begin(), 6);
		v3.insert(v3.begin(), 6);
		v3.insert(v3.end()-1, 6);
		v3.insert(v3.end() - 1, 6);
		v3.insert(v3.end(), 6);
		v3.insert(v3.end(), 6);

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

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

	//测试二维
	void test2()
	{
		vector<int>v;
		v.push_back(1);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);

		vector<vector<int>> vv1;
		vv1.push_back(v);
		vv1.push_back(v);
		vv1.push_back(v);
		vv1.push_back(v);
		vv1.push_back(v);

		vector<vector<int>> vv2(vv1);
		vv2.pop_back();
		vv2.pop_back();
		
		vector<vector<int>> vv3;
		vv3.resize(2);
		vv3.reserve(6);
		vv3.push_back(v);
		vv3.push_back(v);
		vv3 = vv1;
		vv3.push_back(v);
		vv3.push_back(v);

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

		cout << "vv2" << endl;

		for (auto ev : vv2)
		{
			for (auto e : ev)
			{
				cout << e << " ";
			}
			cout << endl;
		}
		cout << "vv3" << endl;

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



	}
}

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

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

相关文章

水电收费远程抄表

1.前言&#xff1a;从传统到现代的改变 水电收费远程抄表&#xff0c;是科学技术在公共服务领域的一次重要运用&#xff0c;它改变了过去人力上门服务抄表的传统模式&#xff0c;提高了高效率&#xff0c;降低了偏差&#xff0c;为群众与企业带来了极大的便利。这种系统运用智…

富途面试题:用面向对象的思想设计人关门的场景

我工作两年后&#xff0c;有一次不成功的富途证券的面试&#xff0c;印象非常深刻&#xff0c;面试官提出了一个看似简单实则充满深意的问题&#xff1a;如何用面向对象的思想设计一个人关门的场景&#xff1f; 我当时是这样设计的&#xff0c;创建两个类&#xff1a;Person和D…

Linux命令那么多,先来一篇文件和目录管理命令!

&#x1f4a1;本文建议大家收藏&#xff01; 文件和目录管理命令 1. ls - 列出目录内容 ls命令是Linux中最常用的命令之一&#xff0c;用于列出目录中的文件和子目录。 ls显示当前目录下的所有文件和目录。 ls -l以长格式列出目录内容&#xff0c;显示文件权限、所有者、大…

云途探索——移动云云日志接入实践

目录 1 前言2 新手入门2.1 前置条件2.2. 配置采集对象及规则2.3 安装Agent2.4 日志管理 3 使用场景3.1 优势3.2 应用场景 4 总结 1 前言 随着人工智能、大数据、物联网以及云计算时代的到来&#xff0c;在日志数据量持续爆增、日志数据日益多样化的今天&#xff0c;传统日志服…

Java(六)——抽象类与接口

文章目录 抽象类和接口抽象类抽象类的概念抽象类的语法抽象类的特性抽象类的意义 接口接口的概念接口的语法接口的特性接口的使用实现多个接口接口与多态接口间的继承抽象类和接口的区别 抽象类和接口 抽象类 抽象类的概念 Java使用类实例化对象来描述现实生活中的实体&…

[数据集][图像分类]家庭场景下的家具分类数据集1010张101类别

数据集类型&#xff1a;图像分类用&#xff0c;不可用于目标检测无标注文件 数据集格式&#xff1a;仅仅包含jpg图片&#xff0c;每个类别文件夹下面存放着对应图片 图片数量(jpg文件个数)&#xff1a;1010 分类类别数&#xff1a;101 类别名称:[“bath_bath”,“bath_heated_t…

java版CRM客户关系管理系统源码-CRM客户关系管理系统的技术架构与功能实现

CRM客户关系管理系统的技术架构与功能实现 一、引言 随着市场竞争的日益激烈&#xff0c;客户关系管理&#xff08;CRM&#xff09;已成为企业赢得市场、提升客户满意度、促 进业务增长的关键手段。本文旨在介绍一款先进的CRM客户关系管理系统的技术架构与功能实现&#xff0…

C++:类的内存分布

类的成员变量和方法是分开存储的&#xff0c;内存给类实例化出的对象开辟空间时只开辟成员变量所占用的空间。类中的所有方法&#xff08;成员函数&#xff09;都会放在代码区&#xff0c;所以类的大小一般只计算类中成员变量的对齐之后大小的综合&#xff08;如果没有虚函数的…

Java设计模式 _行为型模式_观察者模式

一、观察者模式 1、观察者模式 观察者模式 ( Observer Pattern )是一种行为型模式。 常用于对象间存在一对多关系时&#xff0c;比如&#xff0c;当一个对象被修改时&#xff0c;需要自动通知它的依赖对象。 2、实现思路 &#xff08;1&#xff09;、定义被观察者的行为&…

ETLCloud中如何执行SQL脚本

SQL脚本 在数据库管理与数据分析的广阔领域中&#xff0c;SQL&#xff08;Structured Query Language&#xff0c;结构化查询语言&#xff09;脚本扮演着举足轻重的角色。作为一门专为关系型数据库设计的编程语言&#xff0c;SQL不仅能够执行数据的检索、更新、插入及删除等基…

算法与数据结构:二叉排序树与AVL树

ACM大牛带你玩转算法与数据结构-课程资料 本笔记属于船说系列课程之一&#xff0c;课程链接&#xff1a; 哔哩哔哩_bilibilihttps://www.bilibili.com/cheese/play/ep66799?csourceprivate_space_class_null&spm_id_from333.999.0.0 你也可以选择购买『船说系列课程-年度会…

便携式应急气象站:应急气象监测装备

TH-BQX5便携式应急气象站&#xff0c;作为现代气象监测的重要装备&#xff0c;以其独特的便携性、高效性和灵活性&#xff0c;在应急气象监测领域发挥着至关重要的作用。这类气象站不仅为灾害预警、环境保护、农业生产等多个领域提供了实时、准确的气象数据&#xff0c;还在突发…

uniapp通过Canvas绘制网格(心电图,坐标纸等可用)

本篇文档是Canvas绘制心电图的第一个部分&#xff0c;想了解详情的可以关注后学习交流。 心电图的最底层需要一个网状底层&#xff0c;来方便进行数据的测量。 一、白底分大、中、小三个区域的网格 1、首先是HTML部分 <!DOCTYPE html> <html lang"en">…

DNS设置(linux)

1.配置dns需要现在/etc/sysconfig/network-scripts/目录下的ifcfg-ens33(后面数字也可能是其他的)中配置DNS 2.编辑/etc/resolv.conf文件&#xff0c;将上面网卡中加的dns服务器ip添加到此文件 vi /etc/resolv.conf重启网络配置 service network restart常用的dns的ip 国内…

qt中实现多语言功能

qt中实现多语言功能 原理&#xff1a; 其本质就是生成ts文件&#xff0c;然后使用Linguist软件手工翻译&#xff0c;再生成qm文件&#xff0c;最后在主程序的开始加载不同的qm文件&#xff0c;实现多语言。 步骤&#xff1a; 修改程序文件 在pro文件中加入说明 TRANSLATI…

通讯录恢复怎么办?保护珍贵联系信息的2个必备技能!

手机通讯录扮演着重要的角色&#xff0c;它不仅仅是一个简单的联系方式列表&#xff0c;更是我们与亲朋好友、同事、业务伙伴等之间关系的见证。万一不慎丢失或误删通讯录&#xff0c;学会通讯录恢复的技能变得非常重要。本文将为你介绍几种保护珍贵联系信息的必备技能&#xf…

这样写代码太优雅了吧

文章目录 优化案例初次优化再次优化看看Spring源码的处理 优化案例 假设一个场景&#xff0c; 开发代码时&#xff0c;需要对类中的方法进行遍历&#xff0c;判断有没有注解NotNull&#xff0c;暂时没有合适的工具类&#xff0c;需要自己手搓一个。 无须多想&#xff0c;分分钟…

从GPT-3.5到GPT-4O:探索AI的进化之旅,哪一版更懂你?

如何评价GPT-4o? 最新的GPT-4O&#xff0c;被誉为GPT-4的增强版。它在保持前代产品优秀性能的基础上&#xff0c;大幅降低了使用成本&#xff0c;使得更多的普通用户也能享受到顶尖AI的服务。GPT-4O在非英语语言处理上的强化&#xff0c;更是让其在全球范围内的适用性大大提高…

使用Java和XxlCrawler获取各城市月度天气情况实践

目录 前言 一、历史数据获取 1、关于天气后报 2、信息界面分析 二、数据的提取开发 1、PageVo的定义 2、属性定义 3、实际信息抓取 三、信息抓取调试以及可能的问题 1、信息获取成果 2、关于超时的问题 四、总结 前言 这篇文章主要来源于一个我们家小朋友的一个作业…

FreeSwitch视频会议同时支持内网和外网接入

我们在使用freeswitch进行视频会议时&#xff0c;之前所有的用户都是通过外网的方式接入&#xff0c;因为fs给其返回的sdp协议内容里&#xff0c;只需要fs配置的外网IP就可以了&#xff1b;最近由于引入新的业务需要有其他内网的服务器也可以直接接入fs的视频会议房间&#xff…