C++——Vector

本文详细介绍了C++vector容器的特性、常见接口(如reserve、push_back、insert、erase和resize)以及模拟实现过程,特别关注了迭代器失效的问题及其解决方案。同时讨论了浅拷贝和深拷贝在vector中的应用以及正确使用vector构造函数的注意事项。
摘要由CSDN通过智能技术生成

目录

一介绍

特点

二常见接口

构造

capacity

 Modifiers

三模拟实现

1reserve

2push_back

3insert

4erase

5resize

6迭代器的函数模板

7拷贝构造

四迭代器失效

场景1

场景2

分析

最后


如果你在前面有先学习完string后再来vector容器,学习起来想必会轻松一点。因为学习各种容器的使用接口是大差不差的。

一介绍

vector的英文翻译过来是向量,矢量的意思。但别被它的意思理解错了:vector容器本质上是我们学习数据结构的顺序表

特点

1. vector是表示可变大小数组的序列容器。
2. 就像数组一样,vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vecto的元素进行访问,和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。
3. 本质讲,vector使用动态分配数组来存储它的元素。当新元素插入时候,这个数组需要被重新分配大为了增加存储空间。其做法是,分配一个新的数组,然后将全部元素移到这个数组。就时间而言,这是一个相对代价高的任务,因为每当一个新的元素加入到容器的时候,vector并不会每次都重新分配大小。(开辟一块较大的空间)
4. vector分配空间策略:vector会分配一些额外的空间(buffer)以适应可能的增长,因为存储空间比实际需要的存储空间更大。不同的库采用不同的策略权衡空间的使用和重新分配。

二常见接口

构造

与string不同的是:vector可以采用空间配置器来进行构造:

explicit vector (const allocator_type& alloc = allocator_type());

也可以使用n个val值来进行构造:

explicit vector (size_type n, const value_type& val = value_type()

capacity

使用resize适合在开空间并进行初始化:(val如果使用缺省值会根据vector的类型来确定)

void resize (size_type n, value_type val = value_type());

使用reserve就仅仅是开辟空间:

void reserve (size_type n);

 Modifiers

push_back:在数组末尾插入一个元素:

void push_back (const value_type& val);

pop_back:在数组末尾删除一个元素:

void pop_back();

insert:在position位置前插入(多个)元素:

iterator insert (iterator position, const value_type& val)
 void insert (iterator position, size_type n, const value_type& val);

erase:在position位置上删除元素:

iterator erase (const_iterator position);
iterator erase (const_iterator first, const_iterator last);

三模拟实现

对各种常见接口进行模拟实现才能更好的理解它,使用它。

verctor为了能够兼容各种类型,它用类模板的形式来实现:

template<class T>

class vector {...}

在vector源码中,成员变量有三个:start,finsh,end_of_storage

其中start指向第一个元素,finsh指向最后一个元素的下一个,end_of_storage指向vector最大空间

所以实现时,成员变量都应该是指针类型:T*

为了待会能够进行与迭代器联系起来,进行重命名:

typedef T* iterator

将成员变量指针转化为在string中用的size(),capacit()的size_t类型的值:

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

1reserve

在实现之前要实现vector的构造函数;但又由于成员变量都是指针类型,所以我们在定义成员变量时后面直接给上缺省值:nullptr;写构造函数直接:vector() {}就行,不用在多此一举

思路:当vector的空间不够就行扩容时,注意在扩容中是要进行深拷贝的:

new一块新空间,将原来的数据拷贝到新空间中,最后释放旧空间:

        void reserve(size_t n)
		{
			if (n > capacity())
			{
				T* tmp = new T[n];
				
				memcpy(tmp, _start, size() * sizeof(T));
				delete[] _start;

				_start = tmp;
				_finish = tmp + size();
				_endofstorage = tmp + n;
			}
		}

但这里有个细节问题:在给新的空间的_finish初始化时,所用的size()是通过被释放空间的成员_finsh-_start来计算得到的:_start已经指向新空间,计算出来的size()必定是错误的!

解决:在释放之前保留size()值就行:

void reserve(size_t n)
{
	if (n > capacity())
	{
		T* tmp = new T[n];
		size_t old_size = size();//保存旧的size()值
		memcpy(tmp, _start, size() * sizeof(T));
		delete[] _start;
		_start = tmp;
		_finish = tmp + old_size;
		_end_of_storage = tmp + n;
	}
}

但是,如果我们vector的类型是string呢?

这里也会存在问题:

memcpy一个一个字节拷贝内置类型(char,int)不会有问题,但如果拷贝是string类型时会造成string的浅拷贝

所以我们不能用memcpy拷贝数据,有风险

暴力解决:有多少个数据直接进行赋值(tmp[i]=_start[i]):(系统底层对string进行深拷贝)

void reserve(size_t n)
{
	if (n > capacity())
	{
		T* tmp = new T[n];
		size_t old_size = size();//保存旧的size()值
		//vector<T>不是内置类型(string)会导致浅拷贝
		//memcpy(tmp, _start, size() * sizeof(T));
		//解决:直接赋值
		for (int i = 0; i < old_size; i++)
		{
			tmp[i] = _start[i];
		}
		
		delete[] _start;
		_start = tmp;
		_finish = tmp + old_size;//tmp+新的size() = _finish-_start(更新了)
		_end_of_storage = tmp + n;
	}
}

2push_back

判断空间是否足够后进行对_finish的赋值:

void push_back(const T& val)
{
	if (_finish == _end_of_storage)
	{
		reserve(capacity() == 0 ? 4 : capacity() * 2);
	}

	*_finish = val;
	++_finish;
}

3insert

在pos位置前插入val元素与string的insert类型:要对pos后的数据进行挪动;

在移动之前还要考虑空间问题,如果空间不够要进行扩容,

扩容后还要对pos进行更新!:如果没有更新,pos还在旧空间的上导致结果错误:

void insert(iterator pos, const T& val)
{
	assert(pos >= _start);
	assert(pos <= _finish);
	/*if (_finish == _end_of_storage)
	{
		reserve(size() + 1);
	}*/
	if (_finish == _end_of_storage)
	{
		size_t len = pos - _start;//老的pos位置
		reserve(capacity() == 0 ? 4 : capacity() * 2);
		//扩容后更新pos的位置(迭代器失效)
		pos = _start + len;
	}

	iterator it = end()-1;
	while (it >= pos)
	{
		*(it+1) = *(it);
		it--;
	}
	//_start[pos - _start] = val;
	*pos = val;
	_finish++;
}

4erase

思路:在pos位置删除就仅需将pos后的数组往前覆盖,最后--finish。这里可用迭代器来进行遍历

void erase(iterator pos)
{
	assert(pos >= _start);
	assert(pos < _finish);
	while (pos < _finish)
	{
		*pos = *(pos + 1);
		pos++;
	}
	_finish--;
}


5resize

思路:实现resize有两种情况:1n>size():空间不够进行扩容,扩容后后进行n-size()个val值的插入;2n<size();进行删除元素后使数组元素不超过n:

void resize(size_t n, const T& val = T()) //T()匿名对象的构造,内置类型进行升级
{
	//只要大于原来的个数直接扩容
	if (n > size())
	{
		reserve(n);
		while (_finish < _start + n)
		{
			*(_finish++) = val;
		}
	}
	else
	{
		//删除
		_finish = _start + n;
	}
}

6迭代器的函数模板

实现迭代器的函数模板为了能够支持:vector类型与其它类型的数据能够进行兼容(插入)

函数模板可以是类模板的成员函数!

只需传入数据的begin()与end(),在实现的函数里去遍历它就行了:

//支持vector的类型是各种容器
template<class InputIterator>
vector(InputIterator fist, InputIterator end)
{
	while (fist != end)
	{
		push_back(*fist);
		++fist;
	}
}

7拷贝构造

与string类似:有传统写法(自己做)与现代写法。但这个现代写法有些不同:直接开好空间对拷贝对象就行范围for遍历一遍就实现;

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

//现代写法
vector(const vector<T>& v)
{
	reserve(v.capacity());
	
	for (auto e : v)
	{
		push_back(e);
	}
}

但这个现代写法有个问题:如果是string类型的对象会进行浅拷贝!

所以要对auto进行引用进行深拷贝

库里面还有另一个拷贝构造函数:n个val值的构造;实现思路与上面类似

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

	for (size_t i = 0; i < n; i++)
	{
		push_back(val);
	}
}

实现出来后,如果我们要进行使用:

出现了报错,这么回事?要使用的明明是n个val值的构造,怎么变成了去其它位置找了?

因为有两个成员函数在编译器看来都很匹配,而构造函数的n的类型是size_t,编译器觉得要进行类型转换麻烦,就选择其它一个而不选拷贝构造

解决:把n类型换成int可以解决,但设计vector则是将它进行函数重载来解决:

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

	for (size_t i = 0; i < n; i++)
	{
		push_back(val);
	}
}

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

	for (size_t i = 0; i < n; i++)
	{
		push_back(val);
	}
}

四迭代器失效

场景1

void test()
{
	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);
	print(v);

	vector<int>::iterator it = v.begin() + 3;
	
	v.insert(it, 40);
	print(v);
	cout << *it << endl;

}

