STL之vector
Posted randyniu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了STL之vector相关的知识,希望对你有一定的参考价值。
vector是我用的最多的容器之一,非常的好用。在做leetcode的时候,用的也很多,现在来剖析一下其源码的实现逻辑。
array和vector唯一的区别在于空间的灵活使用。
vector提供的迭代器就是原生指针类型。
vector其实就是3个指针变量,start, finish, end_of_storage指针变量,因此一个vector的大小应该为12byte。
vector是会生长的,当当前空间不足以满足将要放下的数据,将会重新找到一块连续的空间用来存放元素,这就涉及到了3个步骤,重新配置,元素移动,释放原来的空间,生长总是痛苦的,容器中可以生长也就是vector和dqueue了,但是和dqueue相比,vector的成长会更加痛苦。
vector的构造与内存管理
protected: allocator_type _M_data_allocator;//配置器类型 _Tp* _M_start;//内存空间起始地址 _Tp* _M_finish;//已使用空间结束地址 _Tp* _M_end_of_storage;//可用空间结束地址 _Tp* _M_allocate(size_t __n)//配置空间 是调用空间配置器来进行元素大小的分配。 { return _M_data_allocator.allocate(__n); } void _M_deallocate(_Tp* __p, size_t __n)//释放空间 { if (__p) _M_data_allocator.deallocate(__p, __n); } };
explicit vector(const allocator_type& __a = allocator_type()) : _Base(__a) {}//默认构造函数 vector(size_type __n, const _Tp& __value, const allocator_type& __a = allocator_type()) : _Base(__n, __a)//构造函数,里面包含n个初始值为value的元素 //全局函数,填充值函数,即从地址M_start开始连续填充n个初始值为value的元素 { _M_finish = uninitialized_fill_n(_M_start, __n, __value); } explicit vector(size_type __n)//该构造函数不接受初始值,只接受容易包含元素的个数n : _Base(__n, allocator_type()) { _M_finish = uninitialized_fill_n(_M_start, __n, _Tp()); } vector(const vector<_Tp, _Alloc>& __x) : _Base(__x.size(), __x.get_allocator())//拷贝构造函数 { _M_finish = uninitialized_copy(__x.begin(), __x.end(), _M_start); }
将元素插入到尾部的时候,首先检查是否还有备用空间,如果有就直接在备用空间上面构造元素,并调整finish,是vector变大,如果没有备用空间,就需要扩充空间,重新配置,移动元素,释放原空间。
void push_back(const _Tp& __x) {//在最尾端插入元素 if (_M_finish != _M_end_of_storage) {//若有可用的内存空间 construct(_M_finish, __x);//构造对象 ++_M_finish; } else//若没有可用的内存空间,调用以下函数,把x插入到指定位置 _M_insert_aux(end(), __x); } vector<_Tp, _Alloc>::_M_insert_aux(iterator __position, const _Tp& __x) { if (_M_finish != _M_end_of_storage) {//若有可用的内存空间 处理的是在中间插入一个元素这一种情况 construct(_M_finish, *(_M_finish - 1));//在备用空间起始处构造一个元素,并以vector最后一个元素值为其初始值 ++_M_finish;//调整迭代器的位置 _Tp __x_copy = __x; /*函数原型: template< class BidirIt1, class BidirIt2 > BidirIt2 copy_backward( BidirIt1 first, BidirIt1 last, BidirIt2 d_last ); { while (first != last) { *(--d_last) = *(--last);//反序复制数据 } return d_last; } 功能:把[first,last)范围的数据复制到d_last之前的空间 */ copy_backward(__position, _M_finish - 2, _M_finish - 1);//相当于把[position,finish-2]的数据向后以为覆盖数据 *__position = __x_copy;//把x插入到指定的位置 } else {//若不存在可用的内存空间,重新分配空间,使其满足要求 const size_type __old_size = size();//保存原来的内存空间大小 /*若原始空间不等于0,则新分配的空间为原来的两倍 *若原始空间为0,则新分配的空间为1 */ const size_type __len = __old_size != 0 ? 2 * __old_size : 1; iterator __new_start = _M_allocate(__len);//分配大小为len的内存空间 iterator __new_finish = __new_start; __STL_TRY { //把[start,position)的原始数据复制到新的内存空间 __new_finish = uninitialized_copy(_M_start, __position, __new_start); construct(__new_finish, __x);//把x值插入到指定的位置 ++__new_finish; //复制[position,finish)原始剩余数据到新的空间 __new_finish = uninitialized_copy(__position, _M_finish, __new_finish); } __STL_UNWIND((destroy(__new_start,__new_finish), _M_deallocate(__new_start,__len))); destroy(begin(), end());//析构原始对象 _M_deallocate(_M_start, _M_end_of_storage - _M_start);//释放原来的内存空间 //调整迭代器所指的地址 _M_start = __new_start; _M_finish = __new_finish; _M_end_of_storage = __new_start + __len; } }
在这里其实考虑过,为什么不尝试使用给一个realloc这个函数调用,如果后面的空间扩展足够两倍,这一次不久不需要进行大量的拷贝移动构造析构了么?也是不知道为什么,可能是工程经验吧。
void pop_back() {//取出最尾端元素 --_M_finish; destroy(_M_finish);//析构对象 } iterator erase(iterator __position) {//擦除指定位置元素 if (__position + 1 != end()) copy(__position + 1, _M_finish, __position);//后续元素前移一位 --_M_finish; destroy(_M_finish);//析构对象 return __position; } iterator erase(iterator __first, iterator __last) {//擦除两个迭代器区间的元素 iterator __i = copy(__last, _M_finish, __first);//把不擦除的元素前移 destroy(__i, _M_finish);//析构对象 _M_finish = _M_finish - (__last - __first);//调整finish的所指的位置 return __first; } void resize(size_type __new_size, const _Tp& __x) {//改变容器中可存储的元素个数,并不会分配新的空间 if (__new_size < size()) //若调整后的内存空间比原来的小 erase(begin() + __new_size, end());//擦除多余的元素 else insert(end(), __new_size - size(), __x);//比原来多余的空间都赋予初值x } void resize(size_type __new_size) { resize(__new_size, _Tp()); } void clear() { erase(begin(), end()); }//清空容器
上面这些函数都很容易理解,就是找到位置,构造对象,移动指针,析构对象这些过程,是挺细腻的内容。
下面讲一下插入函数。
iterator insert(iterator __position, const _Tp& __x) {//把x值插入到指定的位置 size_type __n = __position - begin();//找到位置 if (_M_finish != _M_end_of_storage && __position == end()) {//还有空间并且位置是在尾部 construct(_M_finish, __x);//直接在尾部进行构造 ++_M_finish; } else //调用之前的那个辅助函数 _M_insert_aux(__position, __x); return begin() + __n; } iterator insert(iterator __position) { size_type __n = __position - begin(); if (_M_finish != _M_end_of_storage && __position == end()) { construct(_M_finish); ++_M_finish; } else _M_insert_aux(__position); return begin() + __n; } void insert (iterator __pos, size_type __n, const _Tp& __x) { //在pos位置连续插入n个初始值为x的元素 _M_fill_insert(__pos, __n, __x); } template <class _Tp, class _Alloc> void vector<_Tp, _Alloc>::_M_fill_insert(iterator __position, size_type __n, const _Tp& __x) { if (__n != 0) {//当n不为0,插入才有效 这种判断是很有必要的逻辑判断 if (size_type(_M_end_of_storage - _M_finish) >= __n) {//若有足够的可用空间,即备用空间不小于新插入元素个数 _Tp __x_copy = __x; const size_type __elems_after = _M_finish - __position;//计算插入点之后的现有元素个数 iterator __old_finish = _M_finish; //case1-a:插入点之后的现有元素个数大于新插入元素个数 if (__elems_after > __n) { uninitialized_copy(_M_finish - __n, _M_finish, _M_finish);//把[finish-n,finish)之间的数据复制[finish,finish+n) _M_finish += __n;//调整迭代器finish所指的位置 copy_backward(__position, __old_finish - __n, __old_finish);//把[position,old_finish-n)之间的数据复制[old_finish-n,old_finish) fill(__position, __position + __n, __x_copy);//在指定位置(插入点)填充初始值 } //case1-b:插入点之后的现有元素个数不大于新插入元素个数 else { uninitialized_fill_n(_M_finish, __n - __elems_after, __x_copy);//先在可用空间填入n-elems_after个初始值x _M_finish += __n - __elems_after;//调整迭代器finish uninitialized_copy(__position, __old_finish, _M_finish);//把[position,old_finish)之间的数据复制到[old_finish,finish) _M_finish += __elems_after; fill(__position, __old_finish, __x_copy); } } //case2:若备用空间小于新插入元素个数 else {//若备用空间小于新插入元素个数,则分配新的空间 //并把原始数据复制到新的空间,调整迭代器 const size_type __old_size = size(); //获取原始空间的大小 //新的空间为旧空间的两倍,或为旧空间+新增长元素个数 const size_type __len = __old_size + max(__old_size, __n); //配置新的空间 iterator __new_start = _M_allocate(__len); iterator __new_finish = __new_start; __STL_TRY {//把插入点之前的原始数据复制到新的空间 __new_finish = uninitialized_copy(_M_start, __position, __new_start); //将新加入数据添加在[new_finish,new_finish+n) __new_finish = uninitialized_fill_n(__new_finish, __n, __x); //将插入点之后的原始数据复制到新空间 __new_finish = uninitialized_copy(__position, _M_finish, __new_finish); } //释放原来空间的对象和内存 __STL_UNWIND((destroy(__new_start,__new_finish), _M_deallocate(__new_start,__len))); destroy(_M_start, _M_finish); _M_deallocate(_M_start, _M_end_of_storage - _M_start); //调整迭代器所指的位置 _M_start = __new_start; _M_finish = __new_finish; _M_end_of_storage = __new_start + __len; } } }
插入函数时进行转调用的,首先判断数量是不是等于0,如果是,就什么也不做,否在才进行插入。
判断剩余空间是否够用,如果够用则计算插入点之后现有元素的个数。
如果插入点之后现有元素个数大于新增元素的个数,则对后面的元素进行构造初始化,然后调用copy_backword函数将内容数据赋值到后边,在对插入点位置进行填充
如果插入点之后的元素个数小于新增元素的个数,则对后面新增的元素用x进行填充,然后讲前面的元素拷贝到后面,在对中间的元素进行填充赋值。这些完全都是从性能上看的考量。假设新增的元素数量是远远大于当前位置后面元素的数量,这样做可以直接构造成为你需要的具体元素,减少了调用构造函数的次数。
如果空间不满足, 则重新分配,元素移动,释放原空间。
先用x进行构造,然后在将前面的元素用之前的元素进行替换,这样就可以了,别忘了最后更新三个参数。
vector的操作大概就这样,还是很容易理解的,相比较而言,所有的这些考虑全部都是为了性能的考虑,真可谓是锱铢必较了。
忘了说一点,由于vector是会生长的,因此当你插入元素之后,你之前保留的迭代器就可能会失效了,因此这一点应该尤为注意。
以上是关于STL之vector的主要内容,如果未能解决你的问题,请参考以下文章