目录
如果你在前面有先学习完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上的总结,有错误欢迎指正!