【C++】深入剖析vector

news2024/11/25 2:32:56

好久不见~让大家久等啦~

本期让我们来揭开vector的面纱,看看它底层是怎么实现的~


目录

一、STL定义vector的源码分析:

二、vector的模拟实现

2.1 vector框架的搭建(一些简单功能函数的实现)

2.2 迭代器失效问题

2.2.1 实现insert函数遇到的迭代器失效问题

2.2.2 实现erase函数遇到的迭代器失效问题

2.3 带模版的构造函数的实现

2.4 vector的深浅拷贝问题

三、vector实现全部代码


一、STL定义vector的源码分析:

我们先来看一看STL定义vector的源码:

template <class T, class Alloc = alloc>
class vector {
public:
  typedef T value_type;
  typedef value_type* pointer;
  typedef const value_type* const_pointer;
  typedef value_type* iterator;
  typedef const value_type* const_iterator;
  typedef value_type& reference;
  typedef const value_type& const_reference;
  typedef size_t size_type;
  typedef ptrdiff_t difference_type;

protected:
  typedef simple_alloc<value_type, Alloc> data_allocator;
  iterator start;
  iterator finish;
  iterator end_of_storage;

};

这三个成员变量(start,finish,end_of_storage),它们到底用来干嘛的?别急,我们先来看到它的无参构造函数:

  vector() : start(0), finish(0), end_of_storage(0) {}

从中我们可以看出要想模拟实现vector必须得有start,finish,end_of_storage这三个成员,而且它们最初的类型是T*,只不过被重定义为了iterator。

现在我们来画一张图,解释一下start,finish,end_of_storage这三个指针类型的迭代器到底是干嘛的:

我们可以看到:

● start和finish用于确定vector中存储的元素的范围,即[start, finish)。

● end_of_storage用于确定vector分配的内存空间的范围,即[start, end_of_storage)。

● 当vector中的元素个数达到end_of_storage时,需要重新分配更大的内存空间,然后将原来的元素拷贝到新的内存空间中,同时更新start、finish和end_of_storage的值。

通过这三个指针类型的迭代器,我们可以方便地获取vector中的元素,并进行遍历和操作

明白了其底层原理后,我们就可以尝试模拟实现一下vector了

二、vector的模拟实现

2.1 vector框架的搭建(一些简单功能函数的实现)

我们先来搭建一个vector的框架,废话少说,我们运用数据结构的知识快速构建:

namespace lhs
{
	template<class T>
	class vector 
	{

	public:
		typedef T* iterator;
		typedef const T* const_iterator;

		//构造函数
		vector()
			:_start(nullptr),
			_finish(nullptr),
			_end_of_storage(nullptr)
		{}
		vector(size_t n, const T& val = T())
			:_start(nullptr),
			_finish(nullptr),
			_end_of_storage(nullptr)
		{
			reserve(n);
			for (int i = 0; i < n; ++i)
			{
				push_back(val);
			}
		}

        //析构
		~vector()
		{
			delete[] _start;
			_start = nullptr;
			_finish = nullptr;
			_end_of_storage = nullptr;
		}
		size_t size()const//返回有效数据个数
		{
			return _finish - _start;
		}
		size_t capacity()const//返回容量大小
		{
			return _end_of_storage - _start;
		}
		bool empty()const//检查是否为空
		{
			return _start == _finish;
		}
		void reserve(size_t n)//扩容
		{
			if (n > capacity())//防止传入的n小于实际容量
			{
				T* temp = new T[n];
				if (_start)//存有有效数据就进行拷贝
				{
					memcpy(temp, _start, sizeof(T) * size());
					delete[] _start;
				}
				_finish = temp + size();//先更改_finish,避免_strat更改后影响size函数
				_start = temp;
				_end_of_storage = _start + n;
			}
		}
		void resize(size_t n, T val = T())//改变size,并用val来填充扩充部分,注意这里的val默认是其默认构造的值
										  //因为T类型不确定,所以使用构造函数
		{
			assert(n >= 0);
			if (n < size())//n<size就相当于删除数据
			{
				_finish = _start + n;
			}
			else
			{
				if (n > capacity())//检查是否需要扩容
				{
					reserve(n);
				}
				while (_finish != _start + n)
				{
					*_finish = val;
					++_finish;
				}
			}
		}
		void push_back(const T& x)//尾插
		{
			if (_finish == _end_of_storage)//判断是否需要扩容
			{
				reserve(capacity() == 0 ? 4 : capacity() * 2);//扩容时要注意容量是否为0
			}
			*_finish = x;//插入新元素
			++_finish;
		}
		void pop_back()//尾删
		{
			assert(!empty());//vector不能为空
			--_finish;
		}
		//迭代器
		iterator begin()
		{
			return _start;
		}
		const_iterator begin()const
		{
			return _start;
		}
		iterator end()
		{
			return _finish;
		}
		const_iterator end()const
		{
			return _finish;
		}
		//运算符重载
		T& operator[](size_t pos)
		{
			return *(_start + pos);
		}
		const T& operator[](size_t pos)const
		{
			return *(_start + pos);
		}
	private:
		iterator _start;
		iterator _finish;
		iterator _end_of_storage;
	};
}

