SGI STL顺序容器 vector

vector vs array

在SGI STL中,vector和array都是数组容器,两种操作非常相似。区别在于:array是静态空间,一旦配置就不能改变;vector是动态空间,随着新元素加入,内部机制或自行扩充空间以容纳新元素。

vector的迭代器

vector维护的是一个连续线性空间,不论元素类型是什么,普通指针都可以作为vector的迭代器而满足所有必要条件,因为vector需要的操作:operator*,operator->,operatro++, operator--, operator+, operator-, operator+=, operator-=。而普通指针天生就具备这些操作。vector支持随机存取(operator[]),而普通指针也有这样的能力。因此,vector提供的迭代器是Random Access Iterators。

// vector迭代器关联类型
template <class _Tp, class _Alloc = __STL_DEFAULT_ALLOCATOR(_Tp) >
class vector : protected _Vector_base<_Tp, _Alloc>
{
...
public:
    // 定义vector的迭代器关联类型, 可用于iterator_traits萃取特性, 兼容STL架构
  typedef _Tp value_type;
  typedef value_type* pointer;
  typedef const value_type* const_pointer;
  typedef value_type* iterator;              // vector的迭代器是普通指针
  typedef const value_type* const_iterator;
  typedef value_type& reference;
  typedef const value_type& const_reference;
...
};

也就是说,如果客户端写出这样的代码:

vector<int>::iterator ivite;
vector<Shape>::iterator svite;

ivite的类型其实是int,svite的类型是Shape

vector的数据结构

vector采用的数据结构就是一个线性连续空间。它以两个迭代器start和finish分别指向配置得来的连续空间中目前已被使用的范围,并以迭代器end_of_storage指向整块连续空间(含备用空间)的尾端。

vector通过基类_Vector_base的三个迭代器start、finish、end_of_storage,提供线性空间的首尾表示、大小、容量、空容器判断、随机访问运算符operator[]、最前端元素值、最后端元素值等功能。

vector主要通过下面这部分,对外提供接口访问内部数据结构存储的元素:

template <class _Tp, class _Alloc = __STL_DEFAULT_ALLOCATOR(_Tp) >
class vector : protected _Vector_base<_Tp, _Alloc>
{
...
public:
  iterator begin() { return _M_start; } // 线性空间起始位置对应迭代器
  const_iterator begin() const { return _M_start; } // 线性空间起始位置对应const迭代器
  iterator end() { return _M_finish; }  // 线性空间末尾位置对应迭代器
  const_iterator end() const { return _M_finish; }  // 线性空间末尾位置对应const迭代器

  reverse_iterator rbegin()
    { return reverse_iterator(end()); }
  const_reverse_iterator rbegin() const
    { return const_reverse_iterator(end()); }
  reverse_iterator rend()
    { return reverse_iterator(begin()); }
  const_reverse_iterator rend() const
    { return const_reverse_iterator(begin()); }

  size_type size() const     // 当前元素个数
    { return size_type(end() - begin()); }
  size_type max_size() const // 理论上最大能支持的元素个数
    { return size_type(-1) / sizeof(_Tp); }
  size_type capacity() const // 当前容量, 即线性空间最大能存放的元素个数
    { return size_type(_M_end_of_storage - begin()); }
  bool empty() const         // 判断是否包含元素
    { return begin() == end(); }

  reference operator[](size_type __n) { return *(begin() + __n); } // 随机访问第n-1个元素 (从0开始计), 返回元素reference
  const_reference operator[](size_type __n) const { return *(begin() + __n); } // 随机访问第n-1个元素 (从0开始计), 返回元素const reference

  reference front() { return *begin(); }    // 第一个元素reference
  const_reference front() const { return *begin(); }    // 第一个元素对应const reference
  reference back() { return *(end() - 1); } // 最后一个元素reference
  const_reference back() const { return *(end() - 1); } // 最后一个元素对应const reference 
...
};

vector对应存储结构示意图:
SGI STL顺序容器 vector

size()是指当前实际存储元素的个数,capacity()是指底层用于存储的线性空间总大小,即容量。有size() <= capacity()成了。

以vector iv(2,9)为例,经过下面操作:

vector<int> iv(2,9);
iv.push_back(1);
iv.push_back(2);
iv.push_back(3);
iv.push_back(4);

往vector 调用push_back()插入元素空间变化示意图:
SGI STL顺序容器 vector

当容量不足时,如果继续插入元素,会导致vector扩容,扩容机制是这样的:
1)当前容量为0,扩容后为1;
2)当然容量>0,扩容为原来的2倍。

