本着实践才是检验是否学会的标准,看完书和写完demo程序后,觉得还是有必要记录下对vector的一些心得。本文截图取自《STL源码剖析》和SGI STL源代码
vector的内存模型
vector的内存模型是动态增长的线性空间,其实本质上来说就是堆上的数组。
动态增长的实质是,需求空间超过当前可用空间后,重新申请一块更大的空间来作为载体,然后复制已有数据过去新空间,删除回收旧空间,然后在新空间上插入新数据等策略上。
上面的话简单概括就是动态增长的本质是:配置新空间->移动数据->释放返还旧空间。可以看出这里的每一步都是一个大工程,都是些很花时间的活。因此vector的各种机制基本都是围绕这尽可能少的出现空间重新配置的次数的目标来设计。
机制1:预留空间备用
vector的结构如上图,有三个迭代器控制这vector的可用空间和已用空间
分别是
iterator start; //使用空间的头,begin()方法返回的所指对象
iterator finish; //使用空间的头,同理是end()方法返回的对象
iterator end_of_stage;//可用空间的尾,vector真是空间的尾
那有个问题来了,对可用空间里的空间复制或者寻址是否合法?
由下面的源码可以看出是合法的,operator[]
并没有检查访问的是否是已用空间里面的元素。所以我们是可以对[finish~end_of_stage-1]区间进行寻址访问的。PS:vector的下标[]访问和数组一样,是从0下标开始的。
//这里的代码是合法,可编译的且能正常输出vec[7]。
vector<int> vec;
for(int i=0;i<5;i++) //插入 0,1,2,3,4
{
vec.push_back(i);
}
vec[7]=10;
cout<<vec[7]<<endl;
但是要注意,通过迭代器访问的话是无法寻址找到vec[7]的,因为operator[]
操作并没有修改finish的位置。使用迭代器遍历vec的话,输出结果如下。
//获取迭代器
vector<int>::iterator it = vec.begin();
while(it!=vec.end())//输出
{
cout<<*it<<" "; //0,1,2,3,4
it++;
}
cout<<"size:"<<vec.size()<<endl; //size 5
cout<<"cap_size:"<<vec.capacity()<<endl; //cap_size:8
机制2:双倍增长空间
当可用空间用完后,会触发空间的增长,STL里面的vector的push_back()
方法触发的增长是增长一倍:new_size=2*old_size
。insert_aux()
是最终用来调整空间的操作。
增长的规律是1,2,4,8,16。
这样的话在频繁插入的场景中,可以尽可能的减少重新配置空间的调用次数。因为空间的重新配置并不是原地配置的,是另找一块地方。
注意
这里要提及一点,insert()
方法中的
void insert (iterator __pos, size_type __n, const _Tp& __x)
插入n个X的实现的
增长机制不一定是两倍,而是new_size=old_size + max(old_size,n)
下面是测试代码
for(int i=0;i<10;i++) //插入 0~9
{
vec.push_back(i);
}//cap_size=16
cout<<"size:"<<vec.size()<<endl; //size 10
cout<<"cap_size:"<<vec.capacity()<<endl; //cap_size 16
vec.insert(vec.end(),1,5); //新插入1个,size=11 cap_size=16 剩余5个可用空间
//insert插入后如果需要空间超过备用的空间,内存增长的策略是
// new_size=old_size +max(old_size,n) 这里的 old_size是已用空间的大小size,不是cap_size
vec.insert(vec.end(),6,5); //新插入6个(小于11)超过可用空间,根据策略new_size=11+11,size=17 cap_size=22 剩余空间0
vec.insert(vec.end(),20,5); // 新插入18个(大于11)超过可用空间,根据策略new_size=17+20,size= 37,cap_size=37
关于元素的移动过程就不多述了,放几张图
机制3:尽可能的原地操作
看到上面insert元素的操作的移动过程的”第一,二张图”。vector在发生插入操作时只要备用空间够用,就在原空间上移动元素进行操作。
还有相对应的擦除操作erase()
,也是在原地进行操作
但要注意删除之后,返回的是最后一个被删除对象的下一对象的迭代器,但是本质上这个迭代器本身的地址并没有发生++操作,变的只是内容,要结合图片和代码来思考
实际上析构的是finish所以的内容。
测试代码如下
for(int i=0;i<5;i++) //插入 0,1,2,3,4
{
vec.push_back(i);
}
it=vec.begin();//重新获取迭代器
while(it!=vec.end()) //0,1,2,3,4
{
cout<<*it<<endl;
//erase方法析构当前it所指对象,并返回下一个对象
cout<<"before erase address:"<<&it<<endl;
it = vec.erase(it);
cout<<"after erase address:"<<&it<<endl;
}
//但是空间是保留的
cout<<"size:"<<vec.size()<<endl; //size 0
cout<<"cap_size:"<<vec.capacity()<<endl; //8
输出的结果it地址是一直不变的。
因此要注意啊,对vector进行操作是很容易引起迭代器失效得,本来指向3,可能后面就不是了,因此使用vector有个好习惯是进行增删改后,及时更新迭代器,免得埋下一颗无形bug。
机制4:只析构对象,不回收空间
这个呢也不知道是好是坏,有人诟病vector太吃空间了,用完后也不释放内存,但是这个机制其实也是有优点的,就是减少申请空间的次数嘛
要注意,erase()
方法和clear()
方法都是只析构对象而不释放内存
可以看到clear只是进一步调用区间erase。
而且有意思的是,对于C++内置基本类型,连析构的操作都省了。。。。
博主发现一个有意思的地方
没错,啥也不干。好奇的博主,特地测试了一下,还真的是什么都不干
vec.push_back(1);
vec.push_back(2);
vec.pop_back();
cout<<vec[1];//输出:2
那如果要回收内存呢,总有些场景需要回收一下吧,这是可以使用临时变量,声明一个临时的空的vector,然后和需要的回收的vector交换,这样原vector就会释放空间,同时在临时vector离开作用域的时候会自动析构并回收内存
下面是测试代码
//大括号可以定义出一段临时作用域
{//使用临时变量回收vec已分配的空间
vector<int> temp;
vec.swap(temp);
cout<<"cap_size:"<<vec.capacity()<<endl; //size 0
}
那如果不想整个析构呢,只是想压缩一下,C++11中增加了一个新的shrink_to_fit()
成员,可以去掉vector的备用空间,即使得capicity_size=size。
另外有两个函数需要提及一下,书里面没有讲
分别是resize()
和reserve()
函数,这里有个很好得讲解
resize和reserve得区别
简单说说resize()
设置的是已用空间的大小,reserve()
设置的是总空间的大小。
其实呢,vector还有很多成员函数,毕竟《STL源码剖析》所使用得SGI_STL比较旧了,比如上面那个shrink_to_fit()就没有。所以我这里不打算逐个逐个说,重复无用工作。正好找到不错得前辈总结vector方法得博文,以后就打算,一篇记录读书笔记,一篇转载别人总结得方法。