Vector底层实现原理


在这里插入图片描述

实现原理:底层实现了一个动态数组

类构成

class vector:protected Vector_base;

Vector_base

三个指针
_M_Star:容器开始的位置
_M_finsh:容器结束的位置
_M_end_of_storage:动态内存最后一个元素的下一个位置

构造函数

无参构造:不会分配动态内存(当插入的时候才会分配内存);效率高;
初始化元素个数的构造:会预先分配动态内存;避免多次申请动态内存,影响性能;最好一次性申请足够的动态内存,避免多次分配内存的动作;
插入元素:
1、插入到最后:先检查空间是否需要分配内存,是否需要翻倍;
2、插入非最后位置:检查空间是否需要分配内存,是否需要翻倍;将待插入位置之后的元素往后平移一位,然后插入;
频繁申请内存非常损耗性能;
删除元素:
1、删除最后的元素:直接删除,_M_finsh向前移动一位;删除元素不会释放现有的已经申请的内存;_M_finsh向前移动一位
2、删除非最后的元素:将所有待删除后的元素整体向前平移一位;也不会释放内存;_M_finsh向前移动一位
读取元素:
操作符方式[]
at方式:比操作符方式多了一个检查越界的操作,两种方式都是返回具体元素的引用;
**修改元素:**vector不支持直接修改某个未知的元素;
1、通过读取元素,获得引用,然后修改引用的值;
2、先删除后插入;这种方式效率低
**释放空间:**确保一个vecto的空间为0;
使用无参构造的空间为0;
swap传入一个空容器的方式;空间得到释放
shrink_to_fit:C++11新特性(匹配当前对应的空间,先使用clean清楚元素,在调用shrink就可以释放空间):释放未使用的内存;就是让_M_end_of_storage复制给_M_finsh,把这两者之间的内存都释放掉;

https://blog.csdn.net/weixin_52259848/article/details/126634343

vector实现原理

arrary是静态空间,配置好了就不可改变,如果需要更大的空间,只能全盘拷贝到另一个另一个array中;
vector是动态空间,随着元素的加入,它的内部机制会自动扩容以容纳新的元素。vector的关键技术在于对大小的控制以及重新分配时的数据移动效率。

vector采用的数据结构是线性的连续空间;它的两个迭代器start和finash分别指向配置得来的连续空间中目前已经被使用的空间。迭代器end_of_storage指向整个连续的尾部;
vector在增加元素时,如果超过自身最大的容量,vector则将自身的容量扩容为原来的两倍。扩充步骤:重新配置空间,元素移动,释放旧的内存空间。一但vector空间重新配置,则指向原来vector的所有迭代器都失效了,因为vector的地址改变了;

vector的使用

vector支持随机访问

vector底层是连续空间,并且vector重载了[]下标运算符,用户可以向使用数组的方式访问vector中的每一个元素,即支持随机访问,但vector不适宜做任意位置的插入和删除操作,因为要进行大量元素的搬移,比如插入:
`reference operator[](size_type n)
{
return *(begin() + n);
}
const_reference operator[](size_type n) const
{
return *(begin() + n);
}
vector不适合做任意位置插入以及删除操作
vector不适宜做任意位置插入与删除操作,因为插入和删除时需要搬移大量元素:最差情况下时间复杂度为O(N);

`

vector使用注意事项

  1. **在不确定的情况下使用at而不是operator[]。**实际上at函数就是调用的operator[]函数,只是多了一个检查是否越界的动作,而operator[]函数是直接跳转位置访问元素,所以速度是很快的,从时间复杂度看,是O(1)。
  2. 什么类型不可以作为vector的模板类型:对于vector模板特化类型,因为在vector的实现过程中,变量会经常被拷贝或者赋值,所以vector的模板类型应该具有公有的拷贝构造函数和重载的赋值操作符函数。
  3. 什么情况下vector的迭代器会失效:
    第一是在vector容器中间根据指定迭代器删除元素,也就是调用erase函数,此时因为当前位置会被后面的元素覆盖,所以该指定迭代器会失效,不过此时可以通过erase的返回值重新得到当前位置的正确迭代器;
    第二是在vector需重新申请内存的时候,比如扩容,比如释放未使用的内存等等这些过程中都会发生迭代器失效的问题,因为内存有了变动,此时就需要重新获得迭代器;
    有人说往vector中间插入数据也会使迭代器失效,实际上根据源码是不会的,看上面的insert实现可以得知,再插入完成以后给当前迭代器重新赋值了的。
    4、vector怎么迅速的释放内存
    有人说是不是可以调用reserve(0)来进行释放,毕竟reserve函数会根据我们指定的大小重新申请的内存,那是行不通的哈,这个函数只有在传入大小比原有内存大时才会有动作,否则不进行任何动作。
    至于通过resize或者clear等都是行不通的,这些函数都只会对当前已保存在容器中的所有元素进行析构,但对容器本身所在的内存空间是不会进行释放的。
    4.1 通过swap函数
    这时我们可以想想,什么情况下vector大小为0呢,就是作为一个空容器的时候,所以要想快速的释放内存,我们可以参考swap函数机制,用一个空的vector与当前vector进行交换,使用形如vector().swap(v)这样的代码,将v这个vector变量所代表的内存空间与一个空vector进行交换,这样v的内存空间等于被释放掉了,而这个空vector因为是一个临时变量,它在这行代码结束以后,会自动调用vector的析构函数释放动态内存空间,这样,一个vector的动态内存就被迅速的释放掉了。
    4.2使用shrink_to_fit函数
    在c++11以后增加了这个函数,它的作用是释放掉未使用的内存,假设我们先调用clear函数把所有元素清掉,这样是不是整块容器都变成未使用了,然后再调用shrink_to_fit函数把未使用的部分内存释放掉,那不就把这个vector内存释放掉了吗。