擦除元素时,会导致finish指针移动,改变size()大小,但并不会导致end_of_storage指针变化,也就是不会导致容量变化。如果想要改变容量以节省空间,需要调用vector::shrink_to_fit(),减少容量以适应size()大小。不过shrink_to_fit()是C++11加入的内容,老版的SGI STL并没有该功能。
简而言之,老版SGI STL无法缩减vector容量。

vector的构造与内存管理

vector的空间配置与释放

vector的空间配置是通过基类_Vector_base完成的,所有细节都封装在基类内部。这样,vector就无需关心空间如何配置、释放的细节。

vector基类_Vector_base:

// vector基类, 负责维护vector底层数据结构的空间配置
template <class _Tp, class _Alloc>
class _Vector_base {
public:
  typedef _Alloc allocator_type;
  allocator_type get_allocator() const { return allocator_type(); } // 构造一个临时allocator_type(空间配置器)对象

  _Vector_base(const _Alloc&)
    : _M_start(0), _M_finish(0), _M_end_of_storage(0) {}
  _Vector_base(size_t __n, const _Alloc&)
    : _M_start(0), _M_finish(0), _M_end_of_storage(0)
  {
    _M_start = _M_allocate(__n);              // 初始配置n byte空间
    _M_finish = _M_start;                     // 初始finish与start在同一位置
    _M_end_of_storage = _M_start + __n;       // 初始end_of_storage位置
  }

  ~_Vector_base() { _M_deallocate(_M_start, _M_end_of_storage - _M_start); } // 释放整个线性空间

protected:
  _Tp* _M_start;             // 线性内存起始位置
  _Tp* _M_finish;            // 线性内存使用的结束位置
  _Tp* _M_end_of_storage;    // 线性内存的终点位置

  typedef simple_alloc<_Tp, _Alloc> _M_data_allocator; // 一级空间配置/二级空间配置器的包装器
  _Tp* _M_allocate(size_t __n)                         // 为子类提供配置器的allocate()接口, 配置n byte空间
    { return _M_data_allocator::allocate(__n); }
  void _M_deallocate(_Tp* __p, size_t __n)             // 为子类提供配置器的deallocate()接口, 释放起始地址为p的n byte空间
    { _M_data_allocator::deallocate(__p, __n); }
};

关于simple_alloc有一点需要注意:在vector中,所有内存操作都是以元素个数为单位,但对于一级/二级空间配置器,内存都是以byte为单位,这其中,就是simple_alloc做了转换处理,将元素个数对应内存,转换成了byte为单位的内存。

vector的构造

站在客户端的角度,构造vector的方法:

vector<int> ivec;           // vector内容为空
vector<int> ivec2(10);      // size()为10, 内容0
vector<int> ivec3(20, 5);   // 20个元素值为5
vector<int> ivec4(ivec3);   // 20个元素值为5
vector<int> ivec5({1,2,3}); // 3个元素, {1,2,3}. C++11内容, 初值列表初始化vector, 这里不列出
vector<int> ivec6(ivec3.begin(), ivec3.end()); // 用迭代区间构造vector, 内容为20个元素值5

vector的默认分配子是alloc(二级空间配置器),使用父类_Vector_base来屏蔽空间配置细节,方便以元素大小为配置单位。

    // vector 构造函数
    // 构造空vector, 对应客户端vector<int> ivec
  explicit vector(const allocator_type& __a = allocator_type())
    : _Base(__a) {}

    // 构造大小为n byte, 所有值为value的vector, 对应客户端vector<int> ivec3(20, 5)
  vector(size_type __n, const _Tp& __value,
         const allocator_type& __a = allocator_type())
    : _Base(__n, __a)
    { _M_finish = uninitialized_fill_n(_M_start, __n, __value); } // 用value填充未初始化内存区段(start, n)

    // 构造大小为n byte的vector, 对应客户端vector<int> ivec2(10)
  explicit vector(size_type __n)
    : _Base(__n, allocator_type())
    { _M_finish = uninitialized_fill_n(_M_start, __n, _Tp()); } // 这里Tp是int, 用int()(默认值0)填充未初始化内存区段(start, n)

    // 拷贝构造的vector, 对应客户端vector<int> ivec4(ivec3)
  vector(const vector<_Tp, _Alloc>& __x)
    : _Base(__x.size(), __x.get_allocator())
    { _M_finish = uninitialized_copy(__x.begin(), __x.end(), _M_start); } // 将x对应的源迭代区间所有元素拷贝到未初始化内存段

    // 用迭代区间[first, last)构造vector
  vector(const _Tp* __first, const _Tp* __last,
         const allocator_type& __a = allocator_type())
    : _Base(__last - __first, __a)
    { _M_finish = uninitialized_copy(__first, __last, _M_start); } // 将源迭代区间所有内容拷贝到start起始的目的区间

