vector

来源:互联网 发布:python timer缺点 编辑:程序博客网 时间:2024/06/07 22:13

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);//重新设置sizevoid reserve(size_t n);//重新设置capacityT& 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;  }}

上面代码前半段可以先不看,主要是后面。如同我们预想那样,

  1. 它先计算新的所需要开辟空间的大小并开辟之;
  2. 然后再将原始数据搬移到新空间中去;
  3. 最后再释放旧空间。

下图展示了它的具体过程:
这里写图片描述

  • 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()来使用;当然具体如何运用还要视情况而定。

——谢谢!


参考资料

  • STL源码下载地址
  • 一个很有用的C++查询文档
  • 模拟实现的vector

【作者:果冻 http://blog.csdn.net/jelly_9】

原创粉丝点击