这里要注意两个地方:

● reserve:在这个函数内先改变_finish的指向,防止_start改变_finish没变导致size()函数的返回值出错。

resize:这个函数有个形参缺省用到了其默认构造函数,自定义类型是有构造函数的,那假如T是内置类型呢?内置类型也有构造函数吗?答案是没有的,但是C++为了匹配模版,对于内置类型编译器可以自动识别并赋值。

架子搭完我们继续深入

2.2 迭代器失效问题

2.2.1 实现insert函数遇到的迭代器失效问题

我们现在来实现一下insert函数的功能:

void insert(iterator pos, const T& val)//在pos位置插入val
{
	assert(pos >= _start);
	assert(pos < _finish);
	if (_finish == _end_of_storage)//判断是否需要扩容
	{
		reserve(capacity() == 0 ? 4 : capacity() * 2);//扩容时要注意容量是否为0
	}
	//将pos位置之后的元素后移一位
	iterator end = _finish - 1;
	while (end >= pos)
	{
		*(end + 1) = *end;
		--end;
	}
    //在pos位置插入val
	*pos = val;
	++_finish;
}

这函数乍一眼看没啥问题,下面我们来测试一下:

void Test4()
{
	lhs::vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);
	v.insert(v.begin(), 99);
	for (auto x : v)
	{
		std::cout << x << ' ';
	}
	std::cout << std::endl;
}

 没啥问题啊,我们再来试一下:

void Test4()
{
	lhs::vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	//v.push_back(5);
	v.insert(v.begin(), 99);
	for (auto x : v)
	{
		std::cout << x << ' ';
	}
	std::cout << std::endl;
}

咦?这是怎么回事?这次测试和上次测试唯一区别就是一个vector尾插了4个元素一个尾插了5个元素啊?

我们可以发现我们搭建的vector的扩容机制是超过了四个元素就会扩容一倍到八个元素的容量,很显然上一次测试我们的v本身有5个元素,在使用insert函数插入数据时不需要扩容,而在这一次测试中v本身只有4个元素,在使用insert函数插入数据时需要扩容。

难道是扩容出了问题?仔细想一想,扩容本身是没问题的,可是扩容时要进行空间的重新分配,重新分配完的空间的地址会发生变化啊!可是我们传入的pos迭代器所指向的位置没有改变,指向的还是原来旧空间的位置,这时pos迭代器不就失效了嘛~

那我们现在来解决这个问题,在扩容后要重新更新pos所指向的空间:

void insert(iterator pos, const T& val)//在pos位置插入val
{
	assert(pos >= _start);
	assert(pos < _finish);
	if (_finish == _end_of_storage)//判断是否需要扩容
	{
		size_t len = pos - _start;//记录pos的相对于_start的相对位置
		reserve(capacity() == 0 ? 4 : capacity() * 2);//扩容时要注意容量是否为0
		pos = _start + len;//扩容完更新pos
	}
	//将pos位置之后的元素后移一位
	iterator end = _finish - 1;
	while (end >= pos)
	{
		*(end + 1) = *end;
		--end;
	}
	//在pos位置插入val
	*pos = val;
	++_finish;
}

测试通过~

函数内部的迭代器失效问题是解决了,可是我们难免遇到这种情况:

void Test4()
{
	lhs::vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	lhs::vector<int>::iterator pos = v.begin() + 2;
	v.insert(pos, 99);
	for (auto x : v)
	{
		std::cout << x << ' ';
	}
	std::cout << std::endl;
	++(*pos);
	for (auto x : v)
	{
		std::cout << x << ' ';
	}
	std::cout << std::endl;
}

在我们使用完pos迭代器进行insert之后,再将pos所指向的位置进行了++,而insert函数在内部更新pos形参是不会影响外部实参pos的,这样外部的pos就成了一个野指针:

我们可以发现运行结果是有问题的

为了解决这种情况,我们可以让insert函数返回更新之后pos的值:

iterator insert(iterator pos, const T& val)//在pos位置插入val
{
	assert(pos >= _start);
	assert(pos < _finish);
	if (_finish == _end_of_storage)//判断是否需要扩容
	{
		size_t len = pos - _start;//记录pos的相对于_start的相对位置
		reserve(capacity() == 0 ? 4 : capacity() * 2);//扩容时要注意容量是否为0
		pos = _start + len;//扩容完更新pos
	}
	//将pos位置之后的元素后移一位
	iterator end = _finish - 1;
	while (end >= pos)
	{
		*(end + 1) = *end;
		--end;
	}
	//在pos位置插入val
	*pos = val;
	++_finish;
	//返回pos,避免更新后所带来的影响
	return pos;
}

下面,我们接受其返回值重新测试一下:

void Test4()
{
	lhs::vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	lhs::vector<int>::iterator pos = v.begin() + 2;
	pos = v.insert(pos, 99);
	for (auto x : v)
	{
		std::cout << x << ' ';
	}
	std::cout << std::endl;
	++(*pos);
	for (auto x : v)
	{
		std::cout << x << ' ';
	}
	std::cout << std::endl;
}

这就对了嘛~

从这里我们可以从根本上理解STL库中的迭代器失效的问题了,在我们使用完迭代器之后一定要注意!

2.2.2 实现erase函数遇到的迭代器失效问题

下面我们来实现一下erase函数的功能:

void erase(iterator pos)//删除pos位置的数据
{
	assert(pos >= _start);
	assert(pos < _finish);
	//将pos位置之后的元素前移一位
	iterator begin = pos + 1;
	while (begin < _finish)
	{
		*(begin - 1) = *begin;
		++begin;
	}
	--_finish;
}

测试一下:

void Test5()
{
	lhs::vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);
	for (auto x : v)
	{
		std::cout << x << ' ';
	}
	std::cout << std::endl;
	lhs::vector<int>::iterator pos = v.begin() + 4;
	v.erase(pos);
	for (auto x : v)
	{
		std::cout << x << ' ';
	}
	std::cout << std::endl;
}

结果是正确的

但是我们用完erase后再对pos进行访问呢?

void Test5()
{
	lhs::vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(5);
	for (auto x : v)
	{
		std::cout << x << ' ';
	}
	std::cout << std::endl;
	lhs::vector<int>::iterator pos = v.begin() + 4;
	v.erase(pos);
	for (auto x : v)
	{
		std::cout << x << ' ';
	}
	std::cout << std::endl;
	std::cout << ++(*pos);//越界访问
}

我们可以看到,当我们删除第五个元素后,pos还指向一个非法的空间,这时我们再对pos进行访问就越界了,这是不合理的,也就是说在使用完erase后pos这个迭代器失效了

那当我们可以像insert函数一样,让erase函数返回传入pos位置的后一个元素的位置:

iterator erase(iterator pos)//删除pos位置的数据
{
	assert(pos >= _start);
	assert(pos < _finish);
	//将pos位置之后的元素前移一位
	iterator begin = pos + 1;
	while (begin < _finish)
	{
		*(begin - 1) = *begin;
		++begin;
	}
	--_finish;
	//返回pos
	if (pos != _finish)
		return pos;
	else
		return nullptr;
}

2.3 带模版的构造函数的实现

我们现在来写一个构造函数,这个构造函数可以接受任意类型的迭代器,并将迭代器区间的数据储存在我们自己的vector里面:

