【STL学习】(4)vector的模拟

前言

本文将模拟实现vector的常用功能,目的在于更深入理解vector。

一、前置知识

  1. 在模拟之前先对vector的结构和常用接口学习,有一个大致了解。
  2. 看源码,本文参考的源码是SGI版本的stl3.0。
    • 技巧:
      • 看源码不要一行一行的看,要先看框架,了解整体框架
      • 看源码要学会猜,根据单词的意思去猜它想表达什么。规范的代码,每一个名字都有它的含义。
      • 总结:一看框架;二去猜(带着猜想,去验证)。
    • 看框架的步骤:
      • 先看成员变量
      • 再看成员函数
  3. 参考vector的源码:
    • vector的成员变量:是三个原生指针变量(设为原生指针类型有什么好处,在模拟时讲解)在这里插入图片描述
    • vector的成员函数:vector的常用接口讲解链接
  4. STL中的容器因为需要频繁的申请和释放空间,所以STL中提供了内存池(allocator类模板),内存池的本质是先在堆区中申请一定的空间留作备用,当有新的内存需求时,就从内存池中分出一块内存块,若内存块不够再继续申请新的内存,这样可以提高内存分配的效率。现阶段我们只是简单模拟vector,所以我们这没有使用内存池,而是直接在堆区申请空间,后期会讲解内存池的。

二、vector常用接口的模拟

1、vector的成员变量

vector的成员变量是三个原生指针T*:

  1. _start:开始位置,即指向第一个元素的位置
  2. _finish:结束位置,即指向最后一个元素的下一个位置
  3. _end_of_storage:存储结束位置在这里插入图片描述
    虽然vector使用的是三个原生指针,但是可以通过指针运算得到size和capacity。

代码示例:

//为了避免与库中的vector冲突,将其封装在wjs的命名空间中
namespace wjs
{
	//类模板的实现和定义不分离,后续学到模板进阶会讲解!
	template<class T>
	class vector
	{
	public:
		typedef T* iterator;
		//获取容器中的元素个数
		size_t size()const//内部不改变成员变量,建议加上const——普通对象和const对象都可以调用
		{
			//指针-指针=两者之间的元素个数
			return _finish - _start;
		}

		//获取为当前容器分配的存储空间
		size_t capacity()const//内部不改变成员变量,建议加上const——普通对象和const对象都可以调用
		{
			//指针-指针=两者之间的元素个数
			return _end_of_storage - _start;
		}
	private:
		iterator _start;//开始位置,指向第一个元素
		iterator _finish;//结束位置,指向最后一个元素的下一个位置
		iterator _end_of_storage;//指向存储结束位置
	};
}

tip:

  1. 使用命名空间将我们模拟实现的vector封装,避免命名冲突。
  2. 类模板的定义与实现不分离,后续在模板进阶讲解。
  3. size和capacity可以通过指针-指针得到,注意指针-指针运算有一个前提是:物理存储空间是连续的。
  4. const成员:
    • const修饰的是*this,即const成员函数的内部不能修改成员变量
    • 建议只要成员函数内部不修改成员变量,都应该加const,这样普通对象和const对象都可以调用

2、vector的默认成员函数

构造函数

  • 构造函数:创建类对象时,编译器自动调用,给成员变量赋初值
  • 类的成员变量建议在初始化列表初始化
  • 成员变量为内置类型需要我们手动初始化,不然为随机值;成员变量为自定义类型不初始化,会去调用它的默认构造(建议每个类,都要有一个默认构造)
  • vector的常用构造函数:
    • 默认构造函数:一般使用最多,构造一个空的vector,即将每个成员初始化为空
    • 构造并初始化n个val:先初始化成员变量,再复用reserve开n的空间,最后再通过尾插将val插入容器
    • 使用迭代器初始化构造:先初始化成员变量,再将迭代器区间的数据尾插入容器

析构函数

  • 析构函数:对象销毁时,编译器自动调用,完成对象中资源的清理
  • 编译器生成的析构函数,对内置类型不做处理,自定义类型会去调用它的析构函数
  • 当类涉及动态资源的申请时,就需要显式实现析构释放资源。

赋值重载函数

  • 赋值重载函数:已经存在的两个对象复制拷贝
  • 当类涉及资源管理时,就需要自己显示实现完成深拷贝,编译器默认生成的赋值重载函数只能完成浅拷贝
  • 赋值重载深拷贝的现代写法:让形参去调用拷贝构造,去帮我们开空间拷贝数据,之后与形参交换(函数结束后形参销毁,也顺便帮我们把旧空间释放了)
  • 现代写法无法避免自己给自己赋值的情况,当现实中也很少会出现

