STL vector扩容
vector容器
vector被称为向量容器,该容器擅长在尾部插入或删除元素,时间复杂度为O(1);而对于在vector容器头部或者中部插入或删除元素,则花费时间要长一些(移动元素需要耗费时间),时间复杂度为线性阶O(n)。使用vector容器,需要包含<vector>
,注意这里无扩展名。
STL中规定容器的区间遵循前闭后开原则,即容器中的一对迭代器start和finish标识的前闭后开区间,从start开始,直到finish-1.迭代器finish所指的是“最后一个元素的下一位置”。这样定义的好处主要有两点:
-
为“遍历元素时,循环的结束时机”提供一个简单的判断依据。只要尚未到达end(),循环就可以继续下去;
-
不必对空区间采取特殊处理手段。空区间的begin()就等于end();
class vector {
……
public:
/**
* begin()函数源码
* 用于返回start迭代器
**/
iterator begin() {
return start;
}
/**
* end()函数源码
* 用于返回finish迭代器
**/
iterator end() {
// STL容器的前闭后开原则,end迭代器并不指向最后一个元素 而是最后一个元素的后面
return finish;
}
/**
* size()函数源码
* 用于返回start-end的值——已保存的元素个数
**/
size_type size() const { // 返回start-end的值 即已保存的元素个数
return size_type(end() - begin());
}
/**
* capacity()函数源码
* 返回end_of_storage - start的值——vector能够保存元素上限
**/
size_type capacity() const { // 返回end_of_storage - start的值, 即vector能够保存元素上限
return size_type(end_of_storage - begin());
}
/**
* empty()函数源码
* 通过判断start迭代器与finish迭代器是否相等,返回vector是否为空
**/
bool empty() const {
return begin() == end();
}
/**
* []操作符重载
* 实现根据下标取元素
**/
reference operator[](size_type n) {
return *(begin() + n);
}
/**
* front函数源码
* 根据start迭代器,返回第一个元素的值
**/
reference front() {
return *begin();
}
/**
* back函数源码
* 根据finish迭代器,返回最后一个元素的值
* 由于finish迭代器指向的是最后一个元素的后一个位置(STL容器的前闭后开原则),因此这里需要用finish-1获取最后一个元素的位置
**/
reference back() { // 返回最后一个要素
return *(end() - 1);
}
};
扩容策略
vector属于序列式容器(sequence_container):其中的元素都可序,但未必有序。
如图所示,vector的内存模型与数组较为相似,但vector的内存模型是动态增长的线性空间,动态增长的实质是:需求空间超过当前可用空间后,不是在原空间之后接续新空间。这是因为线性空间后不一定有足够大小的空间,因此重新申请一块更大的空间来作为载体,然后复制已有数据到新申请的内存空间。
- start:表示目前使用空间的头。
- finish:目前使用空间的尾。
- end_of_storage:目前可用空间的尾。
注意,目前使用空间和目前可用空间是有区别的
具体操作为:首先配置一块新空间,然后将元素从原空间搬到新的空间上,再把原空间释放。(涉及到了新空间的配置和旧空间的释放)
新空间的大小为一般为原空间大小的二倍。注意:二倍增长并不是必然的,不同的编译环境可以有不同的实现,但若增长倍数小于2则可能会导致扩容频繁;增长倍数较大则可能导致申请了较大的空间而未使用,从而造成浪费。 此外,vector为了降低空间扩容的速度,在配置空间时留有一部分空间以便之后扩容,这就是size()和capacity()的区别。size()返回使用了多少空间,capacity()返回了配置了多少空间。当两者相等时说明vector容器已经满了,再插入元素需要扩容。
vector如何做到随机访问?‘[]’操作符
vector可以像数组一样,支持使用’[]'操作符根据下标获取元素。简单来讲,vector中元素大小固定,在知道start的基础上,我们只需要在其基础上进行地址偏移就能找到所需元素。
reference operator[](size_type n){
return *(begin() + n);
}
迭代器失效
需要注意的是,对vector的操作如果引起的空间重新分配,那么原vector的所有迭代器就都失效。因此,如果我们将vector的迭代器保存到变量中时,可能会因为空间重新分配导致该变量保存的迭代器失效,解决迭代器失效可以参考:c++迭代器失效的几种情况总结
#include <vector>
#include <iostream>
using namespace std;
int main()
{
vector<int> vec;
vec.push_back(0);
auto iter = vec.begin(); // 使用变量保存迭代器
cout<< *iter << endl;
for(int i = 0; i < 10; i++) // 可能会引起vector空间重新分配的代码段
{
vec.push_back(i);
}
cout<< *iter << endl; // 原迭代器失效,此处异常中断
}
vector常用元素的操作方法
push_back(const T& x)
将新元素插入vector的尾端,在插入时需要关注两种情况:即vector当前是否还有空间,如果有则直接在备用空间上构造元素,调整迭代器finish;若没有,则需要扩充空间。
push_back源码:
void push_back(const T& x) {
if(finish != end_of_storage)
{
constrcut(finish,x);
++finish;
}
else
{
insert_aux(end(),x);
}
}
insert_aux源码:
template<class T>
void insert_aux(iterator position, const T& x) {
if(finish != end_of_storage) {
// 有备用空间
}
else {
// 获取当前vector的元素个数
const size_type old_size = size();
// 计算扩展后的vector空间
// 若扩充前vector是空的,则配置申请一个元素的空间
// 若扩充前vector非空,则按照二倍扩充原则进行扩充
const size_type len = old_size != 0 ? 2 * old_size : 1;
// 分配器allocator配置新空间
// new_start和new_finish迭代器指向新分配的空间起点
iterator new_start = data_allocator::allocate(len);
iterator new_finish = new_start;
try {
// 拷贝原vector中start到position区间的元素到new_start迭代器所指处
// push_back函数中调用insert_aux(end(),x),因此position就是finish迭代器,这里就将原vector的所有元素拷贝过来
// 执行后 new_finish迭代器就指向了原vector的所有元素拷贝到新位置的末尾
new_finish = uninitialized_copy(start,position,new_start);
// 在new_finish处构造新元素x
construct(new_finish,x);
// new_finish向后移一个位置
++new_finish;
// 再将原vector的position到finish区间的元素拷贝到new_finish迭代器所指处
// push_back函数中调用insert_aux(end(),x),因此position就是finish迭代器,这里元素区间是空
new_finish = uninitialized_copy(position,finish,new_finish);
}
catch(...) {
// 异常时回滚
destroy(new_start,new_finish);
data_allocator::dellocate(new_start,len);
throw;
}
// 析构并释放原vector
destroy(begin(),end());
deallocate();
// 为start和finish迭代器赋新值
start = new_start;
finish = new_finish;
end_of_storage = new_start + len;
}
}
pop_back()
删除尾端元素时,调整迭代器并释放即可,不需要空间分配
void pop_back() {
--finish; //调整末端位置迭代器
destroy(finish); //释放尾端元素的资源
}
iterator erase(iterator first,iterator last)
用于删除vector容器中的一段区间的元素,返回指向删除的区间起点迭代器
// 删除某段元素 参数为需要删除段的迭代器起点和终点
iterator erase(iterator first,iterator last) {
iterator i = copy(last, finish, first); //将后面的元素前移
destroy(i,finish); //释放其后元素
finish = finish - (last - first); //调整迭代器
return first;
}
iterator erase(iterator position)
用于删除vector容器中的一个元素,返回指向被删除元素位置迭代器
// 删除某个元素 参数为需要删除的元素迭代器
iterator erase(iterator position) {
if(position + 1 != end()) {
copy(position + 1,finish,position); // 将position后的元素整体向前移动
}
--finish; // 迭代器前移
destroy(finish);
return position;
}
void insert(iterator position, size_type n, const T& x)
在指定位置position前插入n个值为x的元素,返回指向这个元素的迭代器
由于vector中的元素顺序存储,所以随机插入元素的操作相对比较麻烦:
void insert(iterator position, size_type n, const T& x)
{
if(n != 0)
{
if(size_type(end_of_storage - finish) >= n) { // vector当前备用空间大于n时
T x_copy = x;
const size_type elems_after = finish - position; // 计算需要移动元素的个数
iterator old_finish = finish;
if(elems_after > n) { // 如果需要移动的元素个数 大于插入的元素数量
// 将finish-n 到 finish区间内的数据拷贝到finish后
// 注意:这里只拷贝了倒数n个元素 而不是elems_after个元素
uninitialized_copy(finish - n,finish,finish);
finish += n; // 更新finish迭代器,此时finish的位置为插入n个元素后的为位置
// 再将position 到 old_finish区间的元素移动到old_finish后
copy_backward(postion,old_finish - n,old_finsh);
// 最后 在position后填充n个x元素
fill(position,position + n,x_copy);
// 可参考下图情况1
}
else { // 如果需要移动的元素个数 小于插入的元素数量
// 在finish后 填充n - elems_after个元素,
uninitialized_fill_n(finish,n - elems_after, x_copy);
finish += n - elems_after; // 调整finish 此时finish的位置不是最终的位置
// 将position 到 old_finish区间内的元素拷贝到finish后
uninitialized_copy(position,old_finish,finish);
finish += elems_after; // 继续调整finish
fill(position,old_finish,x_copy); // 将position到old_finish区间内填充为 x
// 可参考下图情况2
}
}
else { // vector当前备用空间小于n时
const size_type old_size = size();
const size_type len = old_size + max(old_size,n);
// 配置新空间(同上)
iterator new_start = data_allocator::allocate(len);
iterator new_finish = new_start;
// 分三段复制到新vector
{
//插入点之前的元素复制到新vector
new_finish = uninitialized_copy(start,position,new_start);
//新增元素填入
new_finish = uninitialized_fill_n(new_finish,n,x);
// 插入点之后的元素复制到新vector
new_finish = uninitialized_copy(position,finish,new_finish);
}
// 析构并释放原vector
destroy(begin(),end());
deallocate();
// 调整迭代器
start = new_start;
finish = new_finish;
end_of_storage = new_start + len;
// 可参考下图情况3
}
}
}
情况1:
情况2:
情况3
倍数扩容和等长扩容
向vector中插入元素时,如果元素有效个数size与容量capacity相等时vector内部就会触发扩容机制
扩容机制:开辟新空间,拷贝元素,释放旧空间【频繁扩容会导致效率变低,所以在创建vector数组的时候要预估元素个数】
等长扩容
- vector插入100个元素等长扩容(k=10)需要扩容十次,而且每次都需要将元素从旧空间搬到新空间
倍数扩容
原理:申请新空间、拷贝元素、释放空间
理想情况:在某一次扩容之后可以复用之前的空间
例如二倍扩容时,在第四次扩容(前面已经释放1+2的空间, 第三次空间释放后就有1+2+4空间,因为二倍扩容每次新内存一定大于前面的总和,所以不能实现复用)即如果倍数超过2(包括2),无法使用到已经释放的内存,而且可能空间浪费高(例如扩容申请64,但是只使用了33)所以使用1.5倍扩容(1。2。3。4。1+2+3)几次扩容之后就可以进行空间的复用
- windows(vs)1.5倍扩容
- Linux(gcc)2倍扩容
参考
vector扩容问题源代码剖析_kongkongkkk的博客-CSDN博客
《STL 源码剖析》学习笔记之容器(一)vector_51CTO博客_《STL源码剖析》
3-1 STL容器剖析(vector & list) _牛客网 (nowcoder.com)