template<class InputIterato>
vector(InputIterato begin, InputIterato end)
	:_start(nullptr),
	_finish(nullptr),
	_end_of_storage(nullptr)
{
	while (begin != end)
	{
		push_back(*begin);
		++begin;
	}
}

测试一下:

void Test7()
{
	std::string str("Hello");
	lhs::vector<char> v1(str.begin(), str.end());
	for (auto x : v1)
	{
		std::cout << x << ' ';
	}
	std::cout << std::endl;
	int arr[] = { 10,50,89,55 };
	lhs::vector<int> v2(arr, arr + 4);
	for (auto x : v2)
	{
		std::cout << x << ' ';
	}
	std::cout << std::endl;
}

我们可以看到我们实现的vector可以接收如何类型的迭代器了,甚至是数组(数组传参本身就是地址) 

2.4 vector的深浅拷贝问题

最后我们从实现其拷贝构造函数入手,分析一下vector的深浅拷贝问题:

vector(const vector<T>& val)
{
	_start = new T[val.capacity()];
	memcpy(_start, val._start, sizeof(T) * val.size());
	_finish = _start + val.size();
	_end_of_storage = _start + val.capacity();
}

我们先来用上面写的拷贝构造函数来测试一下: 

void Test8()
{
	int arr[] = { 10,50,89,55 };
	lhs::vector<int> v1(arr, arr + 4);
	lhs::vector<int> v2(v1);
	for (auto x : v2)
	{
		std::cout << x << ' ';
	}
	std::cout << std::endl;
}

 很好,结果令人满意~

再来试试高级一点的:

void Test8()
{
	int arr[] = { 10,50,89,55 };
	lhs::vector<int> v1(arr, arr + 4);
	lhs::vector<int> v2(v1);
	for (auto x : v2)
	{
		std::cout << x << ' ';
	}
	std::cout << std::endl;
	std::string str("Hello");
	lhs::vector<std::string> v3(3, str);
	lhs::vector<std::string> v4(v3);
	for (auto x : v3)
	{
		std::cout << x << ' ';
	}
	std::cout << std::endl;
}

咦?报错了?怎么拷贝构造vector<string>类型的数据就出错了呢?

原因都是memcpy惹的祸,memcpy只能继续浅拷贝,对于string类型的数据只是将其内部成员_str的地址简单的进行了赋值,实际并没有另开空间去存储,所以导致v3和v4所存储的srting元素指向的同一块空间,释放空间时会重复释放导致报错:

 这样子我们自己手动来进行深拷贝:

vector(const vector<T>& val)
{
	_start = new T[val.capacity()];
	for (size_t i = 0; i < val.size(); ++i)//深度拷贝
	{
		_start[i] = val._start[i];
	}
	_finish = _start + val.size();
	_end_of_storage = _start + val.capacity();
}

上面将_start一一赋值,如果_start是自定义类型的话就会调用相对应的拷贝构造函数进行深拷贝了~

测试一下: 

成功运行~

那这样子说,我们之前实现的reserve函数里面用的是memcpy进行的浅拷贝,这样子也会出错的,下面我们来改进一下:

void reserve(size_t n)//扩容
{
	if (n > capacity())//防止传入的n小于实际容量
	{
		T* temp = new T[n];
		if (_start)//存有有效数据就进行拷贝
		{
			for (size_t i = 0; i < size(); ++i)//深拷贝
			{
				temp[i] = _start[i];
			}
			delete[] _start;
		}
		_finish = temp + size();//先更改_finish,避免_strat更改后影响size函数
		_start = temp;
		_end_of_storage = _start + n;
	}
}

三、vector实现全部代码

#include <iostream>
#include <assert.h>

namespace lhs
{
	template<class T>
	class vector 
	{

	public:
		typedef T* iterator;
		typedef const T* const_iterator;

		//构造函数
		vector()
			:_start(nullptr),
			_finish(nullptr),
			_end_of_storage(nullptr)
		{}
		vector(size_t n, const T& val = T())
			:_start(nullptr),
			_finish(nullptr),
			_end_of_storage(nullptr)
		{
			reserve(n);
			for (int i = 0; i < n; ++i)
			{
				push_back(val);
			}
		}
		vector(int n, const T& val = T())
			:_start(nullptr),
			_finish(nullptr),
			_end_of_storage(nullptr)
		{
			reserve(n);
			for (int i = 0; i < n; ++i)
			{
				push_back(val);
			}
		}
		template<class InputIterato>
		vector(InputIterato begin, InputIterato end)
			:_start(nullptr),
			_finish(nullptr),
			_end_of_storage(nullptr)
		{
			while (begin != end)
			{
				push_back(*begin);
				++begin;
			}
		}

