vector类(一)
vector的介绍
vector是表示可变大小数组的序列容器。就像数组一样,vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问,和数组一样高效。但是又不像数组,它的大小是可以动态改变的,而且它的大小会被容器自动处理,有点像顺序表。
当vector需要重新分配大小时,其做法是,分配一个新的数组,然后将全部元素移到这个数组当中,并释放原来的数组空间。
vector分配空间策略:vector会分配一些额外的空间以适应可能的增长,因此存储空间比实际需要的存储空间一般更大。
与其他动态序列容器相比(deque, list and forward_list),vector 的访问元素更加高效,在其末尾添加和删除元素的时候效率很高,但是对于不在末尾的操作,效率就很低。
vector的定义
有四大类构造函数:
(constructor)构造函数声明 | 接口说明 |
vector() | 无参构造 |
vector(size_type n, const value_type& val = value_type()) | 构造并初始化n个val |
vector (const vector& x); | 拷贝构造 |
vector (InputIterator first, InputIterator last); | 使用迭代器进行初始化构造 |
例1:构造一个某类型的空容器。
vector<int> v1; //构造int类型的空容器
例2:构造一个含有n个val的某类型容器。
vector<int> v2(10, 2); //构造含有10个2的int类型容器
例3: 拷贝构造某类型容器的复制品。
vector<int> v3(v2); //拷贝构造int类型的v2容器的复制品
例4:使用迭代器拷贝构造某一段内容。
vector<int> v4(v2.begin(), v2.end()); //使用迭代器拷贝构造v2容器的某一段内容
注意:该方式也可用于拷贝其他容器的某一段内容。
string s("hello world");
vector<char> v5(s.begin(), s.end()); //拷贝构造string对象的某一段内容
vector iterator 的使用:
vector 当中的迭代器和string 当中迭代器的时候使用一样的,主要使用的有以下几种:
begin + end | 获取第一个数据位置的iterator/const_iterator, 获取最后一个数据的下一个位置 的iterator/const_iterator |
rbegin + rend | 获取最后一个数据位置的reverse_iterator,获取第一个数据前一个位置的 reverse_iterator |
还是和string一样,有正向和反向的迭代器。
关于 vector当中迭代器的使用可以参考string当中迭代器的使用。
vector容量空间
接口也是string一样的:
容量空间 | 接口说明 |
size | 获取有效个数 |
capacity | 获取最大容量 |
empty | 判断是否为空 |
resize | 改变vector的size,初始化 |
reserve | 改变vector的capacity |
复习:
reserve:
- 当所给值大于容器当前的capacity时,将capacity扩大到该值。
- 当所给值小于容器当前的capacity时,什么也不做。
resize:
- 当所给值大于容器当前的size时,将size扩大到该值,扩大的元素为第二个所给值,若未给出,则默认为0。
- 当所给值小于容器当前的size时,将size缩小到该值。
注意点:
- capacity的代码在vs和g++下分别运行会发现,vs下capacity是按1.5倍增长的,g++是按2倍增长的。这个问题经常会考察,不要固化的认为,vector增容都是2倍,具体增长多少是根据具体的需求定义的。vs是PJ版本STL,g++是SGI版本STL。
- reserve只负责开辟空间。如果确定知道需要用多少空间,reserve可以缓解vector增容的代价缺陷问题。
- resize在开空间的同时还会进行初始化,影响size。
vs下测试扩容:
void TestVectorExpand()
{size_t sz;vector<int> v;sz = v.capacity();cout << "making v grow:\n";for (int i = 0; i < 100; ++i){v.push_back(i);if (sz != v.capacity()){sz = v.capacity();cout << "capacity changed: " << sz << '\n';}}
}
VS下输出:
我们还要注意,不要误用reserve(),例如如下代码:
vector<int> v1;v1.reserve(10);for (size_t i = 0; i < 10; i++){v1[i] = i;}
运行报错,断言:
reserve() 修改的是 _capacity容量,并没有修改有效字符长度,上述代码的有效字符长度还是0。类似于你可以在银行存10万(代码原来的容量),但是你存款0元(有效字符长度为0),后来你可以在银行存100万(reserve修改更大容量),但是你用不了钱啊,你用钱是犯法的(调用v1[i] = i 是不允许的),因为你的存款一直没变啊,一直都是0(有效字符长度为0),你要把你的存款提上去才能用啊。这时候推荐使用resize() ,因为resize() 也会进行扩容,而且会修改 _size。
vector 增删查改
vector增删查改 | 接口说明 |
push_back | 尾插 |
pop_back | 尾删 |
find | 查找 (注意这个是算法模块实现,不是vector的成员接口) |
insert | 在position之前插入val |
erase | 删除position位置的数据 |
swap | 交换两个vector的数据空间 |
operator[] | 像数组一样访问 |
我们注意到,修改当中,vector并没有提供头插头删,因为头插和头删是需要挪动数据的,这样的话效率不高,不是vector的适用范围。但是也是可以头插头删的,使用 insert() 和erase() 函数就可以了(如下例子)。
int a[] = { 16,86,64,42 };vector<int> v1(a, a + 4);for (auto val : v1){cout << val << " ";}//16 86 64 42v1.erase(v1.begin());for (auto val : v1){cout << val << " ";}//86 64 42v1.insert(v1.begin(), 1000);for (auto val : v1){cout << val << " ";}//1000 86 64 42
我们还发现在vector类当中是没有实现 find() 函数的,但是string类当中就实现了find() 函数。其实 find() 函数在STL当中是算法模块(<algorithm>)当中已经实现了模版,我们可以直接使用其中的 find() 函数:
int a[] = { 16,86,64,42 };vector<int> v1(a, a + 4);for (auto val : v1){cout << val << " ";}//16 86 64 42vector<int>::iterator pos = find(v1.begin(), v1.end(), 86);if (pos != v1.end()){v1.erase(pos);}for (auto val : v1){cout << val << " ";}//16 64 42
为什么在string当中就实现了find()函数,而在vector当中就没有实现呢?其实是因为string当中的find() 不仅要求查找字符,还要要求查找字符子串,而vector当中就没有这么多功能,这需要遍历查找即可。
迭代器失效
首先我们来看下面这个代码关键部分,是自己实现的 vector的 insert() 函数:
void insert(iterator pos, const T& x) { //iterator表示地址assert(pos >= _start && pos <= _finish); //插入在合理范围内// 扩容if (_finish == _endOfStorage) //判断扩容{int newcapacity = capacity() == 0 ? 4 : capacity() * 2;reserve(newcapacity);}// 把数据往后挪一位size_t end = finish - 1;while (pos <= end){*(end + 1) = *end; //问题所在end--;}*pos = x;++finish;}
vector 的内部指针
_start
:指向内存块的起始位置_finish
:指向最后一个元素的下一个位置(即当前元素数量)_endOfStorage
:指向内存块的末尾(即当前容量)
上述代码有问题,示例场景:
假设我们有一个包含 3 个元素的 vector,容量为 3:
步骤1,插入前的状态:
步骤2,扩容操作:
当 _finish == _endOfStorage
时,触发扩容:
int newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
扩容后:
步骤3,问题代码执行:
size_t end = finish - 1; // end = 0x2008(新内存中C的位置)
while (pos <= end) // 比较 0x1004(旧地址) 和 0x2008(新地址)
{*(end + 1) = *end; // 问题所在end--;
}
比较不同内存块的指针是未定义行为。当 end递减到小于pos时,循环条件pos <= end可能仍然成立,程序会继续尝试访问已释放的内存!
修正代码:
void insert(iterator pos, const T& x){assert(pos >= _start && pos <= _finish);// 扩容if (_finish == _endOfStorage){size_t len = pos - _start; //保存偏移量int newcapacity = capacity() == 0 ? 4 : capacity() * 2;reserve(newcapacity);pos = _start + len; //起点+偏移量 找到pos}// 把数据往后挪一位iterator end = _finish - 1;while (pos <= end){*(end + 1) = *end;end--;}*pos = x;++_finish;}
其实上述还是没有完全解决迭代器失效,上述只是修改了内部的迭代器失效,如果迭代器在外部也是可能会失效的,如下例子:
auto pos = v.begin() + 3; // 获取迭代器v.insert(pos, 100); // 插入元素(可能扩容)*pos += 10; // 危险!迭代器已失效
上述 insert() 函数扩容之后,使用*pos += 10; 是错误的。我们在函数内使用完pos,pos是传值返回,在函数内部的修改不会影响到函数外pos的值。
也不能用 “&”传参,因为如果使用的是 begin() 类似的迭代器,begin()返回 iterator的对象,返回的是值,并不是引用,所以在两者传参的时候会产生临时对象,临时对象具有常性,这里就涉及到权限的放大,就会编译报错。
外部的迭代器失效问题 insert() 函数无法解决,所以在使用 insert()函数的时候要格外注意。
所以,我们为pos提供一个返回值,来解决外部的迭代器失效问题。
最终函数:
iterator insert(iterator pos, const T& x){assert(pos >= _start && pos <= _finish);if (_finish == _endofstorage){size_t len = pos - _start;size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;reserve(newcapacity);// 解决pos迭代器失效问题pos = _start + len;}iterator end = _finish - 1;while (end >= pos){*(end + 1) = *end;--end;}*pos = x;++_finish;return pos;}