漫步STL-vector in 【Cpp】 V.S. Vector in 【Java】

image

image-20220315091801713

0. Intro

和上一篇string的学习一样,刚刚学习了cpp中STL提供的vector类,之前也有浅学Java相关Vector类的知识点,于是希望整理有关知识,加深记忆,并区分不同的方法与函数,巩固记忆,当然重点还是放在STL中的vector类上

1. Vector in Java

我们还是先来回顾一下Vector in Java

1.1 Vector类的层次结构图

image-20220307191428549

1.2 Vector快速入门

Vector底层也是一个对象数组

protected Object[] elementData;

Vector线程安全,Vector类的操作方法带有synchronized,开发中若有线程安全需求,使用Vector

public synchronized E get(int index) {
if (index > = elementCount)
	throw new ArrayIndexOutOfBoundsException(index) 
    return elementData(index)
}

1.3 Vector底层浅析

// new Vector() 底层
//无参构造器
public Vector() {
	this(10);
}

//如果是Vector vector = new Vector(8);
//有参构造器:
public Vector(int initialCapacity) {
	this(initialCapacity, 0);
}

//Vector.add(i)
//下面这个方法就添加数据到vector 集合
public synchronized boolean add(E e) {
	modCount++;
	ensureCapacityHelper(elementCount + 1);
	elementData[elementCount++] = e;
	return true;
}

//确定是否需要扩容条件: minCapacity - elementData.length>0
private void ensureCapacityHelper(int minCapacity) {
	// overflow-conscious code
	if (minCapacity - elementData.length > 0)
		grow(minCapacity);
}

//如果需要的数组大小不够用,就扩容, 扩容的算法
//newCapacity = oldCapacity + ((capacityIncrement > 0) ?
// capacityIncrement : oldCapacity);
//就是扩容两倍.
private void grow(int minCapacity) {
	// overflow-conscious code
	int oldCapacity = elementData.length;
	int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
		capacityIncrement : oldCapacity);
	if (newCapacity - minCapacity < 0)
		newCapacity = minCapacity;
	if (newCapacity - MAX_ARRAY_SIZE > 0)
		newCapacity = hugeCapacity(minCapacity);
	elementData = Arrays.copyOf(elementData, newCapacity);
}

1.4 Vector 和ArrayList 的比较

我们常常会在ArrayList和Vector中做比较和选择

image-20220307192559358

2. vector in Cpp

2.0 Intro

首先要知道vector其实是一个可以动态增长的数组,看看文档

🍁 vector是表示可变大小数组的序列容器。
🍁 就像数组一样,vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问,和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。
🍁 本质讲,vector使用动态分配数组来存储它的元素。当新元素插入时候,这个数组需要被重新分配大小为了增加存储空间。其做法是,分配一个新的数组,然后将全部元素移到这个数组。就时间而言,这是一个相对代价高的任务,因为每当一个新的元素加入到容器的时候,vector并不会每次都重新分配大小。
🍁 vector分配空间策略:vector会分配一些额外的空间以适应可能的增长,因为存储空间比实际需要的存储空间更大。不同的库采用不同的策略权衡空间的使用和重新分配。但是无论如何,重新分配都应该是对数增长的间隔大小,以至于在末尾插入一个元素的时候是在常数时间的复杂度完成的
🍁 因此,vector占用了更多的存储空间,为了获得管理存储空间的能力,并且以一种有效的方式动态增长。
🍁 与其它动态序列容器相比, vector在访问元素的时候更加高效在末尾添加和删除元素相对高效。对于其它不在末尾的删除和插入操作,效率更低比起lists和forward_lists统一的迭代器和引用更好

2.1 模板和泛型的应用

利用模板vector就可以存入多种类型的值

template<class T>
void PrintVector(const vector<T>& v)
{
	vector<T>::const_iterator it1 = v.begin();
	while (it1 != v.end())
	{
		cout << *it1 << " ";
		++it1;
	}
	cout << endl;
}

2.2 常用构造函数接口

(constructor)构造函数接口说明
vector() 无参构造
vector(size_type n, const value_type& val = value_type())构造并初始化n个val
vector (const vector& x); 拷贝构造
vector (InputIterator first, InputIterator last);使用迭代器进行初始化构造
	//无参构造
	vector<int> v1;
	//构造并初始化n个val
	vector<int> v2(10, 0);
	//使用迭代器进行初始化构造
	vector<int> v3(v2.begin(), v2.end());

