vector是STL诸多序列式容器之一,它可以像数组那样操作其中所储存的元素,不过vector的威力不止于此,它可是一个“智能数组”,为什么呢?以下:
- 完整实现代码
概述
我们都知道,如果你定义一个大小为10的整形数组,那么,在该数组的整个生命中,它的大小就永远只能是10,它是静态的。想要换个更大(也可能是更小)的房子,一切的琐碎事务都需要你手动完成(配置新数组空间+搬移旧数组数据+释放旧数组空间)。然而,vector不同,它是一个动态的“数组”。
如同数组一样,vector也使用一段连续的空间储存数据, 这就意味着我们可以通过“下标”去访问该连续空间中任意一个位置。但若只有这样,它好像和数组没什么区别。不同的是,vector是“动态”的管理它的空间,它的空间随着所储存元素的增多而增长,它的内部算法会主动帮你完成”开辟新空间+搬移元素+释放旧空间”这些繁琐的操作。不过,并不是每进来一个元素,它就增长一次,因为“增长”本身来说就是一种比较昂贵(需要花费额外的时间或空间)的操作。它使用一种未雨绸缪的做法,每次增长时,会开辟一个比所需空间更大的空间,预留出空间以达到当有元素进来时,不必频繁的去做那些琐碎昂贵的增长操作。
内部结构
vector的内部通过几个指针来管理空间:
以下相关代码摘自STL源码
iterator start;
iterator finish;
iterator end_of_storage;
(iterator
:暂且把它当成指针)
start
: 指向所管理的空间的起始位置;finish
:指向最后一个有效元素的下一个位置;end_of_storage
:指向所管理的空间的结尾。
其中,finish
总是指向vector所管理的空间的起始位置,每当开辟出一块新的空间时,就会让start
指向它(当然前提要释放它原来所指的空间),而finish所指向的位置是vector中最后一个有效元素的下一个位置,这样做的好处是在尾端插入或删除数据时,只需简单的改动finish
指针。至于end_of_storage
,它指向vector所管理空间的结尾位置。由于它们指向了同一块连续的空间,所以我们可以通过指针之间简单的加减运算算出vector的size和capacity。
基本操作
既然vector是一个“智能数组”,那么它到底智能在哪里呢?我们通过它的几个接口一探究竟。
vector主要包括(但不限于)以下接口:(更多接口)
void push_back(const T& x);//在尾部插入一个元素
void pop_back();//删除尾部的一个元素
void resize(size_t n, const T& x);//重新设置size
void reserve(size_t n);//重新设置capacity
T& operator[](size_t n);//返回下标n处的元素(以支持数组的下标访问)
- push_back()
如上,简单的使用push_back()
可以在vector的尾部插入一个元素,在STL中它的内部是这样实现的:
void push_back(const T& x) {
if (finish != end_of_storage) {//如果空间没满
construct(finish, x);//在尾端插入元素
++finish;//移动尾指针
}
else
insert_aux(end(), x);//空间满了,则换个更大的房子
}
我们发现,在插入操作中,当finish
指针和 end_of_storage
指针不相等时,表明当前预留有空间足以储存进来的元素,那么这样的话调用construct()
在尾部构造元素并移动finish
指针;否则说明当前的空间已经满了,那么只能去找更大的容身之所了(insert_aux()
)。
construct()
它是STL 空间配置器中的一个基本构造工具,在此处我们可以把它简单理解为把x
储存到finish
位置上,不过关于空间配置器的东西可远比这复杂。
insert_aux()
的内部做了这样一些事情:
void vector<T, Alloc>::insert_aux(iterator position, const T& x) {
if (finish != end_of_storage) {
construct(finish, *(finish - 1));
++finish;
T x_copy = x;
copy_backward(position, finish - 2, finish - 1);
*position = x_copy;
}
else {
const size_type old_ size = size();//计算旧容量
const size_type len = old_size != 0 ? 2 * old_size : 1;//计算新容量
iterator new_start = data_allocator::allocate(len);//开辟新空间
iterator new_finish = new_start;
__STL_TRY {//下面是搬移元素
new_finish = uninitialized_copy(start, position, new_start);
construct(new_finish, x);
++new_finish;
new_finish = uninitialized_copy(position, finish, new_finish);
}
# ifdef __STL_USE_EXCEPTIONS
catch(...) {
destroy(new_start, new_finish);
data_allocator::deallocate(new_start, len);
throw;
}
# endif /* __STL_USE_EXCEPTIONS *///以下是释放旧空间
destroy(begin(), end());
deallocate();
start = new_start;
finish = new_finish;
end_of_storage = new_start + len;
}
}
上面代码前半段可以先不看,主要是后面。如同我们预想那样,
- 它先计算新的所需要开辟空间的大小并开辟之;
- 然后再将原始数据搬移到新空间中去;
- 最后再释放旧空间。
下图展示了它的具体过程:
- pop_back()
相对于插入元素,删除就很简单了,如上示例:
void pop_back() {
--finish;
destroy(finish);
}
我们可以看到,在pop_back()
中,只是简单移动finish
指针就达到删除元素的目的了。
下面是两个比较重要的也是很容易误用的接口。
- resize()
resize()
这个接口用于设置重新size
的大小,它的第一个参数表示要设置的size的大小,如果新size小于旧size,那么它会删除掉新size以后的元素,否则它将介于新size和旧size之间的位置设置成第二个参数的值(若没有第二个参数则使用缺省值)。
它的源码如下:
void resize(size_type new_size, const T& x) {
if (new_size < size()) //如果新size小于旧size
erase(begin() + new_size, end());//删除新size之后的元素
else//否则用x填充旧size到新size之间的位置
insert(end(), new_size - size(), x);
}
通过上面的源码我们可以轻易地看懂resize()
内部做了什么。它的主要作用就是改变当前vector的size大小。
- reserve()
reserve()
感觉好像有点奇怪。它只接受一个参数n,不准确的说,“该接口目的是将vector的容量设置成n”,但是他有限制条件,就是只有你欲设置的大小比当前容量大时,它才会开辟新的空间,否则它就什么也不做,它不会改变size的大小。
void reserve(size_type n) {
if (capacity() < n) {//只有n比当前容量大时才做下面这些事情
const size_type old_size = size();//计算旧空间大小
iterator tmp = allocate_and_copy(n, start, finish);//申请新的空间并搬移元素
destroy(start, finish);//释放旧空间
deallocate();
start = tmp;//膝盖相关指针指向
finish = tmp + old_size;
end_of_storage = start + n;
}
}
阅读源码,正如我们预想的一样。
总结
在STL中有关vector还有许多接口,感兴趣的可以取查阅该文档。
上面简单介绍了一些vector常用的或者是容易误用的接口,下面来简单总结一下。
数组和vector
数组:
- 数组是有限个相同类型的元素的集合;
- 它支持“下标”和“数组名加偏移量”随机访问元素;
- 数据储存在一块静态的空间上,整个生命周期中它的空间无法被改变(非要改变的话只能有你自己手动处理)。
vector:
- 如同数组一样,用于储存同类型的数据元素;
- 支持下标随机访问元,但不支持对象名加偏移量访问,这个大家可以自行验证;
- 内部封装了一块静态空间和诸多处理算法,通过三个指针管理空间,可以随着数据元素的插入而重新分配空间,调整大小。动态管理它的空间;
- 支持但不限于尾插、尾删、调整size、调整capacity、清空以及迭代器操作等结口。
其实上述区别也得益于C++封装的特性,C++的类可以将数据结构和算法封装在对象的内部,而只需将操作接口暴露给用户,这样,底层就解决掉我们的很多顾虑。
resize()和reverse()
resize():
- 本质上是要改变size:
- 当新size小于旧size时——就将新size之后的元素删掉;
- 当新size大于旧size但小于旧容量时——用第二个参数填充旧size至新size之间的空间;
- 当新size大于容量时——先开辟一个足以容纳新size大小的空间,然后搬移旧元素,并用参数二填充旧size至新size之间的空间。
reserve():
- 不会改变size,只可能会改变容量:
- 当参数n小于当前容量时——什么也不做;
- 当n大于当前容量时——开辟新空间,搬移元素,释放旧空间。
介于resize()
和reserve()
上述区别,在使用时应该这样搭配:
- 用resize()处理的空间搭配着下标(就是[])来使用;
- 用reserve()处理的空间搭配着
push_back()
来使用;当然具体如何运用还要视情况而定。
——谢谢!