vector的析构

vector的析构函数很简单,调用全局destroy() 释放线性内存空间。

// 释放迭代器区间[first, last), 如果迭代器指向基本类型, 什么也不做; 如果迭代器指向class 类型, 就先逐个析构
~vector() { destroy(_M_start, _M_finish); }

// 注意vector析构之后, 会接着析构基类. 因此即使vector什么也没做, 基类会回收线性内存空间
// 析构基类会先回收内存段(start, end_of_storage - start)
// 空间配置那一章提到过, 如果是二级配置器, 内存>128byte, 会直接返还给OS; 如果内存<=128byte, 会加入到某个合适的free list, 留作备用
~_Vector_base() { _M_deallocate(_M_start, _M_end_of_storage - _M_start); }

指定数组初始大小reserve(), resize()

有没有办法让vector一开始就保留指定大小的线性空间,而不是慢慢动态增长到?
答案是有的,可以用vector::reserve()。

 // 让vector容量 >= n 个元素
  void reserve(size_type __n) {
    if (capacity() < __n) { // 只有当前容量 < n时, 才需要重新配置空间
      const size_type __old_size = size(); // 已经装了元素个数
      iterator __tmp = _M_allocate_and_copy(__n, _M_start, _M_finish); // 配置n个元素新空间, 并将[start, finish)元素拷贝到新空间
      destroy(_M_start, _M_finish); // 调用全局destroy()销毁[start, finish)上的元素. 对于基本类型, 什么也不做; 对于class类型, 析构对象
      _M_deallocate(_M_start, _M_end_of_storage - _M_start); // 调用基类的deallocate() 释放线性空间
        // 重新配置start, finish, end_of_storage 管理线性空间
      _M_start = __tmp;
      _M_finish = __tmp + __old_size;
      _M_end_of_storage = _M_start + __n;
    }
  }

    // 配置n个元素新空间, 并将[start, finish)元素拷贝到新空间
    // 遵循 "commit or rollback"规则
  iterator _M_allocate_and_copy(size_type __n, const_iterator __first,
                                               const_iterator __last)
  {
    iterator __result = _M_allocate(__n);
    __STL_TRY {
      uninitialized_copy(__first, __last, __result);
      return __result;
    }
    __STL_UNWIND(_M_deallocate(__result, __n)); // commit or rollback精髓: 发生异常时, 释放线性空间
  }

还有一个跟reserve()类似的接口resize(),它是负责什么的呢?
resize()用于指定vector的新size(),改变线性空间的finsih指针,但不改变start和end_of_storage指针。也就是说,resize()并不会影响capacity()大小。为满足最终size()为新指定值,如果当前元素超过指定的size,就擦除多余部分;如果当前元素占用size()不足,就会用指定值插入vector。

    // 如果size()超过new_size, 就擦除多余的; 如果不超过, 就在末尾插入指定元素x. 最终目标是让size()等于new_size
  void resize(size_type __new_size, const _Tp& __x) {
    if (__new_size < size()) // 新size < 现有size()时, 说明原来的size较大, 需要擦除一部分
      erase(begin() + __new_size, end()); // 擦除多余空间元素 [begin() + new_size, end())
    else
      insert(end(), __new_size - size(), __x);
  }
  void resize(size_type __new_size) { resize(__new_size, _Tp()); }

    // 擦除指定位置position的元素, 后面的(position~末尾)元素整体向前移动
  iterator erase(iterator __position) {
    if (__position + 1 != end()) // 要删除的元素不是末尾元素
      copy(__position + 1, _M_finish, __position); // 将擦除位置后的区间[position+1, finish)元素, 拷贝到position起始处
    --_M_finish;  // 因为只擦除一个元素, finish向前移动1
    destroy(_M_finish); // finish指向的就是要删除的那个元素, 析构之, 但不释放空间(尚未归还给配置器)
    return __position; // 返回销毁元素的位置
  }

    // 擦除迭代区间[first, last), 后面的元素整体向前移动
  iterator erase(iterator __first, iterator __last) {
    iterator __i = copy(__last, _M_finish, __first); // 将擦除区间后的区间[last, finish)元素, 拷贝到first起始处
    destroy(__i, _M_finish); // 析构[i, finish)对象, 但并没有释放空间
    _M_finish = _M_finish - (__last - __first); // 先前移动finish指针
    return __first; // 返回销毁后的起始位置
  }