拷贝构造函数

  • 拷贝构造函数:用一个已经存在的对象初始化另一个对象
  • 注意:拷贝构造只有一个参数,并且必须是本类型的引用(使用传值会引发无限递归)
  • 编译器默认生成的拷贝构造也是只能完成浅拷贝,所以当类涉及资源管理时,就需要自己显式实现完成深拷贝
  • 拷贝构造深拷贝的现代写法:自己开空间,自己拷贝数据

总结:当类涉及资源管理时,拷贝构造、赋值重载、析构都需要显式实现。

//为了避免与库中的vector冲突,将其封装在wjs的命名空间中
namespace wjs
{
	//类模板的实现和定义不分离,后续学到模板进阶会讲解!
	template<class T>
	class vector
	{
	public:
		//默认构造函数
		vector()
			//初始化列表:成员变量定义的地方,建议在初始化列表初始化成员变量
			//成员变量为内置类型不初始化,为随机值
			:_start(nullptr),
			_finish(nullptr),
			_end_of_storage(nullptr)
		{}
		//构造并初始化n个val
		vector(size_t n, const T& val = T())//T()调用构造函数
			//初始化成员变量
			:_start(nullptr),
			_finish(nullptr),
			_end_of_storage(nullptr)
		{
			//复用reserve,开空间
			reserve(n);
			//复用push_back,尾插n个val
			for (size_t i = 0; i < n; ++i)
			{
				push_back(val);
			}
		}
		//使用迭代器区间初始化
		//类模板的成员函数也可以是函数模板
		template<class InputIterator>
		vector(InputIterator first, InputIterator last)
			//初始化成员变量
			:_start(nullptr),
			_finish(nullptr),
			_end_of_storage(nullptr)
		{
			//复用push_back,将迭代器区间[first,last)的数据尾插进容器
			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}
		//当wjs::vector<int> v(10, 1);报错——》 error C2100: 非法的间接寻址
		//函数重载,调用时会走最匹配的,wjs::vector<int> v(10, 1)两个参数类型都是int,所以他走使用迭代器构造
		//重载一个vector(int n, const T& val = T()),他就会走构造n个val
		//构造并初始化n个val
		vector(int n, const T& val = T())//T()调用构造函数
			//初始化成员变量
			:_start(nullptr),
			_finish(nullptr),
			_end_of_storage(nullptr)
		{
			//复用reserve,开空间
			reserve(n);
			//复用push_back,尾插n个val
			for (int i = 0; i < n; ++i)
			{
				push_back(val);
			}
		}
		
		//交换两个vector对象
		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)//形参v直接就去调用拷贝构造,帮我们开空间可拷贝数据了
		{
			//与v交换
			swap(v);
			return *this;
		}

		//拷贝构造函数
		vector(const vector<T>& v)
		{
			//传统写法:自己开空间自己拷贝
			_start = new T[v.capacity()];
			//注意不能使用memcpy,它只能完成浅拷贝
			for (size_t i = 0; i < v.size(); ++i)
			{
				//当T为自定义类型时,会去调用的它的赋值重载,完成深拷贝
				_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;
			}
		}
	};
}

tip:

  • 重载函数,在调用时,会走匹配的。
  • T():T是一个模板参数,所以T()是一个任意类型的匿名对象。如果T是定义类型会去调用它的默认构造(从这点建议每个类都需要有一个默认构造),如果是内置类型也去调用内置类型的默认构造。
  • 理论上内置类型是没有构造函数的,但是有了模板之后,C++对此做了特殊处理,对内置类型做了升级,也提供了构造。
  • 结论:如果对象中涉及到资源管理时,千万不能使用memcpy进行对象之间的拷贝,因为memcpy是浅拷贝,可能会引起内存泄漏甚至程序崩溃。

3、vector的遍历

迭代器

  • begin():返回指向容器的第一个元素的位置的迭代器
  • end():返回指向容器最后一个元素的下一个位置的迭代器
  • begin和end一般会实现两个版本:普通版本和const版本
  • 有了迭代器,就可以使用范围for,因为范围for的底层就是替换为begin和end