		//拷贝构造
		vector(const vector<T>& val)
		{
			_start = new T[val.capacity()];
			for (size_t i = 0; i < val.size(); ++i)//深度拷贝
			{
				_start[i] = val._start[i];
			}
			_finish = _start + val.size();
			_end_of_storage = _start + val.capacity();
		}

		//析构
		~vector()
		{
			delete[] _start;
			_start = nullptr;
			_finish = nullptr;
			_end_of_storage = nullptr;
		}
		size_t size()const//返回有效数据个数
		{
			return _finish - _start;
		}
		size_t capacity()const//返回容量大小
		{
			return _end_of_storage - _start;
		}
		bool empty()const//检查是否为空
		{
			return _start == _finish;
		}
		void reserve(size_t n)//扩容
		{
			if (n > capacity())//防止传入的n小于实际容量
			{
				T* temp = new T[n];
				if (_start)//存有有效数据就进行拷贝
				{
					for (size_t i = 0; i < size(); ++i)//深拷贝
					{
						temp[i] = _start[i];
					}
					delete[] _start;
				}
				_finish = temp + size();//先更改_finish,避免_strat更改后影响size函数
				_start = temp;
				_end_of_storage = _start + n;
			}
		}
		void resize(size_t n, T val = T())//改变size,并用val来填充扩充部分,注意这里的val默认是其默认构造的值
										  //因为T类型不确定,所以使用构造函数
		{
			assert(n >= 0);
			if (n < size())//n<size就相当于删除数据
			{
				_finish = _start + n;
			}
			else
			{
				if (n > capacity())//检查是否需要扩容
				{
					reserve(n);
				}
				while (_finish != _start + n)
				{
					*_finish = val;
					++_finish;
				}
			}
		}
		void push_back(const T& x)//尾插
		{
			if (_finish == _end_of_storage)//判断是否需要扩容
			{
				reserve(capacity() == 0 ? 4 : capacity() * 2);//扩容时要注意容量是否为0
			}
			*_finish = x;//插入新元素
			++_finish;
		}
		void pop_back()//尾删
		{
			assert(!empty());//vector不能为空
			--_finish;
		}
		iterator insert(iterator pos, const T& val)//在pos位置插入val
		{
			assert(pos >= _start);
			assert(pos < _finish);
			if (_finish == _end_of_storage)//判断是否需要扩容
			{
				size_t len = pos - _start;//记录pos的相对于_start的相对位置
				reserve(capacity() == 0 ? 4 : capacity() * 2);//扩容时要注意容量是否为0
				pos = _start + len;//扩容完更新pos
			}
			//将pos位置之后的元素后移一位
			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				--end;
			}
			//在pos位置插入val
			*pos = val;
			++_finish;
			//返回pos,避免更新后所带来的影响
			return pos;
		}
		iterator erase(iterator pos)//删除pos位置的数据
		{
			assert(pos >= _start);
			assert(pos < _finish);
			//将pos位置之后的元素前移一位
			iterator begin = pos + 1;
			while (begin < _finish)
			{
				*(begin - 1) = *begin;
				++begin;
			}
			--_finish;
			//返回pos
			if (pos != _finish)
				return pos;
			else
				return nullptr;
		}
		//迭代器
		iterator begin()
		{
			return _start;
		}
		const_iterator begin()const
		{
			return _start;
		}
		iterator end()
		{
			return _finish;
		}
		const_iterator end()const
		{
			return _finish;
		}
		//运算符重载
		T& operator[](size_t pos)
		{
			return *(_start + pos);
		}
		const T& operator[](size_t pos)const
		{
			return *(_start + pos);
		}
	private:
		iterator _start;
		iterator _finish;
		iterator _end_of_storage;
	};
}


到这里本期博客就结束了,后期博主会加快更新,正式进入到快车道,冲冲冲~

