今天研读了下侯大师的《STL源码剖析》关于vector容器的章节,在这里总结下它的数据管理策略以及一些用法上的注意点。
以下代码参考自《STL源码剖析》。
vector容器属于序列式容器,其数据安排和操作方式,与数组十分相似,但是数组管理的是静态空间,vector对于空间的运用更加灵活。
//stl_vector.h template <class T,class Alloc = alloc> class vector{ public: //vector 相关的迭代器 typedef T value_type; typedef value_type* pointer; typedef value_type* iterator; typedef value_type* reference; typedef size_t size_type; typedef ptrdiff_t differrnce_type; protected: typedef simple_alloc<value_type,Alloc>data_allocator; iterator start; //表示目前使用空间的头 iterator finish; //表示目前使用空间的尾 iterator end_of_storage; //表示目前可用空间的尾 ........ public: iterator begin(){return start;} iterator end(){return finish;} //获得当前元素个数 size_type size()const{return size_type(end()-begin());} //获得当前容器的能容纳的最大元素数量 size_type capacity()const{ return size_type(end_of_storage-begin());} //判断容器是否为空 bool empty()const{return begin()==end();} //构造函数 vector():start(0),finish(0),end_of_storage(0){} //初始化n个值为value的元素 vector(size_type n,const T& value); vector(int n ,const T& value); vector(long n,const T&value); //vector空间大小初始化为n explicit vector(size_type n); reference front(){return *begin();} reference back(){return *(end()-1);} //将元素插入至finish处 void push_back(const T& x); //将最尾端的元素去除 void pop_back(); //清除指定位置上的元素 iterator erase(iterator position); ......
使用vector可能会碰到的坑:
1.成员函数end()返回的不是当前容器中最后一个元素的迭代器,而是最后一个元素迭代器+1,即finish。
2.使用erase(iterator p)函数时,当p所指的元素从容器中删除时,p也会失效,erase函数会返回失效前p的下一个迭代器,所以要想继续使用p进行操作,可以令p=erase(p);
3.容器能够容纳的元素的能力只有当容器空间不够存放新增的元素时才会发生改变,对容器元素的删除操作并不会改变容器容纳元素的能力。
(都是平时使用vector过程中碰到过的,以后碰到新坑还会补充)
再来谈vector实现细节:
vector维护的是一个连续的线性空间,故迭代器对容器的操作要满足*,->,++,--,+,-,+=,-=等运算操作,而普通指针刚好满足这些操作要求,所以vector的迭代器就是指向某一类型的原始指针,这点从代码就可以看出来。
vector的空间配置器使用的就是SGI 的空间配置器:
template<class T,class Alloc = alloc>
但是为了适应不同类型元素的大小,vector又在其内部定义了data_allocator:
typedef simple_alloc<value_type,Alloc> data_allocator;
vector元素删除操作:pop_back函数和erase函数
void pop_back(){ --finish; //尾端前移一格 destroy(finish); //销毁最后一个元素 }pop_back()函数类似于栈的pop()操作。
erase函数有两个重载:
iterator erase(iterator first,iterator last)和iterator erase(iterator position);
前者从容器删除[first,last]之间的元素,后者从容器删除指定位置position的元素。
前者的删除策略是:先将[last,finish)之间的元素拷贝到[first,i)空间,然后销毁[i,finish)之间的元素,并修改finish值。i值应该等于first+finish-last。
后者的删除策略是:如果position不是最后一个元素迭代器,那么将[position+1,finish)的元素向前移动一个迭代器,如果是,就无需移动;finish前移,销毁最后一个元素。
iterator erase(itertor first,iterator last){ iterator i = copy(last,finish ,first); destroy(i,finish); finish -=last-first; return first; } iterator erase(iterator position){ if(position+1!=end()) copy(position+1,finish,position); --finish; destroy(finish); return position; }
再看vector添加元素操作:push_back函数和insert函数。
无论是push_back还是insert,都需要解决的一个问题是:当容器无法容纳新增元素时怎么办?
总的策略其实是类似的:如果没有空间,就扩充空间,步骤就是
1、重新配置更大的空间;
2、将原来空间的元素全部移动到新空间;
3、将新增元素填充到新空间中旧元素的后面;
4、销毁原来的空间,并调整迭代器指向新空间。
具体的实现细节不同。
push_back函数:
当空间不够时,push_back自身不会去做上述的步骤,而是调用vector的成员函数insert_aux()去做;
insert_aux会按照前述步骤来解决空间不够的问题,在配置新空间时其配置原则为:
1、如果原空间大小为0,则配置一个元素大小空间;
2、否则配置为原空间大小的两倍。
void push_back(const T&x){ if(finish!=end_of_storage){ construct(finish,x); ++finish; } else insert_aux(end(),x); }
insert函数:
insert函数在实现时相对较复杂,这里不再罗列代码,有兴趣可以参考《STL源码剖析》。
这里讲下不同情况下insert如何做到在p位置插入n个值为x的元素。现假定vector元素初始位置为s,最末元素下一个位置为f,备用空间大小为m,插入元素位置为p,插入元素个数为n,值均为x。
情况一:备用空间充足,即m>=n。
记录原始f位置,old_f = f。
若插入点之后旧元素个数f-p>新增元素个数n:
策略:将f前面n个旧元素移动到f之后,再把[p,f)之间未移动的旧数据向后移动n个空间,最后将n个x填入[p,p+n-1]空间
具体步骤:
(1)将[f-n,f-1]之间的数据移动到[f,f+n]空间;
(2)令f=f+n;
(3)将[p,old_f-n-1]之间的数据移动到[p+n,old_f-1]空间;
(4)将n个x填入[p,p+n-1]空间。
若插入点之后旧元素个数 f-p<=新增元素个数n:
策略:在finish之后的空间填入若干个x,要使得包括p位置元素在内的p位置之后的元素个数=n,然后将[p,old_f]之间的旧元素移动到这些元素后面,最后再在这些旧元素原始位置处填入x。
具体步骤:
(1)在[f,p+n-1]空间填满值为x的元素;
(2)令f = p+n;
(3)将[p,old_f-1]间的旧元素移动到[f,old_f+n-1]之间;
(4)改变f=old_f+n;
(5)在[p,old_f-1]之间填满值为x的元素。
情况二:备用空间不足,即m<n。
策略类似于前面提到的insert_aux函数用到的策略。
具体步骤:
(1)扩充空间至原来的两倍;
(2)将旧空间的[s,p-1]元素移动到新空间的[s,p-1]上;
(3)在新空间的[p,p+n-1]上填满值为x的元素;
(4)将旧空间的[p,f-1]元素移动到新空间的[p+n,f+n-1]上;
(5)销毁旧空间,将迭代器指向新空间。