operator[]

  • operator []越界是断言处理(断言只在debug下会生效,release下不生效)
  • operator[]一般也会实现两个版本:一个返回普通引用,一个返回常引用
//为了避免与库中的vector冲突,将其封装在wjs的命名空间中
namespace wjs
{
	//类模板的实现和定义不分离,后续学到模板进阶会讲解!
	template<class T>
	class vector
	{
	public:
		typedef T* iterator;
		typedef const T* const_iterator;
		//普通版本迭代器——迭代器可读可写
		iterator begin()
		{
			//返回指向第一个元素的位置的迭代器
			return _start;
		}
		iterator end()
		{
			//返回指向最后一个元素的下一位置的迭代器
			return _finish;
		}
		//const版本迭代器——迭代器只可读
		const_iterator begin()const
		{
			//返回指向第一个元素的位置的const迭代器
			return _start;
		}
		const_iterator end()const
		{
			//返回指向最后一个元素的下一位置的const迭代器
			return _finish;
		}

		//operator[]
		//普通版本——返回普通引用,可读可写
		T& operator[](size_t pos)
		{
			//operator[]越界断言
			assert(pos >= 0 && pos < size());
			//返回pos位置元素的引用
			return _start[pos];
		}
		//cosnt版本——返回const引用,只可读
		const T& operator[](size_t pos)const
		{
			//operator[]越界断言
			assert(pos >= 0 && pos < size());
			//返回pos位置元素的常引用
			return _start[pos];
		}
	};
}

4、vector的reserve和resize

resize

  • resize将容器大小调整为n
    • 如果n小于当前容器大小,则内容将减少到其前n个元素,删除超出的部分
    • 如果n大于当前容器大小,则通过在末尾插入所需数量的元素来扩展内容,以达到n的大小。如果指定了val,则新元素将初始化为val的副本,否则,它们将进行值初始化
    • 注意:如果n也大于当前容器容量,则会自动重新分配存储空间

reserve

  • reserve请求改变容器的capacity,只有当n>当前容量时,reserve才会重新分配空间,将其容量增加到n(或更大)
namespace wjs
{
	//类模板的实现和定义不分离,后续学到模板进阶会讲解!
	template<class T>
	class vector
	{
	public:
		//调整容器的size
		void resize(size_t n, const T& val = T())
		{
			//如果n>size
			if (n > size())
			{
				//如果n>capacity,一次扩容,避免多次扩容
				reserve(n);
				//尾插val,使size=n
				while (_finish < _start + n)
				{
					push_back(val);
					//++_finish;尾插之后finish会+1,所以这里不能再重复+1了
				}
			}
			else
			{
				//n<size,使size=n,调整_finish的位置即可
				_finish = _start + n;
			}
		}

		//调整容器容量
		void reserve(const size_t n)
		{
			//只有n>capacity时,才会重新开空间,将其capacity增加到n
			if (n > capacity())
			{
				size_t old_size = size();
				iterator tmp = new T[n];
				//判断
				if (_start)
				{
					//memcpy是浅拷贝,所以当拷贝的自定义类型且涉及资源管理时,就会报错!
					//memcpy(tmp, _start, sizeof(T) * old_size);
					for (size_t i = 0; i < old_size; ++i)
					{
						//当T为自定义类型时会去调用它的赋值重载,完成深拷贝
						tmp[i] = _start[i];
					}
					delete[] _start;
				}
				_start = tmp;
				//_start的改变会影响size,所以需要在前面将旧size保存
				_finish = _start + old_size;
				_end_of_storage = _start + n;
			}
		}
	};
}

tip:

  • memcpy是内存的二进制格式拷贝,将一段内存空间中的内容原封不动的拷贝到另一段内存空间中,即memcpy是浅拷贝
  • 当memcpy拷贝的内容不涉及资源管理时,memcpy即高效又不会出错,但当memcpy拷贝的是自定义类型且涉及资源管理时,就会出错,因为memcpy不能完成深拷贝在这里插入图片描述
  • 结论:如果对象中涉及到资源管理时,千万不能使用memcpy进行对象之间的拷贝,因为memcpy是浅拷贝,可能会引起内存泄漏甚至程序崩溃。

5、vector的插入

push_back

  • 尾插,在vector的末尾插入x
  • 尾插需要注意:①尾插之前需要检查是否扩容;②尾插之后size+1,即++_finish
