C++vector的模拟实现

news2024/11/27 8:34:17

文章目录

  • 模拟实现
    • 1. 迭代器
    • 2. 容量操作
      • (1)size和capacity
      • (2)reserve
      • (3) resize
    • 3. 元素访问
      • (1)下标 + [ ]
    • 4. 修改操作
      • (1)push_back
      • (2)insert
      • (3)erase
      • (4)swap
    • 5. 默认成员函数
      • (1) 构造函数
      • (2) 拷贝构造
      • (3) 复制重载
      • (4) 析构函数
  • 全部代码参考


模拟实现

整体的架构:我们要实现的vector类是包在【hao】这个命名空间中,而对于这个类而言,我要将其定义为一个 模版类,这一块如果还有同学不太熟悉的话可以去看看 C++模版

这里需要设置三个私有成员变量,都是指针类型分别是[_start]、[_finish]、[_end_of_storage]
_start表示数组的起始地址
_finish表示数组中最后一个元素的下一个 的 地址,功能上可以理解为类似于数据结构中实现顺序表结构体定义中的size。比如有个数组开了十个空间那里面有三个数据,那_start指向的就是0号下标处元素的地址。一共三个数据,下标占用0,1,2,那_finish就指向下面为3元素的地址(所谓最后一个元素**的下一个** 的 地址)
_end_of_storage表示数组最大容量的地址,功能上可以理解为类似于数据结构中实现顺序表结构体定义中的capacity。

这三个成员变量都是指针那我们如何计算该数组中到底有多少个元素,容量是多少呢?答案是我们可以通过指针相减的操作得到
数组中元素个数(即size)=_finish-_start
数组的最大容量(即capacity)=_end_of_storage-_start

[_start]、[_finish]、[_end_of_storage]提前声明的形式将它们都初始化为nullptr,这样当我们后面在写 构造函数和析构函数 的时候就不需要再去做初始化了。

并且我们定义了两种迭代器,在vector中迭代器本质上就是指针,一种是普通指针,一种是const类型的指针,我们使用typedef将指针重命名一下,分别叫iteratorconst_iterator

//使用命名空间的目的是将自己实现的vector和库中的vector区分一下
namespace hao //命名空间的名字可以随便取
{
	template<class T>//模板
	class vector {
	public:
		typedef T* iterator;  //普通迭代器
		typedef const T* const_iterator; //const类型的迭代器
	// 主要接口函数
	private:
		iterator _start = nullptr;
		iterator _finish = nullptr;
		iterator _end_of_storage = nullptr;
	};
}

1. 迭代器

返回值有两种一种是普通版本iterator另一种是const版本const_iterator

  • 普通类型的迭代器可以修改迭代器指向元素的值
  • const类型的迭代器不允许修改迭代器指向元素的值

在函数后面加个const是为了让const类型的vector对象进行调用
因为const类型的vector对象不能调用不加const的成员函数?????

const_iterator begin() const
		//迭代器
		iterator begin()
		{
			return _start;
		}
		iterator end()
		{
			return _finish;
		}
		//const类型的迭代器
		const_iterator begin() const
		{
			return _start;
		}
		const_iterator end() const
		{
			return _finish;
		}

2. 容量操作

(1)size和capacity

size可以获得vector中元素个数,也就是我们在上面所讲的_start和_finish这两个迭代器之间的距离,vector迭代器的底层其实就是指针,那要计算出两个指针之间的数据个数的话让它们做一个相减_finish - _start那对于整个容器的容量【capacity】来说,即为_end_of_storage - _start。
在这里插入图片描述

_finish - _start就是size
_end_of_storage - _start就是capacity

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

(2)reserve

reserve就是提前开好空间,避免频繁扩容,提高效率。当然当传入的参数n>capacity时才会执行扩容机制

这个函数的逻辑简单来说就是,新开一块大小为n的空间,然后将原先数组中的元素全部都拷贝到新开的数组中去。

拷贝的逻辑我们可以使用内存函数memcpy(),拷贝完后再去释放原空间,接下去把这些成员变量去做一个更新即可。但是这里需要注意的是memcpy拷贝的时候是原封不动的去拷贝,在拷贝内置类型时没啥问题,但拷贝自定义类型的时候就可能会产生错误。

下面的代码虽然看起来没有问题,但实际上问题不少。