vector的查询

尺寸size()与容量capacity()

size()用于查询vector当前元素个数,capacity()用于查询当前vector线性空间最多容纳元素个数。

size()和capacity()代码很简单,利用了线性空间的3个指针(start, finish, end_of_storage)

  size_type size() const     // 当前元素个数
    { return size_type(end() - begin()); }

  size_type capacity() const // 当前容量, 即线性空间最大能存放的元素个数
    { return size_type(_M_end_of_storage - begin()); }

迭代器访问元素

vector支持迭代器访问元素,提供iterator进行正向顺序访问,reverse_iterator进行反向访问,以及它们的const版本。

所谓正向迭代器,是指从(地址空间)起始到末尾,从小下标到大下标的顺序访问;
所谓反向迭代器,是指从(地址空间)末尾到开始,从大下标到小下标的顺序访问。

    // 正向迭代器
  iterator begin() { return _M_start; } // 线性空间起始位置对应迭代器
  const_iterator begin() const { return _M_start; } // 线性空间起始位置对应const迭代器
  iterator end() { return _M_finish; }  // 线性空间末尾位置对应迭代器
  const_iterator end() const { return _M_finish; }  // 线性空间末尾位置对应const迭代器

    // 反向迭代器
  reverse_iterator rbegin()
    { return reverse_iterator(end()); }
  const_reverse_iterator rbegin() const
    { return const_reverse_iterator(end()); }
  reverse_iterator rend()
    { return reverse_iterator(begin()); }
  const_reverse_iterator rend() const
    { return const_reverse_iterator(begin()); }

引用访问元素

通过两类元素访问方式:
1)通过front(),back()直接访问第一个、最后一个元素,返回的是元素引用;
2)通过operator[] 随机访问指定下标的元素,返回的是元素引用。

  reference operator[](size_type __n) { return *(begin() + __n); } // 随机访问第n-1个元素 (从0开始计), 返回元素reference
  const_reference operator[](size_type __n) const { return *(begin() + __n); } // 随机访问第n-1个元素 (从0开始计), 返回元素const reference

  reference front() { return *begin(); }    // 第一个元素reference
  const_reference front() const { return *begin(); }    // 第一个元素对应const reference
  reference back() { return *(end() - 1); } // 最后一个元素reference
  const_reference back() const { return *(end() - 1); } // 最后一个元素对应const reference

vector元素操作

尾端插入元素push_back

push_back是在线性空间当前已经使用段的末尾,添加一个新对象,同时右移finish指针。
根据插入对象是否有参数,有两个版本push_back:1)以值x构造的Tp对象;2)无参构造Tp对象,即不需要初值x用于构造Tp对象。

  // 在尾端插入以x构造的对象(调用Tp(x)), 会导致size加1. 容量不够时, 需要扩容
  void push_back(const _Tp& __x) {
    if (_M_finish != _M_end_of_storage) { // 备用空间足够, 不需要扩容
      construct(_M_finish, __x); // 在尾端finish所指位置用x构造对象Tp()
      ++_M_finish;               // 尾端标记右移一格
    }
    else
      _M_insert_aux(end(), __x); // 在指定位置end() 插入以x构造的对象Tp()
  }

  // 在尾端插入空对象(调用Tp()), 会导致size加1. 容量不够时, 需要扩容
  void push_back() {
    if (_M_finish != _M_end_of_storage) { // 备用空间足够, 不需要扩容
      construct(_M_finish); // 在尾端finish所指位置构造对象Tp()
      ++_M_finish;          // 尾端标记右移一格
    }
    else
      _M_insert_aux(end()); // 在指定位置end() 插入新构造对象Tp()
  }

指定位置插入元素insert

insert也是用于插入元素,跟push_back的区别在于insert是在指定位置(迭代器)插入新元素。
也就是说,push_back(x)相当于insert(end(), x)。

//-----------------------------
// insert单个对象
  // 在指定位置position插入单个对象Tp(x)
  iterator insert(iterator __position, const _Tp& __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;
  }

  // 在指定位置position插入单个对象Tp()
  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;
  }

//-----------------------------
// insertd多个对象

  // 在指定位置position插入源区间[first, last)所有对象
  void insert(iterator __position,
              const_iterator __first, const_iterator __last);

  // 在指定区间{pos, n}插入构造的Tp(x)对象
  void insert (iterator __pos, size_type __n, const _Tp& __x)
    { _M_fill_insert(__pos, __n, __x); }