当我在vector中插入8个int元素后,再在这个数组的第四个位置插入值。

最后我想使用迭代器时打印出来会是随机值

原因:在进行insert插入时空间不够会进行扩容,导致it(pos)还在原来的空间上没有进行更新

但是你或许要问了,我们之前在实现insert的时候不是有对pos位置进行更新吗?

但是我们是进行传值调用的:形参的改变不影响实参!

要解决加引用?不行!加引用就不能传v.begin()这样的类型了

设计者在这方面也没想要解决问题,而是来告诉我们:“坏了的苹果不能吃”的道理

场景2

void test5()
{
	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>::iterator it = v1.begin();
	while (it != v1.end())
	{
		if (*it % 2 == 0)
		{
			v1.erase(it);
		}
		it++;
	}
	print(v1);
}
void test6()
{
	vector<int> v2;
	v2.push_back(1);
	v2.push_back(2);
	v2.push_back(3);
	v2.push_back(4);
	v2.push_back(4);
	v2.push_back(5);

	vector<int>::iterator it = v2.begin();
	while (it != v2.end())
	{
		if (*it % 2 == 0)
		{
			v2.erase(it);
		}
		it++;
	}
	print(v2);
}
void test7()
{
	vector<int> v2;
	v2.push_back(1);
	v2.push_back(2);
	v2.push_back(3);
	v2.push_back(4);
	v2.push_back(5);
	v2.push_back(4);

	vector<int>::iterator it = v2.begin();
	while (it != v2.end())
	{
		if (*it % 2 == 0)
		{
			v2.erase(it);
		}
		it++;
	}
	for (auto e : v2)
	{
		cout << e << ' ';
	}
	cout << endl;
}

分别有三组值:实现删除元素是偶数:

第一组:

 

第二组:

第三组:

第一组答案是正确的;第二组有点小问题;第三组直接出现错误。怎么回事?

分析

先说结论:迭代器的失效所造成的

第一组:it在遍历的过程中,走到末尾刚好走到元素的后面,与end()相等,循环结束:

第二组:it走到4的位置时删除4后,后面的数挪动前面:即删除4后,另一个4就到了it的位置,而此时的it要往后++;所以要解决是否就是在it++作判断?

第三组:it走到了end()的后面,错过了判断条件,越界了

关于在第二组中提出的加判断,我们来看看好不好解决: 

	void test5()
	{
		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>::iterator it = v1.begin();
		while (it != v1.end())
		{
			if (*it % 2 == 0)
			{
				v1.erase(it);
			}
			else
			{
				it++;
			}
		}
		print(v1);
	}
	void test6()
	{
		vector<int> v2;
		v2.push_back(1);
		v2.push_back(2);
		v2.push_back(3);
		v2.push_back(4);
		v2.push_back(4);
		v2.push_back(5);

		vector<int>::iterator it = v2.begin();
		while (it != v2.end())
		{
			if (*it % 2 == 0)
			{
				v2.erase(it);
			}
			else
			{
				it++;
			}
		}
		print(v2);
	}
	void test7()
	{
		vector<int> v2;
		v2.push_back(1);
		v2.push_back(2);
		v2.push_back(3);
		v2.push_back(4);
		v2.push_back(5);
		v2.push_back(4);

		vector<int>::iterator it = v2.begin();
		while (it != v2.end())
		{
			if (*it % 2 == 0)
			{
				v2.erase(it);
			}
			else
			{
				it++;
			}
		}
		for (auto e : v2)
		{
			cout << e << ' ';
		}
		cout << endl;
	}

 

观察后得出:可以解决问题。

但我们要知道:我们实现出来的erase与库里的erase是有区别的:我们实现出来的相当于是Linux中g++的实现思路(不会缩容);

而C++库里的erase是会进行进行缩容的;那么C++要如何解决问题呢?

erase会返回一个值:在新空间(已经进行移到后的新数组)中要删除元素的位置(相对位置)

void test7()
{
	std::vector<int> v2;
	v2.push_back(1);
	v2.push_back(2);
	v2.push_back(3);
	v2.push_back(4);
	v2.push_back(5);
	v2.push_back(4);

	std::vector<int>::iterator it = v2.begin();
	while (it != v2.end())
	{
		if (*it % 2 == 0)
		{
			it=v2.erase(it);
		}
		else
		{
			it++;
		}
	}
	for (auto e : v2)
	{
		cout << e << ' ';
	}
	cout << endl;
}

这样,代码就能够实现在所有的平台上都能跑通了!

最后

这便是我们在学习vector上的总结,有错误欢迎指正!

  • 7
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值