namespace wjs
{
	//类模板的实现和定义不分离,后续学到模板进阶会讲解!
	template<class T>
	class vector
	{
	public:
		//尾插
		void push_back(const T& x)
		{
			//插入之前判断是否需要扩容
			if (_finish == _end_of_storage)
			{
				//扩容
				size_t new_capacity = capacity() == 0 ? 4 : 2 * capacity();
				reserve(new_capacity);
			}
			//尾插
			*_finish = x;
			++_finish;
		}
	};
}

insert

  • 在pos位置的元素之前插入x
  • insert需要注意:①断言pos位置是否合理;②插入之前检查是否需要扩容;③插入之后需要更新size
namespace wjs
{
	//类模板的实现和定义不分离,后续学到模板进阶会讲解!
	template<class T>
	class vector
	{
	public:
		//pos位置的元素之前插入
		void insert(iterator pos, const T& x)
		{
			//断言pos位置是否合理
			assert(pos >= _start && pos <= _finish);
			//插入之前判断是否需要扩容
			if (_finish == _end_of_storage)
			{
				//扩容
				size_t new_capacity = capacity() == 0 ? 4 : 2 * capacity();
				reserve(new_capacity);
			}
			//[pos, _finish - 1]的数据向后挪动,将pos位置空出
			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				--end;
			}
			//pos位置插入x
			*pos = x;
			++_finish;
		}
	};
}

tip:

  • insert在pos位置的元素之前插入x,需要向后挪动数据,在模拟实现string的时候,我们使用的是下标,当是头插的时候,结束条件我们不好控制,因为size_t不会小于0,我们当时使用了npos,现在vector使用iterator就不会出现这种情况了。

测试代码:

//测试insert
void test_vector04()
{
	wjs::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);
	v.push_back(8);
	for (auto& e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}

运行结果:

在这里插入图片描述

分析:

在这里插入图片描述
修改1:

namespace wjs
{
	//类模板的实现和定义不分离,后续学到模板进阶会讲解!
	template<class T>
	class vector
	{
	public:
		//pos位置的元素之前插入
		void insert(iterator pos, const T& x)
		{
			//断言pos位置是否合理
			assert(pos >= _start && pos <= _finish);
			//插入之前判断是否需要扩容
			//注意:扩容之后需要更新pos,否则pos指向释放的旧空间,会造成迭代器失效
			if (_finish == _end_of_storage)
			{
				//计算pos与_start的相对距离
				size_t len = pos - _start;
				//扩容
				size_t new_capacity = capacity() == 0 ? 4 : 2 * capacity();
				reserve(new_capacity);
				//更新pos
				pos = _start + len;
			}
			//[pos, _finish - 1]的数据向后挪动,将pos位置空出
			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				--end;
			}
			//pos位置插入x
			*pos = x;
			++_finish;
		}
	};
}

测试代码:

//测试insert
void test_vector04()
{
	wjs::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);
	v.push_back(8);
	for (auto& e : v)
	{
		cout << e << " ";
	}
	cout << endl;

	//头插100
	auto pos = v.begin();
	v.insert(pos, 100);
	//插入之后修改pos位置的元素
	*pos += 10;
	cout << *pos << endl;
	for (auto& e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}

运行结果:

在这里插入图片描述

分析:

  • 扩容之后,在insert中更新pos只解决了内部迭代器失效的问题,外面的pos并没有解决,它仍指向一块已经释放的空间。
  • 思考:那我们可以将pos参数设为引用,内部的改变也影响外面吗?
    答案是:不可以,因为引用的权限可以平移或缩小,但是不可以放大。当外面传的pos迭代器为一个const迭代器时,引用权限被放大,这是错误的,那将参数也设为常引用呢,这也不可以,因为常引用就不可以修改pos了。
  • insert解决外部pos迭代器失效的方法是插入之后,返回修改的pos迭代器(即指向新插入的第一个元素的迭代器)。

修改2:

namespace wjs
{
	//类模板的实现和定义不分离,后续学到模板进阶会讲解!
	template<class T>
	class vector
	{
	public:
		//pos位置的元素之前插入
		iterator insert(iterator pos, const T& x)
		{
			//断言pos位置是否合理
			assert(pos >= _start && pos <= _finish);
			//插入之前判断是否需要扩容
			//注意:扩容之后需要更新pos,否则pos指向释放的旧空间,会造成迭代器失效
			if (_finish == _end_of_storage)
			{
				//计算pos与_start的相对距离
				size_t len = pos - _start;
				//扩容
				size_t new_capacity = capacity() == 0 ? 4 : 2 * capacity();
				reserve(new_capacity);
				//扩容之后更新pos,解决内部pos失效问题
				pos = _start + len;
			}
			//[pos, _finish - 1]的数据向后挪动,将pos位置空出
			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				--end;
			}
			//pos位置插入x
			*pos = x;
			++_finish;
			//返回形参pos,解决外部pos失效问题
			return pos;
		}
	};
}

