STL(四):vector和array

本文详细介绍了STL中vector容器的工作原理,包括动态扩容机制、默认allocator如何分配内存以及vector的插入操作。在扩容时,vector通常会将容量翻倍以确保空间,这一策略在大多数情况下提高了效率。同时,插入操作可能导致迭代器失效,特别是当内存需要重新分配时。文章还对比了不同扩容策略的效率,并提到了静态数组array作为vector的补充,其大小在创建后不可变。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

STL的容器分为序列式容器和关联式容器,其中序列式容器的特点是每个元素均有特定的位置,这个位置和元素的值无关,下面我们介绍四种序列式容器。

vector

vector是一种支持动态扩容的数组,在SGI中,其继承关系如下:
在这里插入图片描述

这里与GNU的STL稍有不同,GNU中_vector_base包含一个名为_vector_impl的成员,这个成员实现了_M_allocate()和_M_deallocate(),而SGI中可以看到是直接在_Vector_base中实现的。
vector默认的allocator会为vector分配一段连续的内存空间,class _Vector_base中的_M_start指向这段内存的头部,_M_finish指向这段内存中已使用部分的尾部,_M_end_of_storage则指向内存结束的位置。也就是说,分配给vector的内存不一定全部被使用allocator给vector分配内存时,会留有一定的余量,我们可以调用vector的capacity方法查看内存总量,调用size()方法查看已使用的大小。

#include <bits/stdc++.h>

int main(){
    std::vector<int> nums;
    for(int i=0;i<5;++i){
        nums.emplace_back(i);
    }
    std::cout << "size:" << nums.size() << ",capacity:" << nums.capacity() << std::endl;
	return 0;
}

运行结果如下:
在这里插入图片描述

vector扩容机制

上面的运行结果中展示了目前这块内存总大小是8,假设我们向vector中插入9个元素,capacity会变成多少?我们运行下面的代码:

#include <bits/stdc++.h>

int main(){
    std::vector<int> nums;
    for(int i=0;i<9;++i){
        nums.emplace_back(i);
    }
    std::cout << "size:" << nums.size() << ",capacity:" << nums.capacity() << std::endl;
	return 0;
}

在这里插入图片描述
capacity变成了以前的两倍,需要说明的是我的测试环境是在Linux下的GCC 11.1.0版本,如果在其他平台下可能会出现不同的情况。这说明当vector内存大小不足时,vector扩大了之前的内存空间,变为了之前的两倍(VC++是1.5倍)。理想状态下,vector可以保持原来的内存空间不变,直接在后面增加一段相同大小的内存空间,使得容量变为两倍,但如果后面的内存空间长度不够了,是不是就无法扩容了呢。这里可以做个实验,我们打印第0个元素的地址空间,看看是否发生了变化。

#include <bits/stdc++.h>

int main(){
    std::vector<int> nums;
    nums.emplace_back(0);
    for(int i=1;i<9;++i){
        nums.emplace_back(i);
        std::cout << &nums[0] << std::endl;
    }
	return 0;
}

运行结果:
在这里插入图片描述
可以发现在插入第5个和第9个元素的时候,第一个元素的地址发生了变化,而对应的内存大小4和8,正好是需要扩容的内存长度,因此可以得出结论,vector在扩容时,会寻找一段能容纳两倍当前容量大小的内存,将原来的元素复制到新的内存中,下面是尾部插入元素时的代码实现:

 void push_back(const _Tp& __x) {
    if (_M_finish != _M_end_of_storage) { // 有备用空间
      construct(_M_finish, __x);    // 全局函数,将 __x 设定到 _M_finish 指针所指的空间上
      ++_M_finish;         // 调整
    }
    else
      _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;
  }
}

vector还有Insert()方法可以在中间插入元素,其原理和在尾部插入大致相同。

  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;
  }

迭代器失效

对vector的操作要特别考虑iterator是否会失效的问题。我们前面说过,iterator可以看成是进化的指针,当内存中的元素发生变化,iterator原本指向的地址可能就失效了。对于中间插入操作,由于会导致插入节点后续的元素都向后移动,因此导致插入节点后的iterator指向的元素不再是原先的元素,全部失效。
在这里插入图片描述
对vector的插入操作,不论是在哪里插入,都可能会导致vector扩容,扩容由于需要重新寻找一块内存,原先的迭代器将全部失效。

vector扩容大小

前面的实验中我们验证了LInux下扩容大小是原先的2倍,但为什么是两倍呢?为什么没有使用每次增加一个固定容量大小或者使用3倍、4倍呢?
首先我们来比较下成倍扩容和固定量扩容的区别,假设需要插入n个元素,每次插入k个,那么复制的总次数为:
在这里插入图片描述
总操作数除以总元素个数n,得到平均插入每个元素需要复制的次数是o(n)级别的。然后我在看成倍扩容的情况,假设需要插入N=k^n个元素,n为任意正整数。由于给一个大小为nk的数组需要复制nk次,那么复制的总次数为:
在这里插入图片描述
除以总元素个数k^n,得到下面的平均插入每个元素操作数,是O(1)级别,这显然比固定容量扩容效率高。
在这里插入图片描述
显然上图中的表达式在k=2时取到极小值,STL正是考虑到这一点,因此采取了2倍扩容的策略。

array

array是由c++提供的容器。并不在SGI的STL中提供,与vector的不同点是array是静态数组,确定大小后不可改变,其余用法与vector相同。

SGI STL源码

vector相关代码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值