void reserve(size_t n)
{
	if (n > capacity())
	{
		T* tmp = new T[n];		// 开一块新空间
		if (_start)
		{
			memcpy(tmp, _start, sizeof(T) * size());
			delete[] _start;
		}
		_start = tmp;
		_finish = _start + size();
		_end_of_storage = _start + n;
	}
}

在执行中间这条语句的时候就有问题。首先会调用size()函数,那size()函数是如何实现的呢?是通过返回return _finish - _start来实现的,可是在调用之前_start已经修改为tmp了,由此计算出来的size就一定有问题,所以我们这里不能在 _start修改之后调用size(),而是要提前将size保存起来size_t sz = size(),以此来保证_finish的计算正确

_start = tmp;
_finish = _start + size();//这条语句的问题
if (n > capacity())
{
	// 先保存一下原先的size()
	size_t sz = size();
	T* tmp = new T[n];		// 开一块新空间
	if (_start)
	{
		memcpy(tmp, _start, sizeof(T) * size());
		delete[] _start;
	}
	_start = tmp;
	_finish = _start + sz;
	_end_of_storage = _start + n;
}

第二个问题就是这个memcpy的问题,它会原封不动的进行拷贝,对于内置类型来说没有问题,但如果这里我们使用==vector< string >==呢?
我们知道string在底层维护一个char类型的数组,也就是有个char* 类型的指针指向字符串的首字符的地址。
假如这里有两个string类对象A和B,使用memcpy将B拷贝到A中去,因为它会原封不动的进行拷贝,所以会导致A中的指针和B中的指针指向同一块空间,在A进行释放指针指向的空间的时候就会导致B中的空间也会被释放。

带来的问题就是把A释放了,B也不能用了,这部就出问题了么,所以我们不能使用memcpy,因该使用for循环调用string的赋值重载深拷贝解决这个问题。

for (size_t i = 0; i < sz; i++)
{
	tmp[i] = _start[i];//调用string的赋值重载进行深拷贝
}

在这里插入图片描述
在这里插入图片描述

最终正确的代码:

void reserve(size_t n)
{
	size_t sz = size();
	T* tmp = new T[n];
	if (n > capacity())
	{
		for (size_t i = 0; i < sz; i++)
		{
			tmp[i] = _start[i];
		}
		delete[] _start;
	}
	_start = tmp;
	_finish = _start + sz;
	_end_of_storage = _start + n;
}

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

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

(3) resize

resize分为三种情况

  • 第一个是n < _finish的情况,此时只需要修改_finish的值就可以了
  • 第二个是n > _finish && n <= _end_of_storage的情况,第二种和第三种统一交给reserve去判断
  • 第三个是n >_end_of_storage的情况;
    在这里插入图片描述

第一种情况下,直接让==_finish = _start + n==,如果是第二第三种情况,先使用reserve去检查一下是否需要扩容,然后再去通过循环追加对应的数据即可。

这里我们使用了const T& val = T(),给了一个默认的缺省参数T(),当前的形参val的类型使用的就是模版参数类型,采取自动推导的形式去进行自动识别
而T()则是【匿名对象】,这里不可以给0,因为当前的数据类型不一定就是 整型,我们可以根据这个匿名对象去生成不同的默认值

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

3. 元素访问

(1)下标 + [ ]

访问元素最常用的方法就是下标 + [ ],也有两个版本,const版本和非const版本

底层实现方法也比较简单,直接返回数组中的值就好了。

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

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

	return _start[pos];
}

4. 修改操作

(1)push_back

每次调用push_back,在函数内部我们都需要判断是否需要扩容,在vector的使用方法一文中已经说了,在vs中的扩容逻辑是1.5倍,在Linux下是2倍扩容逻辑。在这里我们采用2倍扩容逻辑,并把扩容操作直接交给reserve函数处理。

void push_back(const T& x)
{
//先判断是否需要扩容
	if (_finish == _end_of_storage)
	{
		size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
		reserve(newcapacity);//利用reserve扩容
	}
			
	*_finish = x;//插入元素x
	++_finish;
}

(2)insert

insert函数有两个参数,第一个是一个迭代器,表示要插入的位置,第二个表示要插入的元素,返回值是一个迭代器,只要目的是为了解决迭代器失效的问题

iterator insert(iterator pos, const T& x)