测试代码:

void test_vector04()
{
	wjs::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);
	v.push_back(8);
	for (auto& e : v)
	{
		cout << e << " ";
	}
	cout << endl;

	//头插100
	auto pos = v.begin();
	//insert之后,pos迭代器可能会失效(扩容)
	//记住,insert之后就不要使用这个pos迭代器,因为它可能失效了
	//使用这个pos迭代器是一个高危行为
	//如果非要使用这个pos这个位置的迭代器,可以接收insert的返回值
	//insert的返回值就是指向pos这个位置的迭代器
	auto ret = v.insert(pos, 100);
	*ret += 10;
	cout << *ret << endl;
	for (auto& e : v)
	{
		cout << e << " ";
	}
	cout << endl;
}

tip:

  • insert之后,pos迭代器可能会失效(扩容)
  • 记住,insert之后就不要使用这个pos迭代器,因为它可能失效了
  • 使用这个pos迭代器是一个高危行为

push_back可以复用insert:

namespace wjs
{
	//类模板的实现和定义不分离,后续学到模板进阶会讲解!
	template<class T>
	class vector
	{
	public:
		//尾插
		void push_back(const T& x)
		{
			插入之前判断是否需要扩容
			//if (_finish == _end_of_storage)
			//{
			//	//扩容
			//	size_t new_capacity = capacity() == 0 ? 4 : 2 * capacity();
			//	reserve(new_capacity);
			//}
			尾插
			//*_finish = x;
			//++_finish;

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

6、vector的删除

erase

  • 删除pos位置的元素
  • erase需要注意:①断言pos位置是否合理(即有没有数据);②删除之后更新size,即_finish。

思考 erase存在迭代器失效吗?

在这里插入图片描述

  • erase删除pos位置元素后,迭代器的意义变了(指向删除的最后一个元素之后的元素的新位置,理论上迭代器并没有失效,因为删除并没有改变底层空间)
  • 注意:如果pos是最后一个元素,删除之后pos刚好是_finish的位置,而_finish位置是没有元素的,所以pos迭代器失效
  • VS系列下检测比较严格,删除vector任意位置上的元素,都认为该位置迭代器失效了
  • Linux下,g++编译器对迭代器失效的检测就相对佛系,处理没有VS下极端,只有删除vector最后一个元素,才认为迭代器失效了(在实际场景中,迭代器的意义变了,也容易出现各种问题)
  • 总结:vector 迭代器对象在erase或insert后,不能再访问这个迭代器,我们都认为它失效了,访问结果是未定义的。
  • erase通过返回一个指向删除的最后一个元素之后的元素的新位置迭代器解决迭代器失效问题。
namespace wjs
{
	//类模板的实现和定义不分离,后续学到模板进阶会讲解!
	template<class T>
	class vector
	{
	public:
		//删除pos位置的元素
		iterator erase(iterator pos)
		{
			//断言pos是否合理
			assert(pos >= _start && pos < _finish);
			//删除pos位置元素,即将[pos+1, _finish - 1]的元素向前挪动
			iterator begin = pos + 1;
			while (begin < _finish)
			{
				*(begin - 1) = *begin;
				++begin;
			}
			--_finish;
			//erase通过返回一个指向删除的最后一个元素之后的元素的新位置迭代器解决迭代器失效问题,即pos迭代器
			return pos;
		}
	};
}

测试代码:

void test_vector05()
{
	wjs::vector<int> v2;
	v2.push_back(1);
	v2.push_back(2);
	v2.push_back(2);
	v2.push_back(3);
	v2.push_back(4);
	v2.push_back(5);
	v2.push_back(6);
	for (auto& e : v2)
	{
		cout << e << " ";
	}
	cout << endl;
	//删除所有偶数
	auto it2 = v2.begin();
	while (it2 != v2.end())
	{
		if (*it2 % 2 == 0)
		{
			//erase之后迭代器失效
			//解决方案:it2需要接收erase的返回值
			it2 = v2.erase(it2);
		}
		else
		{
			++it2;
		}
	}
	for (auto& e : v2)
	{
		cout << e << " ";
	}
	cout << endl;
}

tip:

  • insert和erase之后的迭代器失效,是通过接收返回值解决的

pop_back

  • 尾删,删除vector中的最后一个元素,尾删之后size-1
  • 这里直接复用erase即可
namespace wjs
{
	//类模板的实现和定义不分离,后续学到模板进阶会讲解!
	template<class T>
	class vector
	{
	public:
		void pop_back()
		{
			//复用erase
			erase(_finish - 1);
		}
	};
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Severt

severt是让我们自己写一些类,然后把这些类给加载Tomcat中&#xff0c;后续Tomcat收到HTTP请求(来自于浏览器)&#xff0c;就会执行到咱们上面写的代码.从而通过这些代码,完成一定的业务逻辑. 创建项目 此处创建的是一种新的项目的形式称为Maven项目,Maven是Java 中的一个的构建…

libVLC 音频立体声模式切换

在libVLC中&#xff0c;可以使用libvlc_audio_set_channel函数来设置音频的立体声模式。这个函数允许选择不同的音频通道&#xff0c;例如立体声、左声道、右声道、环绕声等。 /*** Set current audio channel.** \param p_mi media player* \param channel the audio channel…

Datacom HCIP笔记-路由策略与路由控制 之二

路由策略和策略的区别&#xff1f; 路由策略&#xff1a; 操作的对象是路由表条目&#xff0c; 实现路由过滤&#xff0c;从而实现访问控制&#xff0c;引入时过滤&#xff0c;发送和接收路由时过滤。 通过配置cost&#xff0c;来实现路径的控制。 策略路由&#xff1a; 对…

【Python】还在用print进行调试,你Out了!!!

1. 引言 Python 中最常用的函数是什么&#xff1f;像在大多数编程语言中&#xff0c;print() 函数是最常用的。我相信大多数开发者都会像我一样&#xff0c;在开发过程中多次使用它将信息进行打印。 当然&#xff0c;没有其他方法可以完全取代print()函数。不过&#xff0c;当…

QA测试开发工程师面试题满分问答9: Python中内存管理的概念、原理、使用

概念原理 Python中的内存管理是由解释器自动处理的&#xff0c;它使用引用计数和垃圾回收机制来管理内存。以下是Python内存管理的一些关键概念、设计原理和最佳实践&#xff0c;以帮助您高效使用和管理内存&#xff1a; 引用计数&#xff1a;Python使用引用计数来追踪对象的引…

谷歌浏览器如何截全屏图片?

有时候想要截取浏览器全屏&#xff0c;谷歌浏览器自带截取全屏命令&#xff0c;操作步骤如下&#xff1a; 1、按住键盘的F12或者是空白处点击鼠标右键找到检查项 2、按住ctrlshiftp&#xff0c;会出现搜索框的界面 3、搜索框中输入screen&#xff0c;选中Capture full size scr…

项目架构MVC,DDD学习

写在前面 本文一起看下项目架构DDD&#xff0c;MVC相关的内容。 1&#xff1a;MVC 不管我们做什么项目&#xff0c;自己想想其实只是做了三件事&#xff0c;如下&#xff1a; 其实&#xff0c;这三件事完全在一个类中做完也可以可以正常把项目完成的&#xff0c;就像下面这…

Redis简介、常用命令

目录 一、关系数据库​​与非关系数据库​​ 1.1. 关系型数据库 1.2 非关系型数据库 1.3.关系数据库与非关系型数据库区别 1.3.1. 数据存储方式不同 1.3.2. 扩展方式不同 1.3.3.对事务性的支持不同 1.4.非关系型数据库产生背景 二、Redis 2.1.Redis简介 2.2.Redis的…

如何使用开源情报跟踪一个人?在线访问网站以及使用方法介绍

如何使用开源情报跟踪一个人&#xff1f;在线访问网站以及使用方法介绍。 开源情报&#xff08;OSINT&#xff09;是一门关于收集和分析公开可用信息的独特技艺&#xff0c;它致力于构建个人或团体的详尽档案。 这一过程中&#xff0c;信息搜集者会利用多元化的信息源&#xff…

火山方舟大模型服务平台调用Demo测试(豆包)

豆包得后台大模型支持为字节得火山方舟&#xff0c;所以想使用豆包的API&#xff0c;直接从这里就可以。 一、首先注册账号&#xff1a; 火山引擎-云上增长新动力 注册完成之后&#xff0c;控制台-账户-API访问密钥 二、找到API测试用例&#xff1a; Skylark-chat API调用…

Linux实验3 shell命令进阶

一&#xff1a;实验目的 学习Linux下的文件系统结构&#xff0c;了解最基本的Linux下的shell命令操作&#xff0c;例如ls, cd, cat等各种指令操作。 学习vim编辑器的使用方式&#xff0c;学习如何使用ssh连接远程服务器。 二&#xff1a;实验内容 1&#xff0e;利用ls命令查找…

记一次Debug与Release版程序输出不一致的问题解决

问题叙述&#xff1a; 在x86平台下无论Debug还是Release都没问题&#xff0c;而在arm平台下Debug版本程序无问题&#xff0c;Release版本程序&#xff08;-O3编译&#xff09;发现输出值不正确&#xff0c;怀疑值被篡改&#xff0c;于是在调用前后分别使用printf打印出参数值&…

pdf操作器(图片转文字、PDF转word、PDF拆分、图片jpg、png互转)

pdf操作器&#xff08;不用联网图片转文字、PDF转word、PDF拆分、图片jpg、png互转&#xff09;介绍目前该软件实现了以下功能 pdf转wordpdf拆分图片&#xff0c;图片导出在桌面的一个文件夹里图片合并为pdf压缩、转换图片格式&#xff08;jpg和png&#xff09;OCR图片转文字&…

Leetcode刷题-哈希表详细总结(Java)

哈希表 当我们想使⽤哈希法来解决问题的时候&#xff0c;我们⼀般会选择如下三种数据结构。 数组set &#xff08;集合&#xff09;map&#xff08;映射&#xff09; 当我们遇到了要快速判断⼀个元素是否出现集合⾥的时候&#xff0c;就要考虑哈希法。如果在做⾯试题⽬的时候…

【Frida】【Android】 10_爬虫之WebSocket协议分析

&#x1f6eb; 系列文章导航 【Frida】【Android】01_手把手教你环境搭建 https://blog.csdn.net/kinghzking/article/details/136986950【Frida】【Android】02_JAVA层HOOK https://blog.csdn.net/kinghzking/article/details/137008446【Frida】【Android】03_RPC https://bl…

鸿蒙HarmonyOS 与 Android 的NDK有什么不一样?

1. 序言 就像开发Android要用Android Studio一样&#xff0c;Android Studio&#xff08;简称AS&#xff09;其实是基于IDEAgradle插件android插件开发而来。 鸿蒙系统&#xff0c;你可以认为它和android有点像&#xff0c;但又是超越android的存在&#xff0c;除了手机&…

IO流

一、IO概述 1&#xff0e;什么是IO流? 存储和读取数据的解决方案l: inputo: output流∶像水流一样传输数据 2.IO流的作用? 用于读写数据&#xff08;本地文件&#xff0c;网络) 3.IO流按照流向可以分类哪两种流? 输出流:程序 - > 文件 输入流:文件 - > 程…

30道python自动化测试面试题与答案汇总!

Python是不可或缺的语言,它的优美与简洁令人无法自拔,下面这篇文章主要给大家介绍了关于30道python自动化测试面试题与答案汇总的相关资料,需要的朋友可以参考下 1、什么项目适合做自动化测试&#xff1f; 关键字&#xff1a;不变的、重复的、规范的 1&#xff09;任务测试明…

Collection与数据结构 Stack与Queue(二):队列与Queue

1. 队列 1.1 概念 只允许在一端进行插入数据操作&#xff0c;在另一端进行删除数据操作的特殊线性表&#xff0c;队列具有先进先出FIFO(First In First Out) 入队列&#xff1a;进行插入操作的一端称为队尾&#xff08;Tail/Rear&#xff09; 出队列&#xff1a;进行删除操作…

JVM内存性能调优思路之:通过GC log、Thread Dump 、Heap Dump分析内存使用说明

文章目录 一. 各日志概述1. Garbage Collection Log - 找到GC规律2. 线程转储(Thread dump) - 分析&#xff08;快照&#xff09;线程状态3. 堆转储(Heap dump) - APP某刻内存使用全貌 二. 命令1. 程序的gc日志2. 线程转储3. 堆转储 概述 在 Java 虚拟机中&#xff0c;(GC) Gar…