vector扩容

vector扩大容量的本质:当 vector 的大小和容量相等(size==capacity)也就是满载时,如果再向其添加元素,那么 vector 就需要扩容。vector 容器扩容的过程需要经历以下 4 步:

1.完全弃用现有的内存空间,重新申请更大的内存空间;
2.将旧内存空间中的数据,按原有顺序移动到新的内存空间中;
3.将旧的内存空间释放。
4.使用新开辟的空间
下面从源码下,看下vector的扩容机制:

`// SGI-STL扩容机制
void reserve(size_type n) {
// 当n大于当前vector的容量时才会扩容,小于等于当前容量则忽略本次操作
if (capacity() < n) {
const size_type old_size = size();
// 使用空间配置器开辟n个新空间,并将旧空间元素拷贝到新空间
iterator tmp = allocate_and_copy(n, start, finish);
// 释放旧空间
// a. 先调用析构函数,将[start, finish)区间总所有的对象析构完整
destroy(start, finish);
// b. 将空间规划给空间配置器
deallocate();
// 3. 接收新空间并更新其成员
start = tmp;
finish = tmp + old_size;
end_of_storage = start + n;
}
}

`

注意:扩容的实际为,在插入时,当finish与start在同一个位置,或者调用reserve操作,或者resize操作等都可能引起扩容。
由上扩容机制也就解释了,为什么 vector 容器在进行扩容后,与其相关的指针、引用以及迭代器可能会失效的原因。并且vector 扩容是非常耗时的。为了降低再次分配内存空间时的成本,每次扩容时 vector 都会申请比用户需求量更多的内存空间(这也就是 vector 容量的由来,即 capacity>=size),以便后期使用。
根据以上,其实如果我们能确定vector必定会被使用且有数据时,我们应该在声明的时候指定元素个数,避免最开始的时候多次申请动态内存消耗资源,进而影响性能。

vector扩容倍数选择

要解释为什么要两倍扩容(gcc)我们可以分两步理解?

在这里插入图片描述

为什么选择1.5倍扩容或者2倍扩容

在这里插入图片描述

如何释放内存

vector的内存空间只增不减,vector内存的回收只能靠vector调用析构函数的时候才被系统收回,当然也可以使用swap来帮你释放内存,具体方法:
vec.swap(vec);

常用接口

为了能使算法操作容器中的元素,每个容器都提供了begin()和end()的迭代器,[begin, end)区间中包含了容器中的所有元素,vector也是如此。

iterator begin() { return start; }
iterator end() { return finish; }

size()和capacity()分别表示vector中的元素个数以及底层的空间大小。

size_type size() const { 
    return size_type(end() - begin()); // finish - start
}

size_type capacity() const { 
    return size_type(end_of_storage - begin()); // end_of_storage - start
}


但是在实际工程中,用vector存储数据时,切忌**,vector不是存储bool类型元素的vector容器!**
不推荐使用 vector 的原因有以下 2 个:
1.严格意义上讲,vector 并不是一个 STL 容器;
2.vector 底层存储的并不是 bool 类型值。

vector不是容器

对于是否为 STL 容器,C++ 标准库中有明确的判断条件,其中一个条件是:如果 cont 是包含对象 T 的 STL 容器,且该容器中重载了 [ ] 运算符(即支持 operator[]),则以下代码必须能够被编译:

T *p = &cont[0];

此行代码的含义是,借助 operator[ ] 获取一个 cont 容器中存储的 T 对象,同时将这个对象的地址赋予给一个 T 类型的指针。
这就意味着,如果 vector 是一个 STL 容器,则下面这段代码是可以通过编译的:

//创建一个 vector<bool> 容器
vector<bool>cont{0,1};
//试图将指针 p 指向 cont 容器中第一个元素
bool *p = &cont[0];

但此段代码不能通过编译。原因在于 vector 底层采用了独特的存储机制。
实际上,为了节省空间,vector 底层在存储各个 bool 类型值时,每个 bool 值都只使用一个比特位(二进制位)来存储。 也就是说在 vector 底层,一个字节可以存储 8 个 bool 类型值。在这种存储机制的影响下,operator[ ] 势必就需要返回一个指向单个比特位的引用,但显然这样的引用是不存在的。
同样对于指针来说,其指向的最小单位是字节,无法另其指向单个比特位。综上所述可以得出一个结论,即上面第 2 行代码中,用 = 赋值号连接 bool *p 和 &cont[0] 是矛盾的。

如何避免使用vector

可以选择使用 deque 或者 bitset 来替代 vector<bool

deque 容器几乎具有 vecotr 容器全部的功能(拥有的成员方法也仅差 reserve() 和 capacity()),而且更重要的是,deque 容器可以正常存储 bool 类型元素。

还可以考虑用 bitset 代替 vector,其本质是一个模板类,可以看做是一种类似数组的存储结构。和后者一样,bitset 只能用来存储 bool 类型值,且底层存储机制也采用的是用一个比特位来存储一个 bool 值。

和 vector 容器不同的是,bitset 的大小在一开始就确定了,因此不支持插入和删除元素;另外 bitset 不是容器,所以不支持使用迭代器。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值