函数内部首先需要判断一下pos的位置是否合法,可以在_start位置插入,也可以在_finish位置插入,所以pos >= _start && pos <= _finish

assert(pos >= _start && pos <= _finish);

如果pos位置有元素,那就把pos位置以及pos以后的元素全部向后挪一个位置,然后把元素放在pos位置。比如在_start位置插入元素7的话,那就把1、3、4、6、3 全部向后挪一个,然后把7放在_start位置。

既然是插入元素,我们当然要检查是否扩容。

if (_finish == _end_of_storage)
{
	size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
	reserve(newcapacity);
}

在这里插入图片描述

但是呢扩容就又会有个新的问题,扩容相当于新开了个空间,但是你pos指向的还是原来空间的位置啊,你还按原来的插入,那不肯定插入错了吗?所以我们要提前计算好pos距离_start的长度len,然后将新_start加上len就获得了,正确的位置pos。

==还有个问题就是我们在调用insert函数肯定会传入一个迭代器比如下面这样,但是如果现在正好发生了扩容,那我们定义的it岂不是指向的不是begin位置了吗?也就说此时这个迭代器就失效了,即发生了迭代器失效问题。==解决方案就是返回一个迭代器指向pos位置即可,所以insert的函数返回值才是iterator类型的。

vector<int> v(5,3);
auto it = v.begin();
v.insert(v.begin,10);
if (_finish == _end_of_storage)
{
	size_t len = pos - _start;//计算len
	size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
	reserve(newcapacity);
	pos = _start + len;//计算新的pos
}

在这里插入图片描述
之后就是一个一个的往后挪数据,最后插入了。

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

完整代码:

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

	}
	*pos = x;
	++_finish;
	return pos;
}

(3)erase

有了insert的铺垫,在理解erase就比较简单了。
首先还是判断一下pos是否在合法的区间内assert(pos >= _start && pos < _finish);
后面就是挪数据了,insert是往后挪,erase就是往前挪了。

那erase也会发生迭代器失效的问题吗,答案是会的。如果我们把pos设置为最后一个元素,那么在调用erase时,元素会往前挪,那pos位置就指向end了,这不就越界了吗?,所以我们依旧返回一个iterator,解决这个问题。

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

(4)swap

就是去调用库里面的这个模版函数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);
}

5. 默认成员函数

(1) 构造函数

逻辑上和resize一样,所以直接调用resize就行了。

// 有参构造
vector(size_t n, const T& val = T())
{
	resize(n, val);
}

由于一开始我们就对private成员进行了初始化,所以就不用在构造函数里初始化他们了

private:
	iterator _start = nullptr;
	iterator _finish = nullptr;
	iterator _end_of_storage = nullptr;

迭代器初始化
调用这个函数需要传入一个迭代器区间,由于你传入的迭代器不知道是string的迭代器,还是list的迭代器,还是谁谁谁的迭代器,类型不确定,所以我们这里要弄成模板,让编译器去自己推导。

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

但是我们指向这条语句的时候就会发现问题

hao::vector<int> v5(10, 1);

指向这条语句我们发现没有调用vector(size_t n, const T& val = T())反而调用了vector(InputIterator first, InputIterator last)。在这里面有个push_back(*first);,有个解引用操作,众所周知指针和迭代器可以解引用,基本数据类型是不能解引用的,那肯定就会出问题。
在这里插入图片描述
那原因是为什么呢?
原来是模版参数会去进行自动类型推导,从而匹配最合适函数模版。因为我们在这里所传入的101都是int类型,但是有参构造的第一个形参类型为==size_t,==并不是最匹配的 而迭代器区间初始化其参数类型都是模版参数,所以在匹配的时候它是最优先进行匹配的,因此就导致了上面的事故。

如何解决呢?当然是直接补充一个形参是int类型的函数重载就行了。

vector(size_t n, const T& val = T())
{
	resize(n, val);
}

vector(int n, const T& val = T())
{
	resize(n, val);
}

那我们在写了这个重载函数后要如何去调用对应的无符号类型size_t呢,此时我们只需要在传递的参数后加上一个u即可,那么编译器在进行识别的时候就会自动将其识别成为无符号整数。

hao::vector<int> v6(10u, 6);

(2) 拷贝构造

如果我们不手动实现的话,默认是浅拷贝,会出很多问题,所以我们这里手搓一个深拷贝出来。注意这里没用memcpy,原因上面也说了。原理比较简单,直接上代码。

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