思考一下可不可以用vector里面存放着char来替代string

	//可以和string一起用
	string s("hello world");
	vector<char> v4(s.begin(), s.end());

不太行,vector没有+=,append,比较,且没有\0

2.2.1 结合模板快捷操作

下面的时候往往可以一步到位,这背后存在着优化

	vector<string> v5;

//两步完成
	string s3("sort");
	v5.push_back(s3);
	
//一步完成
	v5.push_back(string("Hello"));

// 更直接的推荐方法
	v5.push_back("H");

2.3 遍历Element access

三种遍历方式

2.3.1 下标
	// 遍历1
	for (size_t i = 0; i < v1.size(); ++i)
	{
		cout << v1[i] << " ";
	}
	cout << endl;
2.3.2 迭代器和反向迭代器Iterators

迭代器也是可遍历可修改的

iterator的使用接口说明
begin +end获取第一个数据位置的iterator/const_iterator, 获取最后一个数据的下一个位置的iterator/const_iterator
rbegin + rend获取最后一个数据位置的reverse_iterator,获取第一个数据前一个位置的reverse_iterator
迭代器

image-20220311155916926

// 遍历2
	vector<int>::iterator it1 = v1.begin();
	while (it1 != v1.end())
	{
		*it1 += 1;
		cout << *it1 << " ";
		++it1;
	}
	cout << endl;

注意const对象对应的是const迭代器

void PrintVector(const vector<int>& v)
{
	//v[0] = 20;
	vector<int>::const_iterator it1 = v.begin();
	while (it1 != v.end())
	{
		//*it1 += 1;
		cout << *it1 << " ";
		++it1;
	}
	cout << endl;
}
反向迭代器

用auto可以简化代码,使用比较方便

	vector<string> copy(v5);
//vector<string>::reverse_iterator rit = copy.rbegin();
	auto rit = copy.rbegin();
	while (rit != copy.rend())
	{
		cout << *rit << " ";
		++rit;
	}
	cout << endl;
2.3.3 范围for循环
// 遍历3 (自动判断结束,自动迭代++)
	// 实际上其实就是被替换成了迭代器
	for (auto e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

用的最多的还是下标方式的访问方法,简单又好用,当时迭代器也得掌握,因为任何容器都可以使用迭代器方式,一般指针形式的都是可读可写的

还有一点要注意就是说vector底层确实是有指针实现的,但不是所有的都是,list就不一定是

2.3 增删查改Modifiers

vector增删查改接口说明
push_back 尾插
pop_back 尾删
find查找(注意这个是算法模块实现,不是vector的成员接口)
insert在position之前插入val
erase删除position位置的数据
swap交换两个vector的数据空间
operator[ ]像数组一样访问

####🌿 push_back

使用案例

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(), 0);

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

下面来看看查找,如果我要在指定位置插入一个数据怎么办,其实用find即可,这个find在文档中并没有放在vector一章里面,而是在algorithm中,所以要使用的时候要引一下头文件

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

	// 3的前面插入一个30
	vector<int>::iterator pos = find(v.begin(), v.end(), 3);
	if (pos != v.end())
	{
		v.insert(pos, 30);
	}

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

一般情况下,建议少用insert和erase,因为这两个如果完成的是头部或者中间插入删除的话,他们的效率是O(N),因为要挪动数据

🌿 swap

推荐原因就是因为如果用第一个会调用三次深拷贝,代价很大

	// C++98 推荐第二个
	swap(v1, v2);
	v1.swap(v2);
	// C++11 都一样

C++11中又要另说,可以达到不用深拷贝

2.4 空间增长Capacity

容量空间接口说明
size获取数据个数
capacity获取容量大小
empty判断是否为空
resize 改变vector的size
reserve改变vector放入capacity

String中用的很少但是Vector用得很多的地方:resize扩容问题

resize在对象中插入n个空间,默认设置为'\0',如果空间需要减少,就应该是把空间保留到该数字的大小个空间,而且最后一个空间肯定是放成'\0',capacity是不一定改变的

resize函数一般不会按照你给的数字进行扩容,而是会对齐扩容到4/8的倍数

	s1.resize(20, 'x');
	cout << "size:" << s1.size() << endl;
	cout << "capacity:" << s1.capacity() << endl;
	cout << s1 << endl;