本期代码量比较大,如有不足还请大佬们指出~

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

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

相关文章

【Spring】SpringCloud Ribbon中的7种负载均衡策略!

Yan-英杰的主页 悟已往之不谏 知来者之可追 C程序员&#xff0c;2024届电子信息研究生 负载均衡通器常有两种实现手段&#xff0c;一种是服务端负载均衡器&#xff0c;另一种是客户端负载均衡器&#xff0c;而我们今天的主角 Ribbon 就属于后者——客户端负载均衡器。 服务…

个人总结:测试用例万能公式+常见例子(公式的运用)

前言 测试工程师面试的时候&#xff0c;有时候会当场考测试用例&#xff0c;毕竟这是测试工程师的基本功。 对于我来说&#xff0c;让我写测试用例会比让我直接说测试用例更好点。 直接嘴里说出来&#xff0c;容易逻辑混乱&#xff0c;给人一种想到啥说啥的感觉。 其实个人感…

MySQL学习基础篇(九)---子查询

MySQL学习基础篇(九)—子查询 子查询指一个查询语句嵌套在另一个查询语句内部的查询&#xff0c;这个特性从MySQL 4.1开始引入。SQL 中子查询的使用大大增强了 SELECT查询的能力&#xff0c;因为很多时候查询需要从结果集中获取数据&#xff0c;或者需要从同一个表中先计算得出…

MySQL安装与部署

第一种方法&#xff1a;在线安装 配置一个安装yum源 Adding the MySQL Yum Repository 可以手动配置yum源&#xff0c;baseurl指向国内镜像源地址&#xff0c;比如清华、中科大。 Installing MySQL Starting the MySQL Server&#xff1a; 查询临时登录密码 修改数据库密码…

CTFHub XSS DOM反射 WriteUp

前言&#xff1a;本文需要注册一个xss平台&#xff0c;以接收xss反弹回来的数据&#xff0c;请自己在互联网上寻找合适的xss平台 1. 构造闭合语句 根据题目提示&#xff0c;判断网站存在DOM xss漏洞 查看页面源代码&#xff0c;发现关键位置&#xff0c;其中CTFHub is very n…

【揭秘Vue核心】深入解析Object.defineProperty和Proxy的区别,让你秒懂!

问题&#xff1a;Object.difineProperty 和 proxy 有什么区别&#xff1f; Object.defineProperty 和 Proxy 是用于实现响应式数据的两种不同方式。 Object.defineProperty Object.defineProperty 通过直接修改对象的属性描述符来实现数据的劫持。Vue 2.x 中就是通过 Objec…

vscode 之 工作区的应用(解决vue2插件vetur、vue3插件volar禁用启用问题)

目录 前言创建工作区添加文件夹到工作区为当前打开的工作区指定特定环境工作区删除文件夹如何切换工作区 前言 工作区&#xff1f;&#xff1f;&#xff1f;为什么要工作区&#xff1f;&#xff1f;&#xff1f; 首先工作区简单理解就是vscode工作时的区域、范围&#xff1b; 延…

如何正确的安装MySQL

1. 使用rpm包在线安装 1.1 确认自己电脑版本(linux) [rootlocalhost ~]# cat /etc/redhat-release CentOS Linux release 7.9.2009 (Core)ps:本次安装采用的系统为Centos 7.9 所使用的mysql为5.7 1.2 下载rpm包 下载地址 https://dev.mysql.com/downloads/mysql/ ps: 自己…

初识树莓派:强大功能与创造力的结合

目录 树莓派4B简介&#xff1a; 树莓派系统镜像下载&#xff1a; 树莓派4B简介&#xff1a; 树莓派4B是一款功能强大且受欢迎的单板计算机&#xff0c;由树莓派基金会开发。作为树莓派系列的最新版本&#xff0c;4B提供了许多令人兴奋的特性和改进&#xff0c;使其成为教育、创…

SSM简单项目遇到的几个问题(最后一个问题,虽然能运行,但是我找不出问题出在哪里)

这几个问题&#xff0c;干扰了我很长时间。 主要因为书本的例子&#xff0c;是通过controller层返回到jsp层。但是&#xff0c;最后一个SSM项目&#xff0c;它用的是controller返回信息给Service层&#xff0c;再由Service层返回Jsp层。 实训&#xff1a;编写一个模糊查询姓名…

