STL 源码剖析之 vector

实现

vector 中有三个指针:指向使用空间的头(start)和尾(finish),以及可用空间的尾(end_of_storage

可用空间:为了降低空间配置的速度成本,vector 实际配置的大小可能比客户需求的大一些(即capacity >= size

默认的空间分配器是alloc


源码

vector 的数据(三个迭代器)是存储在栈上的,迭代器指向的数组是存放在堆上的


为什么不能把数据放到栈上

  • 栈的空间宝贵且有限,不能无限存放元素
  • 栈中数据的生命周期和函数挂钩

为什么 end 要在最后一个元素再后面的一个位置

  • 当没有元素的时候,begin 和 end 指向一起,方便判空

迭代器

vector 的迭代器本质上就是一个指针,指向元素T

  • 迭代器也可以用 [] 运算符进行访问:b[i] 等价于*(b + i)



基本用法

构造

std::vector<int> vec0(2);

std::vector<int> vec1(3, 333);

std::vector<int> vec2{111, 222, 333};

//    拷贝构造(深拷贝)
std::vector<int> vec3(vec2.begin(), vec2.end());
std::vector<int> vec4(vec3);
std::vector<int> vec5 = vec3;

//    移动构造
std::vector<int> vec6(10, 100);
std::vector<int> vec7 = std::move(vec6);
std::cout << "vec6.size() = " << vec6.size() << std::endl;    //    0

迭代器

std::vector<int> vec;

//    begin 返回容器第一个元素的迭代器,end 返回指向容器尾端的迭代器
//    当容器没有元素时,begin 和 end 相等
for (auto iter = vec.begin(); iter != vec.end(); ++ iter) {}
for (const auto iter = vec.begin(); iter != vec.end(); ++ iter) {}

//    rbegin 返回一个容器最后一个元素的反向迭代器,rend 返回一个容器前端的反向迭代器
//    当反向迭代器 +1 时,会往前面移动
for (auto iter = vec.rbegin(); iter != vec.rend(); ++ iter) {}

//    cbegin 返回容器第一个元素的迭代器,cend 返回指向容器尾端的迭代器
//    与 begin 和 end 不同的是,cbegin 为 const 类型,
//    而 begin 会根据返回值特化为 const 和非 const 类型
for (auto iter = vec.cbegin(); iter != vec.cend(); ++ iter) {}

常用函数

std::vector<int> vec(10, 20);

//    返回容器第一个元素
std::cout << vec.front() << std::endl;

//    返回容器最后一个元素
std::cout << vec.back() << std::endl;

//    容器为空则返回 true,否则返回 false
std::cout << vec.empty() << std::endl;

//    返回容器元素的个数
std::cout << vec.size() << std::endl;

//    返回容器的容量
std::cout << vec.capacity() << std::endl;

//    返回容器可容纳的元素最大数量,可以理解是无穷大,具体与平台实现有关
std::cout << vec.max_size() << std::endl;

//    返回容器首元素的数据指针
int *p = vec.data();

shrink_to_fit

将 capacity 减小为指定大小(使得已有迭代器失效

std::vector<int> vec(10, 0);
vec.push_back(1);

//    20
std::cout << vec.capacity() << std::endl;
auto iter = vec.begin();

vec.shrink_to_fit();

//    11
std::cout << vec.capacity() << std::endl;

//    UB, 概率会 coredump
std::cout << *iter << std::endl;

pop_back

删除容器最后一个元素(size 变小,capacity 不变)

std::vector<int> vec(10, 0);
vec.pop_back();

//    10
std::cout << f.capacity() << std::endl;

//    9
std::cout << f.size() << std::endl;

assign

替换容器的元素

std::vector<int> vec(10, 100);
vec.assign(5, 1000);

//    可以用迭代器或者 initializer_list
//    vec.assign(vec1.begin(), vec1.end()); 
//    vec.assign({'1', '2', '3'});

//  1000 1000 1000 1000 1000
for (const auto& iter : vec) {
  std::cout << iter << " ";
}
std::cout << std::endl;

insert

在指定位置添加元素

std::vector<int> vec(10, 1000);

// 在 begin 的位置添加一个 10 的元素
vec.insert(vec.begin(), 10);
// 在 begin 的位置添加 10 个值为 10 的元素
vec.insert(vec.begin(), 10, 10);

// 将另一个容器的元素插入到指定位置
std::vector<int> vec1(10, 1111);
vec.insert(vec.begin() + 2, vec1.begin(), vec1.end()); 

emplace

将元素插入到指定位置(简化版 insert)

std::vector<int> vec(10, 1000);

// 1000 1000 1000 1000 1000 1000 1000 1000 1000 1000
for (const auto &iter : vec) {
  std::cout << iter << " ";
}
std::cout << std::endl;

vec.emplace(vec.begin() + 6, 66);

// 1000 1000 1000 1000 1000 1000 66 1000 1000 1000 1000
for (const auto &iter : vec) {
  std::cout << iter << " ";
}
std::cout << std::endl;

swap

交换两个容器的内容(需要注意入参为 vector& other

std::vector<int> vec(10, 0);

std::vector<int>{}.swap(vec);
//    vec.swap(std::vector<int>{});    //    error, do not support right value

//  0
std::cout << "vec size is " << vec.size() << std::endl;
//  0
std::cout << "vec capacity is " << vec.capacity() << std::endl;

函数 clear 只会析构容器的元素,但是不会释放容器使用的空间,因此可以用函数 swap 和临时的空对象交换,实现内存空间的释放


erase

删除指定迭代器的内容(size 会变,capacity 不变)

std::vector<int> vec(10, 20);

//    删除指定位置的元素
vec.erase(vec.begin());

//    删除从 first 开始到 last 位置的元素
vec.erase(vec.begin() + 2, vec.begin() + 5);

实际上,当指定位置的元素被删除了之后,会被后面的元素依次的往前挪一个位置,因此返回的迭代器还是需要被删除的迭代器,但是指向的值不一样了

//    删除 vector<int> 中所有等于 2 的值
std::vector<int> vec{0, 1, 2, 2, 2, 2, 2, 3, 4};
for (auto iter = vec.begin(); iter != vec.end();) {
  if (2 == *iter) {
    vec.erase(iter);
  } else {
    ++iter;
  }
}

//  0 1 3 4
for (const auto &iter : vec) {
  std::cout << iter << " ";
}
std::cout << std::endl;

//    一种更为简单的实现
//    先将为 2 的 value 都挪到容器尾部,然后 erase
auto checkIs2 = [](const auto& x) { return x == 2; }; 
vec.erase(std::remove_if(vec.begin(), vec.end(), checkIs2), vec.end()); 

clear

清空容器中所有的元素。只改变 size,不改变 capacity

std::vector<int> vec(10, 20);

vec.clear();

//    0
std::cout << vec.size() << std::endl;

//    10
std::cout << vec.capacity() << std::endl;

根据数据类型的不同,时间复杂度也会不同

  • 如果容器中是 POD 或基本数据类型,那由于该元素没有析构函数,加之 vector 内部连续存储的特性,编译器的实现是可以在 O(1) 完成的
  • 而如果是自定义数据,又或者 list 或 vector 等类型,就要逐个去析构,时间复杂度就会到 O(n)

resize 和 reserve

resize:将容器的 size 修改为指定大小(capacity 只会变大或不变,不会变小

reserve:将容器的 capacity 修改为指定大小(size 不变

{
  std::vector<int> vec(10, 0);
  //    10
  std::cout << "before vec capacity = " << vec.capacity() << std::endl;
  //    20 > capacity,因此 size 和 capacity 一起变为 20
  vec.resize(20);

  //    20
  std::cout << "after vec capacity = " << vec.capacity() << std::endl;
  //    20
  std::cout << "after vec size = " << vec.size() << std::endl;
}

{
  std::vector<int> vec(40, 0);
  //    40
  std::cout << "before vec capacity = " << vec.capacity() << std::endl;
  //    20 < capacity, 因此 size 变为 20,capacity 不变
  vec.resize(20);

  //    40
  std::cout << "after vec capacity = " << vec.capacity() << std::endl;
  //    20
  std::cout << "after vec size = " << vec.size() << std::endl;
}

{
  std::vector<int> vec(10, 20);

  vec.reserve(10000);

  std::cout << vec.at(9) << std::endl;    //    yes

  std::cout << vec.at(999) << std::endl;    //    error
}

push_back 和 emplace_back

将元素放到容器尾端

#include <iostream>
#include <vector>

class StructA {
public:
  StructA() { std::cout << "StructA" << std::endl; }
  StructA(const StructA &other) { std::cout << "StructA copy" << std::endl; }
  StructA(const StructA &&other) { std::cout << "StructA move" << std::endl; }
  StructA(int age) { std::cout << "StructA age" << std::endl; }
};

int main() {
  {
    std::cout << "------- Begin push_back for left value -------" << std::endl;
    std::vector<StructA> vec;
    StructA tt;
    vec.push_back(tt); //  有参构造 + 拷贝构造
  }

  {
    std::cout << "------- Begin emplace_back for left value -------"
              << std::endl;
    std::vector<StructA> vec;
    StructA tt;
    vec.emplace_back(tt); //  有参构造 + 拷贝构造
  }

  {
    std::cout << "------- Begin push_back for temp value -------" << std::endl;
    std::vector<StructA> vec;
    vec.push_back(StructA(12)); //  有参构造 + 移动构造(没有移动,就会调用拷贝)
  }

  {
    std::cout << "------- Begin emplace_back for temp value -------"
              << std::endl;
    std::vector<StructA> vec;
    vec.emplace_back(StructA(12)); //  有参构造 + 移动构造(没有移动,就会调用拷贝)
  }

  {
    std::cout << "------- Begin push_back for value -------" << std::endl;
    std::vector<StructA> vec;
    vec.push_back(12); //  有参构造 + 移动构造(没有移动,就会调用拷贝)
  }

  {
    std::cout << "------- Begin emplace_back for value -------" << std::endl;
    std::vector<StructA> vec;
    vec.emplace_back(12); //  有参构造
  }

  {
    std::vector<std::vector<int>> vec;

    vec.emplace_back(std::vector<int>{1, 2});
    vec.push_back(std::vector<int>{1, 2});

    vec.emplace_back(std::initializer_list<int>{1, 2});
    vec.push_back(std::initializer_list<int>{1, 2});

    auto temp = {1, 2};
    vec.emplace_back(temp);
    vec.push_back(temp);

    vec.push_back({1, 2, 3, 4, 5, 6, 7});
    //    vec.emplace_back({1, 2, 3, 4, 5, 6, 7}); //  error

    // vec.push_back(1, 2);  //  error
    vec.emplace_back(1);
    vec.emplace_back(1, 2);
  }
}

总结

对于左值,都是调用拷贝构造

对于右值,如果是已经构造好了的,都是调用移动或者拷贝构造(优先调用移动构造)

  • 如果是没构造好的(需要隐式转换),push_back 是先构造,然后再移动或者拷贝构造(优先调用移动构造);emplace_back 是直接构造

push_back 的入参是 value_type,即已经指明了需要接受的参数类型(并且只能有一个入参),因此即使传入的并非 value_type 的类型,也会做隐式转换

emplace_back 的入参是 class... _Args,没有指明接受的参数类型和个数,同时不会为入参做隐式转换(需要使用者指明入参的类型)

  • 因此,emplace_back 希望接受 std::initializer_list 或者能够构造出 std::vector 的入参(因为这二者可以构成 value_type),而入参 {1, 2, 3, 4, 5, 6, 7} 希望函数的入参能隐式的把它转换为上述两种类型。结果就有点类似死锁了,编译报错
  • 特别的,当传入 (1, 2) 或者 (1)的时候,可以隐式的转换构造出 std::vector

at 和 []

访问容器下标的元素

std::vector<int> vec{1, 2, 3};

vec.reserve(11111);

//    11111
std::cout << vec.capacity() << std::endl;

//    3
std::cout << vec.size() << std::endl;

//    error
std::cout << vec.at(3) << std::endl;

//    yes
std::cout << vec[3] << std::endl;

[] 的越界访问是不会报错(每次访问都需要判断越界,对性能损耗大)

at 的越界访问是会报错的(会检查越界,以 size 为准,而不是 capacity)




扩容

为什么不是等差扩容

如果是等差扩容

假定每次扩容 m 个元素,总的元素个数是 n,则需要扩容 n/m

\sum^{n/m}_{i = 1}m*i=\frac{ (n+m) * n } { 2 * m}\\

扩容的时间复杂度平均到每个元素上就是 O(n)


如果是成倍扩容

假定有 n 个元素,倍增因子为 m

那么完成这 n 个元素进行 push_back 操作,需要重新分配内存的次数大约为 logm(n)

i 次重新分配将会导致复制 m^i(也就是当前的 vector 大小)个旧空间中元素

因此 npush_back 操作所花费的总时间约为 n*m/(m - 1)

扩容的时间复杂度平均到每个元素上就是O(1)(发现 m 为 2 时,时间复杂度最小,所以一般是 2 倍扩容)



为什么是 2 倍或者 1.5 倍扩容

理想的分配方案:是在第 n 次扩容时能复用之前 n-1 次释放的空间

  • 而当 m=2 的时候每次扩容的大小都会大于前面释放掉的所有的空间
  • 按照小于 2 倍方式扩容,多次扩容之后就可以复用之前释放的空间了
  • 而超过 2 倍,就会导致空间的浪费,并且无法完美的使用到前面已经释放掉的内存

总结

  • 使用 2 倍扩容时,每次扩容后的新内存大小必定大于前面的总和
  • 使用 1.5 倍扩容时,在数次扩容后,就可以重用之前的内存空间

 上一篇
STL 源码剖析之 deque STL 源码剖析之 deque
定义是一种双向开口的连续线性空间,可以在头尾两端分别做元素的插入和删除 允许O(1)时间内对头端进行元素的插入或移除操作 没有容量的概念,因为它是以动态的分段连续空间合成,随时可以增加一段新的空间并连接起来 一旦有必要在deque的前
2022-10-21
下一篇 
Deep into DynamicProgming Deep into DynamicProgming
路径问题(Path Problem)62、不同路径二维数组记录状态,通过滚动数组优化可以变为一维数组 class Solution { public: int uniquePaths(int m, int n) {
2022-10-10
  目录