对于capacity不同的STL版本实现的效果是不同的

	// 验证vector的增容方式
	size_t sz;
	std::vector<int> foo;
	sz = foo.capacity();
	std::cout << "making foo grow:\n";
	for (int i = 0; i < 100; ++i) {
		foo.push_back(i);
		if (sz != foo.capacity()) {
			sz = foo.capacity();
			std::cout << "capacity changed: " << sz << '\n';
		}
	}

比如VS上的PJ版STL按照的就是1.5倍的增容方式

而Linux上的SGI版本STL就是按照2倍的增容方式

2.5 迭代器失效

迭代器的主要作用就是让算法能够不用关心底层数据结构,其底层实际就是一个指针,或者是对指针进行了封装,比如:vector的迭代器就是原生态指针T*。因此迭代器失效,实际就是迭代器底层对应指针所指向的空间被销毁了,而使用一块已经被释放的空间,造成的后果是程序崩溃(即如果继续使用已经失效的迭代器,程序可能会崩溃)。

会引起其底层空间改变的操作,都有可能是迭代器失效,比如:resize、reserve、insert、assign、push_back等。

2.5.1 情况一

下面的代码中我先是在vector中初始化进入123456的数据,然后定位在3之前插入30,此时我要再去删除3的时候就产生了问题,如下图所示

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

	// 3的前面插入一个30
	vector<int>::iterator pos = find(v.begin(), v.end(), 3);
	if (pos != v.end())
	{
		v.insert(pos, 30);
	}

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

	// 删除3
	//这个pos已经失效了
		v.erase(pos);
	for (auto e : v)
	{
		cout << e << " ";
	}
	cout << endl;

下面是两种出错的情况,一种是删除错误的位置,还有一种错误是直接报错

一般情况会是删除失败

image-20220311110807666

直接崩溃是因为如果在我插入新数据的时候vector的size已经满了,这时候需要增容

image-20220311111404662

image-20220311110824552

2.5.2 情况一问题解决

下面是合理的处理方式,只要再重新找一遍

	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);
	// 3的前面插入一个100
	vector<int>::iterator pos = find(v.begin(), v.end(), 3);
	if (pos != v.end())
	{
		v.insert(pos, 100);
	}
	// 删除3
	// pos在insert以后就失效了,所以我们不要用他
		pos = find(v.begin(), v.end(), 3);
		if (pos != v.end())
		{
			v.erase(pos);
		}

image-20220311111151116

2.5.3 情况二

当我要尝试删除所有的偶数

下面是问题代码

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);
	
	vector<int>::iterator it = v.begin();

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

请添加图片描述

在这里Linux的情况,则g++又不一样了,有时候报错有时候不报错这个问题的原因我们来分析一下,下面两张图很精华

image-20220311162608729

image-20220311163208689

VS可能是做的检查,所以不管最后一个数是奇数还是偶数,都会产生错误导致报错

2.5.4 情况二问题解决

既然是错误代码,那肯定要解决,下面是正确代码

erase(it)以后 it失效,不能++,
erase 会返回删除位置it的下一个位置用it接受就可以了

	vector<int>::iterator it = v.begin();
	while (it != v.end())
	{
		if (*it % 2 == 0)
		{
			// v.erase(it)以后 it失效,不能++,
			// erase 会返回删除位置it的下一个位置
			it = v.erase(it);
		}
		else
			++it;
	}

🌸 总结一下迭代器失效有两种可能性

  1. 一种是意义上的失效
  2. 还有是野指针(增容,缩容)

🌸 迭代器失效有两种原因

  1. 迭代器失效常常是由会引起其底层空间改变的操作,都有可能是迭代器失效,比如:resize、reserve、insert、assign、push_back等。
  2. 指定位置元素的删除操作–erase

3. vector模拟实现

先看看源码

image-20220312101611178

image-20220312101637494

From《STL源码刨析》

3.1 构造一个简单的vector雏形

下面是一个vector雏形,函数接口不完善,且可能还有bug,不直接写对,为的是按照一个正常逻辑来写vector并记录下易错的函数以及问题,尤其注意其中的reserve

namespace allen
{
	template <class T>
	class vector
	{
	public:
		typedef T* iterator;
			
