按需分配的vector为什么要二倍扩容

91 阅读3分钟

静态数组是由相同类型的元素线性排列的数据结构,在计算机上会分配一段连续的内存,对元素进行顺序存储。

其中有三个关键词,相同类型、连续内存、顺序存储。之所以这样设计,本质就是为了能做到基于下标,对数组进行 O(1) 时间复杂度的快速随机访问。

存储数组时,会事先分配一段连续的内存空间,将数组元素依次存入内存。因为数组元素的类型都是一样的,所以每个元素占用的空间大小也是一样的,这样我们就很容易用“数组的开始地址 +index* 元素大小”的计算方式,快速定位到指定索引位置的元素,这也是数组基于下标随机访问的复杂度为 O(1) 的原因。

为什么要事先分配一段内存呢?答案也很简单,因为内存空间并不是无限的。一段程序里可能有很多地方都需要分配内存,我们必然要为分配的连续内存寻找一个边界。

STL 扩容逻辑的实现

   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);
   }
   
 template <class _Tp, class _Alloc>
   void 
   vector<_Tp, _Alloc>::_M_insert_aux(iterator __position, const _Tp& __x)
   {
     if (_M_finish != _M_end_of_storage) {
       construct(_M_finish, *(_M_finish - 1));
       ++_M_finish;
       _Tp __x_copy = __x;
       copy_backward(__position, _M_finish - 2, _M_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 = _M_allocate(__len);
       iterator __new_finish = __new_start;
       __STL_TRY {
         __new_finish = uninitialized_copy(_M_start, __position, __new_start);
         construct(__new_finish, __x);
         ++__new_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;
     }
   }
  1. push_back

    • 这个函数旨在在向量的末尾插入一个元素。
    • 它首先检查是否有可用的内存空间(_M_finish != _M_end_of_storage)。如果有,它会在末尾(_M_finish)构建元素,然后增加_M_finish
    • 如果没有剩余空间,它会调用_M_insert_aux函数。
  2. _M_insert_aux

    • 这是一个辅助函数,设计用于处理没有空间时的插入。

    • 首先,它检查是否有可用的内存空间。如果有,它会在末尾构建最后一个元素(相当于复制),然后将所有元素向右移动(从您想要插入的位置到倒数第二个元素)。最后,元素__x被复制到所需位置。

    • 如果没有空间,它会进行以下操作:

      1. 将向量的大小加倍(除非当前大小为0,在这种情况下,它只是将新大小设置为1)。
      2. 为这个新大小分配内存。
      3. 将元素从旧存储复制到新存储,直到所需位置。
      4. 插入新元素。
      5. 复制其余的元素。
      6. 在上述操作期间发生异常(由__STL_TRY__STL_UNWIND宏表示),它会清理新存储并释放内存。
      7. 如果一切正常,它会销毁旧存储,释放旧内存,并更新向量的起始、结束和存储末尾指针。

数组,是支持 O(1) 基于下标随机访问的数据结构,在内存中是连续存储的。基于下标高效访问元素的核心就在于“相同类型”和“连续存储”的特性,当然,也带来了高昂的插入和删除的时间复杂度。动态数组之所以能看起来像是无限容量,也仅仅是因为它内置了倍增的扩容策略,每次数组大小超过容量的时候,就会触发数组的扩容机制,封装了繁琐的拷贝细节。