std::vector源码分析
vector容器概述
// highlighted block
1. 模板类vector类似于string类,也是一种动态数组。
2. 它可以在运行阶段的设置vector对象的长度,可在末尾附加新数据,还可以在中间插入新数据。
3. 它是使用new创建动态数组的替代品。
实际上vector类确实使用new和delete来管理内存,但这种工作是自动完成的。
一、vector对比array容器
-
数组类是固定大小的序列容器,它们包含以严格线性序列排序的特定数量的元素。数组类具有固定大小,并且不通过分配器管理其元素的分配,它们是封装固定大小元素数组的聚合类型。
-
vector和array均是数组的替代品。
-
vector类的功能比数组强大,使用的是自由存储空间,但付出的代价是效率稍低。
-
如果需要的是长度固定的数组,使用数组是更加的选择,但代价是不那么方便和安全。有鉴于此,C++11新增了模板类array,它也是位于名称空间std中。
-
Array与数组一样,array对象的**(容器大小)长度也是固定的**,也使用栈(静态内存分配),而不是自由存储区,因此其效率与数组相同,但更方便更安全。
二、底层技术实现
1.空间的动态扩展
vector的数据安排以及操作方式,与array非常相似。
两者的唯一区别在于空间的运用的灵活性。
array是静态空间,一旦配置了就不能改变;要换个大(或小)一点的房子,可以,一切琐细都得由客户端自己来:首先配置一块新空间,然后将元素从旧址一一搬往新址,再把原来的空间释还给系统。
vector是动态空间,随着元素的加入,它的内部机制会自行扩充空间以容纳新元素。因此,vector的运用对于内存的合理利用与运用的灵活性有很大的帮助,我们再也不必因为害怕空间不足而一开始要求一个大块头的array了,我们可以安心使用array,吃多少用多少。
vector的实现技术,关键在于其对大小的控制以及重新配置时的数据移动效率。一旦vector的旧有空间满载,如果客户端每新增一个元素,vector的内部只是扩充一个元素的空间,实为不智。因为所谓扩充空间(不论多大),一如稍早所说,是” 配置新空间/数据移动/释还旧空间 “的大工程,时间成本很高,应该加入某种未雨绸缪的考虑。
另外,由于 vector维护的是一个连续线性空间,所以vector支持随机存取 。
vector动态增加大小时,并不是在原空间之后持续新空间(因为无法保证原空间之后尚有可供配置的空间),而是以原大小的两倍另外配置一块较大的空间,然后将原内容拷贝过来,然后才开始在原内容之后构造新元素,并释放原空间。因此, 对vector的任何操作,一旦引起空间重新配置,指向原vector的所有迭代器就都失效了 。这是易犯的一个错误,务需小心。
2.迭代器相关
Iterators迭代器的作用是遍历array数组类和vector中的元素。
可以通过begin/end()、rbegin/rend()、cbegin/cend()、crbegin/crend()等函数进行访问。
3.vector容器的内存自增长
与其他容器不同,其内存空间只会增长,不会减小。先来看看"C++ Primer"中怎么说:为了支持快速的随机访问,vector容器的元素以连续方式存放,每一个元素都紧挨着前一个元素存储。设想一下,当vector添加一个元素时,为了满足连续存放这个特性,都需要重新分配空间、拷贝元素、撤销旧空间,这样性能难以接受。因此STL实现者在对vector进行内存分配时,其实际分配的容量要比当前所需的空间多一些。就是说,vector容器预留了一些额外的存储区,用于存放新添加的元素,这样就不必为每个新元素重新分配整个容器的内存空间。
关于vector的内存空间,有两个函数需要注意:
size()成员指当前拥有的元素个数;
capacity()成员指当前(容器必须分配新存储空间之前)可以存储的元素个数。
reserve()成员可以用来控制容器的预留空间。
vector另外一个特性在于它的内存空间会自增长,每当vector容器不得不分配新的存储空间时,会以加倍当前容量的分配策略实现重新分配。例如,
当前capacity为50,当添加第51个元素时,预留空间不够用了,vector容器会重新分配大小为100的内存空间,作为新连续存储的位置。
4.vector内存释放
由于vector的内存占用空间只增不减,比如你首先分配了10,000个字节,然后erase掉后面9,999个,留下一个有效元素,但是内存占用仍为10,000个。
所有内存空间是在vector析构时候才能被系统回收。
empty()用来检测容器是否为空的,clear()可以清空所有元素。
但是即使clear(),vector所占用的内存空间依然如故,无法保证内存的回收。
如果需要空间动态缩小,可以考虑使用deque。
如果非vector不可,可以用swap()来帮助你释放内存。 具体方法如下:
vector<int>().swap(nums); //或者nums.swap(vector<int> ())
{
std::vector<int> tmp = nums;
nums.swap(tmp);
}
swap()是交换函数,使vector离开其自身的作用域,从而强制释放vector所占的内存空间.
总而言之,释放vector内存最简单的方法是vector.swap(nums)。当时如果nums是一个类的成员,不能把vector.swap(nums)写进类的析构函数中,否则会导致double free or corruption (fasttop)的错误,原因可能是重复释放内存。
5.利用vector释放指针
如果vector中存放的是指针,那么当vector销毁时,这些指针指向的对象不会被销毁,那么内存就不会被释放。如下面这种情况,vector中的元素时由new操作动态申请出来的对象指针:
vector<void *> v;
for (vector<void *>::iterator it = v.begin(); it != v.end(); it ++)
if (NULL != *it)
{
delete *it;
*it = NULL;
}
v.clear();
vector是线性容器,它的元素严格的按照线性序列排序,和动态数组很相似,和数组一样,它的元素存储在一块连续的存储空间中,这也意味着我们不仅可以使用迭代器(iterator)访问元素,还可以使用指针的偏移方式访问,和常规数组不一样的是,vector能够自动存储元素,可以自动增长或缩小存储空间。
三、源码摘要
1.默认构造出的vector不分配内存空间
2 向容器尾追加一个元素, 可能导致内存重新分配
// push_back(const T& x)
// |
// |---------------- 容量已满?
// |
// ----------------------------
// No | | Yes
// | |
// ↓ ↓
// construct(finish, x); insert_aux(end(), x);
// ++finish; |
// |------ 内存不足, 重新分配
// | 大小为原来的2倍
// new_finish = data_allocator::allocate(len); <stl_alloc.h>
// uninitialized_copy(start, position, new_start); <stl_uninitialized.h>
// construct(new_finish, x); <stl_construct.h>
// ++new_finish;
// uninitialized_copy(position, finish, new_finish); <stl_uninitialized.h>
3.内存满足条件则直接追加元素, 否则需要重新分配内存空间
4 . 在指定位置插入元素
// insert(iterator position, const T& x)
// |
// |------------ 容量是否足够 && 是否是end()?
// |
// -------------------------------------------
// No | | Yes
// | |
// ↓ ↓
// insert_aux(position, x); construct(finish, x);
// | ++finish;
// |-------- 容量是否够用?
// |
// --------------------------------------------------
// Yes | | No
// | |
// ↓ |
// construct(finish, *(finish - 1)); |
// ++finish; |
// T x_copy = x; |
// copy_backward(position, finish - 2, finish - 1); |
// *position = x_copy; |
// ↓
// data_allocator::allocate(len); <stl_alloc.h>
// uninitialized_copy(start, position, new_start); <stl_uninitialized.h>
// construct(new_finish, x); <stl_construct.h>
// ++new_finish;
// uninitialized_copy(position, finish, new_finish); <stl_uninitialized.h>
// destroy(begin(), end()); <stl_construct.h>
// deallocate();
5.最重要的是resize()这个函数:
简单来说就是,每个动态数组都分配有一定容量,当存储的数据达到容量的上限的时候,就重新分配内存。(重新开辟了一个数组,把之前的值放到复制到新开辟的数组中来。)
// resize()
#ifndef MYVECTOR_H
#define MYVECTOR_H
class MyVector
{
private:
int *data; //实际存储数据的数组
int capacity; //容量
int _size;
void resize(int st)
{
//重新分配空间,在堆区新开辟内存,然后将以前数组的值赋给他,删除以前的数组
int *newData = new int[st];
for (int i = 0; i < _size; i++)
{
newData[i] = data[i];
}
delete[] data;
data = newData;
capacity = st;
}
public:
// 构造函数
MyVector()
{
data = new int[10];
capacity = 10;
_size = 0;
}
~MyVector()
{
delete[] data;
}
MyVector(int st)
{
data = new int[st * 2]; //初始化的时候 容量分配为2倍size,实际vector不知是不是这样做的
capacity = st * 2;
_size = st;
}
void push_back(int e)
{
//如果 当前容量已经不够了, 重新分配内存, 均摊复杂度O(1)
if (_size == capacity)
{
resize(2 * capacity);
}
data[_size++] = e;
}
int pop_back()
{
int temp = data[_size];
_size--;
//如果 容量有多余的,释放掉
if (_size == capacity / 4)
{
resize(capacity / 2);
}
return temp;
}
int size()
{
return _size;
}
//重载[]操作
int &operator[](int i)
{
return data[i];
}
};
#endif