(3) 复制重载

这里就用到了我们之前实现的swap函数
我们使用到这么一个巧计,那就是使用 传值传参首先会去调用一个拷贝构造构造一个临时对象,但是临时对象出了作用域之后肯定是要销毁的此时我们就可以使用【swap】和当前对象去做一个交换,我呢获取到了你里面的内容,你帮我释放了不需要的内容,简直一举两得。

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

(4) 析构函数

释放空间,指针置空,没什么好说的。

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

全部代码参考

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

using namespace std;

namespace hao
{
	template<class T>
	class vector
	{
	public:
		typedef T* iterator;
		typedef const T* const_iterator;
		vector()
		{

		}
		//[first,last)
		template<class InputIterator>
		vector(InputIterator first, InputIterator last)
		{
			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}
		vector(size_t n, const T& val = T())
		{
			resize(n, val);
		}
		vector(int n, const T& val = T())
		{
			resize(n, val);
		}
		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();

		}
		~vector()
		{
			if (_start)
			{
				delete[] _start;
				_start = _finish = _end_of_storage = nullptr;
			}
		}
		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;
		}
		//迭代器
		iterator begin()
		{
			return _start;
		}
		iterator end()
		{
			return _finish;
		}
		//const类型的迭代器
		const_iterator begin() const
		{
			return _start;
		}
		const_iterator end() const
		{
			return _finish;
		}

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

		void reserve(size_t n)
		{
			size_t sz = size();
			T* tmp = new T[n];
			if (n > capacity())
			{
				for (size_t i = 0; i < sz; i++)
				{
					tmp[i] = _start[i];
				}
				delete[] _start;
			}
			_start = tmp;
			_finish = _start + sz;
			_end_of_storage = _start + n;
		}
		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 push_back(const T& x)
		{
			if (_finish == _end_of_storage)
			{
				size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newcapacity);
			}
			*_finish = x;
			++_finish;
		}
		void pop_back()
		{
			erase(--end());
		}


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

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

			return _start[pos];
		}

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

			}
			*pos = x;
			++_finish;
			return pos;
		}

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


	};
	void print(const vector<int>& v)
	{
		for (auto e : v)
		{
			cout << e << " ";
		}
		cout << endl;
	}

	void test_vector1()
	{
		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 e : v1)
		{
			cout << e << " ";
		}
		cout << endl;

		for (size_t i = 0; i < v1.size(); i++)
		{
			v1[i]++;
		}

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

		print(v1);
	}

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

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

		v1.insert(v1.begin(), 100);

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

		/*vector<int>::iterator p = v1.begin() + 3;
		v1.insert(p, 300);*/

		vector<int>::iterator p = v1.begin() + 3;
		//v1.insert(p+3, 300);

		// insert以后迭代器可能会失效(扩容)
		// 记住,insert以后就不要使用这个形参迭代器了,因为他可能失效了
		v1.insert(p, 300);

		// 高危行为
		// *p += 10;

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

	void test_vector3()
	{
		vector<int> v1;
		v1.push_back(1);
		v1.push_back(2);
		v1.push_back(2);
		v1.push_back(3);
		v1.push_back(4);
		v1.push_back(5);
		v1.push_back(6);

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

		auto it = v1.begin();
		while (it != v1.end())
		{
			if (*it % 2 == 0)
			{
				it = v1.erase(it);
			}
			else
			{
				++it;
			}
		}

		//v1.erase(v1.begin());
		//auto it = v1.begin()+4;
		//v1.erase(it);

		 erase以后,迭代器失效了,不能访问
		 vs进行强制检查,访问会直接报错
		//cout << *it << endl;
		//++it;
		//cout << *it << endl;

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

	void test_vector4()
	{
		vector<int> v;
		v.resize(10, 0);

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


		int i = 0;
		int j = int();
		int k = int(1);
	}

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

		vector<int> v1(v);

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

		vector<int> v2;
		v2.resize(10, 1);

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

	void test_vector6()
	{
		vector<string> v;
		v.push_back("111111111111111111");
		v.push_back("222222222222222222");
		v.push_back("333333333333333333");
		v.push_back("444444444444444444");
		v.push_back("555555555555555555");

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

		vector<string> v1(v);
		for (auto& e : v1)
		{
			cout << e << " ";
		}
		cout << endl;
	}

	void test_vector7()
	{
		//vector<int> v(10u, 1);
		vector<string> v1(10, "1111");

		vector<int> v2(10, 1);



		// vector<int> v;

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


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

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

		int a[] = { 16,2,77,29 };
		vector<int> v5(a, a + 4);
		for (auto e : v5)
		{
			cout << e << " ";
		}
		cout << endl;
	}
}

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

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