		//构造函数
			vector()
			:_start(nullptr)
			, _finish(nullptr)
			, _endofstorage(0)
		{}
			vector(size_t n, T val)
			: _start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr)
		{
			reserve(n);
			while (n--)
			{
				push_back(value);
			}
		}
        
		iterator begin()
		{
			return _start;
		}
        
		iterator end()
		{
			return _finish;
		}	
		
        	//析构
		~vector()
		{
			if (_start)
				delete[] _start;
			_start = _finish = _endofstorage = nullptr;
		}
        
		size_t capacity()
		{
			return _endofstorage - _start;
		}

		size_t size()
		{
			return _finish - _start;
		}

		void reserve(size_t n)
		{
			if (n > capacity())
			{
				T* tmp = new T[n];
				if (_start)//一开始是空的
				{
					memcpy(tmp,_start,sz*sizeof(int));
					delete[] _start;
				}
				_start = tmp;
				_finish = _start + size();
				_endofstorage = _start + n;
			}
		}
	private:
		iterator _start;
		iterator _finish;
		iterator _endofstorage;
	};

3.2 进一步完善vector

🌿 push_back

当我要去写push_back的时候产生了报错,这是因为之前雏形有问题所以产生了连带错误

		void push_back(const T& x)
		{
			if (_finish == _endofstorage)
			{
				size_t  newcapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newcapacity);
			}
			*_finish = x;
			++_finish;
		}

image-20220312110424129

🌿 reserve

那改进一下reserve,在start改之前保存一份

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

补充一个resize,resize有三种情况

其一是resize到比现在的finish之前,那多出的数据就可以不移除,直接改变finish指针的位置就可以

情况二是大于size(),小于capacity(),那么就补值就可以了

情况三是大于capacity(),那么需要reserve开空间同时补值

		void resize(size_t n , T val= T() )//默认初始化成构造函数
		{//新的小
			if (n < size())
			{
				_finish = _start + n;
			}
			else 
			{//在中间
				if (n < capacity())
				{
					while (_finish < _start + n)
					{
						*_finish = val;
						++_finish;
					}
				}
				else
				{//超过capacity
					reserve(n);//开新空间,拷贝
					while (_finish < _start + n)
					{
						*_finish = val;
						++_finish;
					}
				}
			}
		}
🌿 insert

会有迭代器失效的insert

下面的代码是在内部也会存在迭代器失效,因为如果增容的话,原来的pos已经失效了

		void insert(iterator pos, const T& x)
		{
			if (_finish == _endofstorage)
			{//增容
				size_t newcapacity = (capacity() == 0) ? 4 : capacity() * 2;
				reserve(newcapacity);
			}
			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) =* end;
				--end;
			}
			*pos = x;
			++_finish;
		}

解决方法就是提前处理pos

		void insert(iterator pos, const T& x)
		{
			if (_finish == _endofstorage)
			{//增容
				size_t len = pos - _start;
				size_t newcapacity = (capacity() == 0) ? 4 : capacity() * 2;
				reserve(newcapacity);
				//更新pos,解决pos增容后失效
				pos = _start + len;
			}
			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) =* end;
				--end;
			}
			*pos = x;
			++_finish;
		}

不过解决了内部的失效问题,外部的迭代器失效还是没解决,形参的改变时不会改变外部的实参

如何写一个外部也不失效的insert,传引用且改变pos值。使得pos指向pos后一个位置

		void insert(iterator& pos, const T& x)
		{
			if (_finish == _endofstorage)
			{//增容
				size_t len = pos - _start;
				size_t newcapacity = (capacity() == 0) ? 4 : capacity() * 2;
				reserve(newcapacity);
				//更新pos,解决pos增容后失效
				pos = _start + len;
			}
			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) =* end;
				--end;
			}
			*pos = x;
			++_finish;
			pos += 1;
		}

那STL中不使用不失效的方法就是因为会导致其他问题

所以说还是,在insert之后最好不要使用pos,一定要用就重新find一次

🌿 erase

根据下面的erase可以发现迭代器失效的原因

		iterator erase(iterator pos)
		{
			iterator it = pos + 1;
			while (it != finish)
			{
				*(it - 1) = *it;
				++it;
			}
			--_finish;
			return pos;
		}
🌿 []重载

继续增加,下面是可读可写和只读

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

			return _start[i];
		}
🌿 empty

判空

		bool empty() const
		{
			return _start == _finish;
		}