insert不仅支持插入单个元素,还支持插入多个元素。通过指定 起始位置 + 源迭代器区间,或者指定 起始位置+元素个数+元素值,就能在指定位置(区间)上一次插入多个元素。

insert批量插入代码较为复杂,我们直接图解:
SGI STL顺序容器 vector

SGI STL顺序容器 vector

SGI STL顺序容器 vector

SGI STL顺序容器 vector

弹出最后一个元素pop_back

pop_back()弹出vector的最后一个元素,会让size减-1,但不会影响capacity。

  // 将尾端元素拿掉, 并调整大小
  void pop_back() {
    --_M_finish; // 尾端标记前移一格, 表示将放弃尾端元素
    destroy(_M_finish); // 全局函数, 如果finish所指元素是trivial type, 什么也不做; 如果是class type, 析构finish所指对象
  }

擦除元素earse

擦除元素分为两类:1)擦除指定位置元素;2)擦除指定区间所有元素。

擦除元素后,后面的所有元素都会往前移动,以补充空位。另外,移动后的空位置,如果不是trivial type,需要调用其析构函数析构对象;如果是trivial type,可以什么也不做。
擦除元素会影响size,因此需要移动尾端标记finish,移动格数取决于擦除的元素个数。但不会影响capacity。

  // 擦除指定位置元素
  iterator erase(iterator __position) {
    if (__position + 1 != end()) // position所指并非最后一个元素, 说明后续还有元素, 需要往前移动补充擦除的空位
      copy(__position + 1, _M_finish, __position); // 将元素从[position+1, finish)拷贝到[position, ...), 也就是position+1之后元素往前移动1格
    --_M_finish; // 尾端标记前移一格
    destroy(_M_finish); // 析构尾端元素对象
    return __position;  // 返回擦除元素的位置对应迭代器
  }

  // 擦除指定区间[first, last)的所有元素
  iterator erase(iterator __first, iterator __last) {
    iterator __i = copy(__last, _M_finish, __first); // 将源区间[last, finish)所有元素前移, 拷贝到目标区间[first, ...), 返回目标区间结尾位置
    destroy(__i, _M_finish); // 销毁拷贝后残余的对象[i, finish)
    _M_finish = _M_finish - (__last - __first); // 尾端标记finish前移(last-first)格, 即已擦除元素个数
    return __first;
  }

注意:vector没有像list那样的remove移除元素接口。

清除所有元素clear

  // 擦除所有元素, 如果是non-trivial type, 会对每个元素调用析构函数
  void clear() { erase(begin(), end()); }

vector比较操作

SGI STL支持多种vector判断操作,不过并不是作为vector member function,而是作为global function。这样做,可以不用破坏容器的封装性。
根据《Effective C++》Item19,operator>可以转换为由operator<实现,operator!=可以转换为由operator<实现。因此,只需要实现operator==和operator<即可。
参考:https://www.cnblogs.com/fortunely/p/15715265.html

operator==

operator==用于比较两个vector是否相等。

// 比较两个vector的所有元素是否相等, 相等性通过equal判断
// 相等的两个vector, 要求长度相等, 并且所有元素都相等(通过元素的 "!=" 判断)
template <class _Tp, class _Alloc>
inline bool
operator==(const vector<_Tp, _Alloc>& __x, const vector<_Tp, _Alloc>& __y)
{
  return __x.size() == __y.size() &&
         equal(__x.begin(), __x.end(), __y.begin());
}

operator<

operator<用于判断第一个vector是否小于第二个。

// 字典序比较两个vector的所有元素, 判断第一个vector是否小于第二个
template <class _Tp, class _Alloc>
inline bool
operator<(const vector<_Tp, _Alloc>& __x, const vector<_Tp, _Alloc>& __y)
{
  return lexicographical_compare(__x.begin(), __x.end(),
                                 __y.begin(), __y.end());
}

vector特殊操作swap

swap()用于交换2个vector,代价很小,只需要交换底层线性空间的3个指针。

  // 与x交换当前vector交换, 只需要交换维护底层线性空间3个指针即可, 无需将vector所有元素交换或拷贝
  void swap(vector<_Tp, _Alloc>& __x) {
    __STD::swap(_M_start, __x._M_start);
    __STD::swap(_M_finish, __x._M_finish);
    __STD::swap(_M_end_of_storage, __x._M_end_of_storage);
  }

原文链接: https://www.cnblogs.com/fortunely/p/16245010.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    SGI STL顺序容器 vector

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/402600

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年4月21日 上午11:08
下一篇 2023年4月21日 上午11:08

相关推荐