相关文章

【死磕Elasticsearch】从实战中来,到实战中去

文章目录 写在前面&#xff1a;1、索引阻塞的种类2、什么时候使用阻塞&#xff1f;场景1&#xff1a;进行系统维护场景。场景2&#xff1a;保护数据不被随意更改场景。场景3&#xff1a;优化资源使用的场景。场景4&#xff1a;遵守安全规则场景。 3、添加索引阻塞API4、解除设置…

使用jquery的autocomplete属性实现联想补全操作

平时使用百度&#xff0c;淘宝等软件搜索时&#xff0c;常见一个搜索框联想提示&#xff0c;感觉确实好用但没有研究过原理&#xff0c;最近恰巧工作中遇到一个同样的场景&#xff0c;不同于大厂使用高端的Python&#xff0c;这次需要使用jQuery的autocomplete属性来自动联想补…

git撤回代码提交commit或者修改commit提交注释

执行commit后&#xff0c;还没执行push时&#xff0c;想要撤销之前的提交commit 撤销提交 使用命令&#xff1a; git reset --soft HEAD^命令详解&#xff1a; HEAD^ 表示上一个版本&#xff0c;即上一次的commit&#xff0c;也可以写成HEAD~1 如果进行两次的commit&#xf…

Singularity(二)| 安装singularity工具

Singularity&#xff08;二&#xff09;| 安装singularity工具 以默认安装 Ubuntu 22.04 (jammy) 发行版的 WSL 2 (Windows Subsystem for Linux 2) 为例&#xff1a; 参考&#xff1a;官方快速安装向导 安装系统依赖项 首先在主机上安装开发工具和库&#xff0c;在基于 debian…

【黑马程序员】python函数

文章目录 函数什么是函数为什么学习函数函数定义函数的传入参数函数的返回值返回值基础None返回值 函数说明文档函数的嵌套调用定义代码示例 全局变量和局部变量全局变量global变量局部变量 函数综合案例 函数 什么是函数 组织好的&#xff0c;可重复使用的、用来实现特定功能…

5.Java并发编程—JUC线程池架构

JUC线程池架构 在Java开发中&#xff0c;线程的创建和销毁对系统性能有一定的开销&#xff0c;需要JVM和操作系统的配合完成大量的工作。 JVM对线程的创建和销毁&#xff1a; 线程的创建需要JVM分配内存、初始化线程栈和线程上下文等资源&#xff0c;这些操作会带来一定的时间和…

【C语言步行梯】自定义函数、函数递归详谈

&#x1f3af;每日努力一点点&#xff0c;技术进步看得见 &#x1f3e0;专栏介绍&#xff1a;【C语言步行梯】专栏用于介绍C语言相关内容&#xff0c;每篇文章将通过图片代码片段网络相关题目的方式编写&#xff0c;欢迎订阅~~ 文章目录 什么是函数库函数自定义函数函数执行示例…

数据结构 第2章:线性表

文章目录 2.1 线性表的定义和操作2.1.1 线性表的基本概念2.1.2 线性表的基本操作 2.2. 顺序表2.2.1. 顺序表的基本概念2.2.2. 顺序表的实现2.2.3. 顺序表的基本操作 2.3 链表2.3.1 单链表的基本概念2.3.2 单链表的实现2.3.3 单链表的插入2.3.4. 单链表的删除2.3.5. 单链表的查找…

VB 数据质量诊断软件(分析数据的完整性,合理性,准确性)-139-(代码+程序说明)

转载地址http://www.3q2008.com/soft/search.asp?keyword139 前言: 为何口出狂言,作任何VB和ASP的系统, 这个就是很好的一个证明 :) 又有些狂了... 数据库操作谁都会,接触的多了也没什么难的,VB编程难在哪?算法上,这个是一个算法题的毕业设计 哈哈忙活了足足有一○小时, …

2024年最新阿里云和腾讯云云服务器价格租用对比

