本文已参与「新人创作礼」活动,一起开启掘金创作之路。
特性
vector
是表示可变大小数组的序列容器。- 就像数组一样,
vector
也采用的连续存储空间来存储元素,说明可以采用下标对vector
的元素进行访问,和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理。 - 本质上
vector
使用动态分配数组来存储它的元素。当新元素插入时候,这个数组需要被重新分配大小为了增加存储空间。其做法是,分配一个新的数组,然后将全部元素移到这个数组。就时间而言,这是一个相对代价高的任务,因为每当一个新的元素加入到容器的时候,vector
并不会每次都重新分配大小。 vector
分配空间策略:vector
会分配一些额外的空间以适应可能的增长,因为存储空间比实际需要的存储空间更大。不同的库采用不同的策略权衡空间的使用和重新分配。但是无论如何,重新分配都应该是对数增长的间隔大小,以至于在末尾插入一个元素的时候是在常数时间的复杂度完成的。- 因此,
vector
占用了更多的存储空间,为了获得管理存储空间的能力,并且以一种有效的方式动态增长。 - 与其它动态序列容器相比(
deques
,list
和forward_lists
),vector
在访问元素的时候更加高效,在末尾添加和删除元素相对高效。对于其它不在末尾的删除和插入操作,效率更低。比起list
和forward_list
统一的迭代器和引用更好。
迭代器失效问题
vector
容器的迭代器:
- 那么什么是
vector
容器的失效问题? - 是指对
vector
进行插入 / 删除(insert
/erase
)导致的迭代器指针非法现象。
代码演示:
int main(){
int a[] = { 1, 2, 3, 4 };
vector<int> v(a, a + sizeof(a) / sizeof(int));
// 使用find查找3所在位置的iterator
vector<int>::iterator pos = find(v.begin(), v.end(), 3);
// 删除pos位置的数据,导致pos迭代器失效。
v.erase(pos); //现在的pos位置是一个失效位置
cout << *pos << endl; // 此处会导致非法访问
// 在pos位置插入数据,导致pos迭代器失效。
// insert会导致迭代器失效,是因为insert可能会导致增容,增容后pos还指向原来的空间,而原来的空间已经释放了。
pos = find(v.begin(), v.end(), 3);
v.insert(pos, 30);
cout << *pos << endl; // 此处会导致非法访问
return 0;
}
常见的迭代器失效的场景(删除vector
中全部偶数):
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
int main(){
int a[] = { 1, 2, 3, 4 };
vector<int> v(a, a + sizeof(a) / sizeof(int));
// 实现删除v中的所有偶数
// 下面的程序会崩溃掉,如果是偶数,erase导致it失效
// 对失效的迭代器进行++it,会导致程序崩溃
vector<int>::iterator it = v.begin();
while (it != v.end()){
if (*it % 2 == 0)
v.erase(it);
++it;
}
// 以上程序要改成下面这样,erase会返回删除位置的下一个位置
vector<int>::iterator it = v.begin();
while (it != v.end()){
if (*it % 2 == 0)
it = v.erase(it); //使用迭代器 重新接收~
else
++it;
}
return 0;
}
源码剖析
其实vector
底层维护了三个成员参数,类型都是迭代器(原生指针):
start
:开始位置finish
:实际结束位置end_of_storage
:容量结束位置
end_of_storage
与finish
的差值就是备用位置。
差值如果为0
了,说明二者相等,就要进行扩容操作了,这会涉及重新配置空间、数据拷贝、释放旧空间等一系列动作,将会付出额外代价进行数据搬运。
vector
源码:【github.com/giturtle/Cp…】
模拟实现
实现一个Vector
类,模拟vector
容器的功能与特性。
#include <assert.h>
template<class T>
class Vector{
public:
typedef T* iterator;
typedef const T* const_iterator;
Vector()
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{}
//拷贝构造
// 老式写法:
//Vector(const Vector<T>& v)
//{
// _start = new T[v.Capacity()];
// memcpy(_start, v._start, v.Size()*sizeof(T));
// _finish = _start + v.Size();
// _endofstorage = _start + v.Capacity();
//}
// 新式写法:
Vector(const Vector<T>& v)
:_start(nullptr)
, _finish(nullptr)
, _endofstorage(nullptr)
{
Reserve(v.Size());
for (size_t i = 0; i < v.Size(); ++i){
this->PushBack(v[i]); //逐个丢进去~
}
}
// v1 = v2
Vector<T>& operator=(Vector<T> v){
this->Swap(v);
return *this;
}
void Swap(Vector<T>& v){ //只交换三个成员变量,代价远低于使用算法交换对象
std::swap(_start, v._start);
std::swap(_finish, v._finish);
std::swap(_endofstorage, v._endofstorage);
}
~Vector(){
if (_start){
delete[] _start;
_start = _finish = _endofstorage = nullptr;
}
}
iterator begin(){
return _start;
}
iterator end(){
return _finish;
}
const_iterator begin() const{
return _start;
}
const_iterator end() const{
return _finish;
}
void Reserve(size_t n){
if (n > Capacity()){
size_t size = Size();
// 开新空间
T* newarray = new T[n];
if (_start)
memcpy(newarray, _start, sizeof(T)*Size());
// T的 operator=
/* for (size_t i = 0; i < size; ++i){
newarray[i] = _start[i];
}*/
// 释放旧空间
delete[] _start;
// 赋值
_start = newarray;
_finish = _start + size;
_endofstorage = _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 PushBack(const T& x){
if (_finish == _endofstorage){
//我们选择2倍增长,其实不是硬性规定
//VS下是1.5倍增长,CentOS下是2倍增长
size_t newcapacity = Capacity() == 0 ? 4 : Capacity() * 2;
Reserve(newcapacity);
}
*_finish = x;
++_finish;
}
void PopBack(){
assert(_finish > _start);
--_finish; //标记为无效数据即可~
}
void Insert(iterator pos, const T& x){
assert(pos < _finish);
if (_finish == _endofstorage){
size_t n = pos - _start;
size_t newcapacity = Capacity() == 0 ? 4 : Capacity() * 2;
Reserve(newcapacity);
pos = _start + n;
}
iterator end = _finish-1;
while (end >= pos){
*(end + 1) = *end; //数据搬运,从后往前
--end;
}
*pos = x;
++_finish;
}
iterator Erase(iterator pos){
assert(pos < _finish);
iterator it = pos;
while (it < _finish - 1){
*it = *(it + 1); //数据搬运:覆盖
++it;
}
--_finish;
return pos;
}
size_t Size() const{
return _finish - _start;
}
size_t Capacity() const{
return _endofstorage - _start;
}
T& operator[](size_t pos){
assert(pos < Size());
return _start[pos];
}
const T& operator[](size_t pos) const{
assert(pos < Size());
return _start[pos];
}
private:
iterator _start;
iterator _finish;
iterator _endofstorage;
};
vector与list的对比
vector | list | |
---|---|---|
底层结构 | 动态顺序表,一段连续空间 | 带头结点的双向循环链表 |
随机访问 | 支持随机访问,访问某个元素效率O(1) | 不支持随机访问,访问某个元素效率O(N) |
插入和删除 | 任意位置插入和删除效率低,需要搬移元素,时间复杂度为O(N) ,插入时有可能需要增容,(增容:开辟新空间,拷贝元素,释放旧空间,导致效率更低) | 任意位置插入和删除效率高,不需要搬移元素,时间复杂度为O(1) |
空间利用率 | 底层为连续空间,不容易造成内存碎片,空间利用率高,缓存利用率高 | 底层节点动态开辟,小节点容易造成内存碎片,空间利用率低,缓存利用率低 |
迭代器 | 原生态指针 | 对原生态指针(节点指针)进行封装 |
迭代器失效 | 在插入元素时,要给所有的迭代器重新赋值,因为插入元素有可能会导致重新扩容,致使原来迭代器失效,删除时,当前迭代器需要重新赋值否则会失效 | 插入元素不会导致迭代器失效,删除元素时,只会导致当前迭代器失效,其他迭代器不受影响 |
使用场景 | 需要高效存储,支持随机访问,不关心插入删除效率 | 大量插入和删除操作,不关心随机访问 |