【Java基础教程】(三)程序概念篇 · 上:探索Java编程基础,注释、标识符、关键字、数据类型~

Java基础教程之程序概念 上 本节学习目标1️⃣ Java中的注释2️⃣ 标识符3️⃣ 关键字4️⃣ 数据类型4.1 整型&#x1f50d; 什么是变量&#xff1f;什么是常量&#xff1f;&#x1f50d;关于数据类型转换的规则&#xff1f; 4.2 浮点数&#x1f50d;关于Java的计算的缺陷 4.3…

chatGPT写综述

文献是任何学术研究的基础&#xff0c;但搜集和整合众多的信息&#xff0c;写成一篇健全的文献综述却是一项艰巨的任务。随着人工智能技术的发展&#xff0c;大型预训练语言模型&#xff08;LM&#xff09;&#xff0c;如OpenAI的ChatGPT变得越来越流行。本文将探讨如何使用Cha…

K8S的概念和基本应用

学习视频&#xff1a;Kubernetes基本概念和应用_哔哩哔哩_bilibili 零 . 架构概览 master节点&#xff1a;管理调度集群资源&#xff0c;一般为多节点构成&#xff0c;可以是物理机&#xff0c;也可以是虚拟机。worker节点&#xff1a;资源的提供者&#xff0c;一般为多节点构…

沟通的层次模型

沟通的层次模型 根据沟通的倾向性将沟通划分为五个层次&#xff0c;分别是情绪、感受、想法、态度和事实。 模型介绍 沟通的层次 第5层&#xff1a;沟通情绪-带着强烈情绪表达&#xff0c;尤其经常使用这些字眼&#xff1a;“总是”、“每次都”第4层&#xff1a;沟通感受-不…

【APP开发】uni-data-select真机下拉框不显示问题

官网示例&#xff1a;uni-data-select 因为外层container有样式&#xff1a; width: 100%; height: 100%; overflow: hidden; display: flex; flex-direction: column; 去掉最外层 class"container"之后就可以了&#xff0c;但具体原因不清楚 我是对比了Hello uni-…

python中多态的作用是什么?

在强类型语言(例如Java或C#)中&#xff0c;多态是指允许使用一个父类类型的变量或常量来引用一个子类类型的对象&#xff0c;根据被引用子类对象特征的不同&#xff0c;得到不同的运行结果。即使用父类的类型来调用子类的方法。 在Python中&#xff0c;多态指在不考虑对象类型…

js压缩base64图片

今天试了用js把base64编码格式的图片进行压缩&#xff0c;记录一下&#xff1a; base64图片转换地址 base64图片转换网址 代码如下 js&#xff1a; $(document).ready(function(){compressImg(targetObj.src, 0.5, useImg, targetObj) });let targetObj {// base64字符串 …

机器学习26:《数据准备和特征工程-IV》数据转换

特征工程 是确定哪些特征可能对训练模型有用&#xff0c;然后通过转换日志文件等数据来源中的原始数据来创建这些特征的过程。在本文中&#xff0c;笔者将重点讨论何时以及如何转换数字和分类数据&#xff0c;以及不同方法的权衡。 目录 1.数据转换的原因 1.1 数据兼容性的强…

并发容器(一)CopyOnWriteArrayList

我们知道&#xff0c;ArrayList&#xff0c;LikedList&#xff0c;HashMap都是线程不安全的容器 同步容器&#xff1a;Vector&#xff0c;HashTable&#xff0c;SynchronizedList是线程安全的&#xff0c;因为里面加了synchronized同步&#xff0c;所以这样的容器也叫同步容器…

Zabbix监控

Zabbix监控 1.Zabbix监控概述1.1 zabbix是什么1.2 zabbix监控原理1.3 Zabbix 6.0新特性1.4 Zabbix 6.0功能组件1.5 Zabbix与prometheus区别对比 2. 部署Zabbix6.02.1 安装NginxPHP2.2 部署Mariadb数据库2.3 安装zabbix Server服务端2.4 部署Web前端&#xff0c;进行访问2.5 部署…