2024年阿里云服务器和腾讯云服务器价格战已经打响&#xff0c;阿里云服务器优惠61元一年起&#xff0c;腾讯云服务器61元一年&#xff0c;2核2G3M、2核4G、4核8G、4核16G、8核16G、16核32G、16核64G等配置价格对比&#xff0c;阿腾云atengyun.com整理阿里云和腾讯云服务器详细配…

C语言 ——关键字

关键字&#xff1a;在C语言中被赋予了特定含义的英文单词&#xff0c;一共有32个关键字 * 关键字全部小写 * 在特定的编译器中&#xff0c;关键字是高亮显示的 vs&#xff1a;蓝色或者紫色 vs&#xff1a;蓝色 下图圈起来的都是关键字 c auto break case char const con…

Kafka消费者重平衡

「&#xff08;重平衡&#xff09;Rebalance本质上是一种协议&#xff0c;规定了一个Consumer Group下的所有Consumer如何达成一致&#xff0c;来分配订阅Topic的每个分区」。 比如某个Group下有20个Consumer实例&#xff0c;它订阅了一个具有100个分区的Topic。 正常情况下&…

【C语言】如何规避野指针

✨✨ 欢迎大家来到莉莉的博文✨✨ &#x1f388;&#x1f388;养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; 目录 一、概念&#xff1a; 二、野指针成因&#xff1a; 2.1. 指针未初始化 2.2 指针越界访问 3. 指针指向的空间释放 三、如何规避野指针 3.…

MT6771 android13 自定义背光曲线

一. Android系统源码中的参数配置 MTK6771平台MTK重写了背光曲线的参数&#xff0c;路径在s0_vnd/vendor/mediatek/proprietary/packages/overlay/vendor/FrameworkResOverlayExt/brightness_adaptive_support/res/values/config.xml 不过MTK的其他平台可能不是在这个路径 来看…

C/C++语言学习基础版(一)

目录 一and二、C语言说明 注释&#xff1a; 1、声明语句 2、输出函数 3、return 语句 三、C语言的数据结构 1、常量与变量 2、基本数据结构 3、关键字 练习&#xff1a;进制转换 四、基本输入输出 1、字符输出函数putchar 2、字符输入函数getchar 3、格式化输出函…

JavaParser的快速介绍

开发的工作主要是写代码&#xff0c; 有考虑过使用代码写代码&#xff0c; 使用代码分析和改进代码吗&#xff1f; JavaParser 就可以帮你用来处理Java 代码的这些功能。 Java Parser 的介绍 Java Parser是一个用于解析和分析Java源代码的开源工具。它提供了一个API接口&…

VMware ESXi 6.7.0 开启SNMP服务

VMware ESXi 6.7.0 开启SNMP服务&#xff0c;操作步骤如下&#xff1a; 1、用root账户&#xff0c;登录VMware ESXi 6.7.0 2、开启SSH服务 Host->Actions->Services->Enable Secure Shell(SSH) 主机->操作->服务->启用安全Shell(SSH) 3、登录SSH&#xff…

抽象工厂模式——创建型模式

抽象工厂模式——创建型模式 抽象工厂模式是一种软件设计模式&#xff0c;它解决了在创建一组相关或相互依赖的对象时的一些核心问题。其核心问题包括&#xff1a; 对象的创建与使用分离&#xff1a; 抽象工厂模式通过引入抽象工厂接口以及具体工厂类&#xff0c;将对象的创建与…

如何在Linux使用docker安装Plik并实现无公网ip上传下载内网存储的文件资源

文章目录 1. Docker部署Plik2. 本地访问Plik3. Linux安装Cpolar4. 配置Plik公网地址5. 远程访问Plik6. 固定Plik公网地址7. 固定地址访问Plik 正文开始前给大家推荐个网站&#xff0c;前些天发现了一个巨牛的 人工智能学习网站&#xff0c; 通俗易懂&#xff0c;风趣幽默&…

python学习1:csv模块、time模块、random、jieba、worldcloud、pycharm的虚拟环境认识、black格式化文件

标准库与第三方库 模块&#xff08;modules&#xff09;&#xff1a;是包含python函数和变量的文件&#xff0c;名称符合Python标识符要求&#xff0c;并使用.py后缀 包&#xff08;package&#xff09;&#xff1a;是包含其他模块、包的文件夹。名称符合Python标识符要求&am…