🌿 pop_back
		void pop_back()
		{//空的时候不可以pop
			assert(!empty());
			--_finish;
		}
🌿 迭代器初始化

构造器中还提供了一个接口就是利用迭代器来初始化

若使用iterator做迭代器,会导致初始化的迭代器区间[first,last)只能是vector的迭代器
所以重新声明迭代器,迭代器区间[first,last)可以是任意容器的迭代器

	// 类模板的成员函数,还可以再是函数模板
			template<class InputIterator>
			vector(InputIterator first, InputIterator last)
				: _start(nullptr)
				, _finish(nullptr)
				, _endofstorage(nullptr)
			{
				while (first != last)
				{
					push_back(*first);
					++first;
				}
			}
🌿 拷贝构造

探讨拷贝构造,也就是深拷贝

下面一种memcpy的方法可能会存在问题的(该问题后面马上提到),所以用reserve,但是要用reserve的话要初始化,因为不初始化可能进入reserve之后会越界

//拷贝构造,深拷贝
		// v2(v1)
			vector(const vector<T>& v)
				: _start(nullptr)
				, _finish(nullptr)
				, _endofstorage(nullptr)
			{//开一样大的空间
				reserve(v.capacity());
				for (auto e : v)
				{
					push_back(e);
				}
				//_start=new T[v.capacity()];
				//memcpy(_start,v._start,sizeof(T)*v.size());
				// _endofstorage=_start+v.capacity();
				//_finish=_start+=v.size();
			}
🌿 赋值运算符重载

传统方法

			//v1=v2
			vector<T>& operator=(const vector<T>& v)//加一个const防止传过来的对象是const的
			{
				if (this != &v)
				{
					delete[]_start;
					_start = new T[v.capacity()];
					memcpy(_start, v._start, sizeof(T) * v.size());
					_finish = _start + v.size();
					_endofstorage = _start + v.capacity();
				}
			}

推荐现代方法(增加一个函数swap)

			void swap(vector<T>& v)
			{
				::swap(_start, v._start);
				::swap(_finish, v._finish);
				::swap(_endofstorage, v._endofstorage);
			}
			//v1=v2
			vector<T>& operator=( vector<T> v)//加一个const防止传过来的对象是const的
			{
					swap(v);
					return *this;
			}

3.3 深层次深浅拷贝问题

3.3.1 分析问题

问题来源于memcpy

这是一个深层次的问题,在于在有些情况中有可能vector中存着string类型,这是我们试着测试一下

		vector<string> v;
		v.push_back("1111111111111111111111111111111111");
		v.push_back("22222");
		v.push_back("333333333333333333333333333333333");
		v.push_back("44444");
		v.push_back("55555");
		//string时加一个引用,减少拷贝构造
		for (const auto& e : v)
		{
			cout << e << " ";
		}
		cout << endl;

在VS中第五次插入要增容但是程序却崩溃了,而且如果是某一次的string超过一定数量的字符串的话,就会出现随机值

调试以下发现问题在一个reserve中的memcpy

因为memcpy只是一个浅拷贝

		void reserve(size_t n)
		{
			if (n > capacity())
			{
				size_t sz = size();//这个很重要
				T* tmp = new T[n];
				if (_start)//一开始是空的
				{
					memcpy(tmp,_start,sz*sizeof(int));
					delete[] _start;
				}
				_start = tmp;
				_finish = _start + sz;
				_endofstorage = _start + n;
			}
		}

image-20220314124430744

image-20220314125227360

这个问题在VS和g++下还不一样,VS中string里面由于有一个buffer数组,如果大小超过16的话才会在堆中,指向堆中的字符串,如果不到16个的话就是存到成员内部buffer中,而g++中则没有buffer数组,只要这么干,不管多少字符都是随机值

3.3.2 解决问题

改成这样就没问题了

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

为什么会没问题

image-20220314132227280

STL是怎么解决的呢?通过类型萃取

⚡️ 内置类型用memcpy

⚡️ 自定义类型用for+赋值

小结

image-20220314130320265

因此拷贝构造中的memcpy也会出现同样的问题,那么同样的问题就不赘述了

以上就是模拟实现的代码,如果想要所有的代码可以上我的Github自取
https://github.com/Allen9012/cpp/tree/main/c%2B%2B%E5%88%9D%E9%98%B6/vector3

  • 12
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 13
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

言之命至9012

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值