std::vector 源码分析

从源码视角观察 STL 设计,代码实现为 libstdc++(GCC 4.8.5).

由于只关注 vector 的实现,并且 vector 实现几乎全部在头文件中,可以用一个这样的方法里获取比较清爽的源码

  1. // main.cpp
  2. #include <vector>
  3. int main() {
  4. std::vector<int> v;
  5. v.emplace_back(1);
  6. }

g++ -E main.cpp -std=c++11 > vector.cpp

在 vscode 中打开 vector.cpp 使用正则 "#.*\n" 把所以编译器相关的行删除,这样再进行格式化,就可以把预编译指令全部过滤了,而且不依赖外部的实现,跳转也没有压力

allocator

对于一个 allocator 需要实现的 trait,至少需要

  • allocate 内存的分配
  • deallocate 内存的回收

allocator 分配的最小粒度为对象,故要增加一个最大分配的数量

  • max_size 最大分配数量

以上是实现一个分配器的最基础功能。在此基础上,扩展对象的构造和析构,对于需要使用分配器的地方比如 STL,容器自身就不用再关注对象的构造和析构的内存相关功能了。

  • construct 对象构造,意味着需要使用模版实现,通用化
  • destroy 对象销毁

综上,实现 allocator 具有的 alloc_traits 如下:

  • allocate 分配
  • deallocate 回收
  • construct 对象构造,意味着需要使用模版实现,通用化
  • destroy 对象销毁
  • max_size 最大分配数量

std::allocator

标准库的分配器实现比较简单,分配和回收使用 ::operator new/delete

  1. pointer allocate(size_type __n, const void * = 0) {
  2. if (__n > this->max_size())
  3. std::__throw_bad_alloc();
  4. return static_cast<_Tp *>(::operator new(__n * sizeof(_Tp)));
  5. }
  6. void deallocate(pointer __p, size_type) { ::operator delete(__p); }

对于最大分配数量,整个进程空间(虚拟)都可以进行分配

  1. // sizeof(size_t) = 进程地址宽度
  2. size_type max_size() const throw() { return size_t(-1) / sizeof(_Tp); }

对于对象的构造和析构,则使用布置构造和析构函数

  1. void construct(pointer __p, const _Tp &__val) {
  2. ::new ((void *)__p) _Tp(__val);
  3. }
  4. void destroy(pointer __p) { __p->~_Tp(); }

std::vector

通用顺序容器,支持自定义内存分配器;

基础实现

libstdc++ 对 vector 的定义如下,里面提供了:

  1. template <typename _Tp, typename _Alloc = std::allocator<_Tp>>
  2. class vector : protected _Vector_base<_Tp, _Alloc> {};

两个模版参数:一个容器内的元素类型,一个分配器类型,并且分配器类型不是必须参数。

使用 protected 继承 _Vector_base,不过这里并没有利用空基类优化(EBO), 更多的是做了类的隔离;

观察 _Vector_base 的实现,包含了一个 impl:

  1. template <typename _Tp, typename _Alloc> struct _Vector_base {
  2. typedef
  3. typename __gnu_cxx::__alloc_traits<_Alloc>::template rebind<_Tp>::other
  4. _Tp_alloc_type;
  5. typedef typename __gnu_cxx::__alloc_traits<_Tp_alloc_type>::pointer pointer;
  6. struct _Vector_impl : public _Tp_alloc_type {
  7. pointer _M_start;
  8. pointer _M_finish;
  9. pointer _M_end_of_storage;
  10. }
  11. public:
  12. _Vector_impl _M_impl;
  13. }

_Vector_base 提供了 vector 的对内存的操作,包括分配内存和释放,_Vector_impl public 继承 _Tp_alloc_type(默认为 std::allocator<_Tp1>),从 C++ 的语义上说 _Vector_impl 也可以叫做一个分配器(事实也是)。

_Vector_impl

_Vector_impl 实现比较简单,三个核心成员变量,作为 vector 的底层表达

  • _M_start 元素空间起始地址,data() 返回的地址
  • _M_finish 元空间结束地址, 和 size() 相关
  • _M_end_of_storage 元素可用空间结束地址,和 capacity() 相关
  1. struct _Vector_impl : public _Tp_alloc_type {
  2. pointer _M_start;
  3. pointer _M_finish;
  4. pointer _M_end_of_storage;
  5. _Vector_impl()
  6. : _Tp_alloc_type(), _M_start(0), _M_finish(0), _M_end_of_storage(0) {}
  7. _Vector_impl(_Tp_alloc_type const &__a)
  8. : _Tp_alloc_type(__a), _M_start(0), _M_finish(0),
  9. _M_end_of_storage(0) {}
  10. void _M_swap_data(_Vector_impl &__x) {
  11. std::swap(_M_start, __x._M_start);
  12. std::swap(_M_finish, __x._M_finish);
  13. std::swap(_M_end_of_storage, __x._M_end_of_storage);
  14. }
  15. };

_Vector_base

_Vector_impl 已经提供了底层存储的表达,_Vector_base 则为对底层表达的初始化,及屏蔽内存的实现并对上层提供申请/释放接口

  1. // 只选了一个构造函数展示
  2. _Vector_base(size_t __n) : _M_impl() { _M_create_storage(__n); }
  3. void _M_create_storage(size_t __n) {
  4. this->_M_impl._M_start = this->_M_allocate(__n);
  5. this->_M_impl._M_finish = this->_M_impl._M_start;
  6. this->_M_impl._M_end_of_storage = this->_M_impl._M_start + __n;
  7. }
  8. // 释放内存
  9. ~_Vector_base() {
  10. _M_deallocate(this->_M_impl._M_start,
  11. this->_M_impl._M_end_of_storage - this->_M_impl._M_start);
  12. }
  13. pointer _M_allocate(size_t __n) {
  14. return __n != 0 ? _M_impl.allocate(__n) : 0;
  15. }
  16. void _M_deallocate(pointer __p, size_t __n) {
  17. if (__p)
  18. _M_impl.deallocate(__p, __n);
  19. }

构造函数

拿了三个构造函数的实现来看,后面两者需要注意构造的时候就会有 size() 个复制的代价

L174 默认构造函数,除了基础的初始化什么都不做

L209 构造拥有 initializer_list init 内容的容器

L214 构造拥有范围 [first, last) 内容的容器

  1. 174 explicit vector(const allocator_type &__a) : _Base(__a) {}
  2. 209 vector(initializer_list<value_type> __l,
  3. 210 const allocator_type &__a = allocator_type())
  4. 211 : _Base(__a) {
  5. 212 _M_range_initialize(__l.begin(), __l.end(), random_access_iterator_tag());
  6. 213 }
  7. 214 template <typename _InputIterator,
  8. 215 typename = std::_RequireInputIter<_InputIterator>>
  9. 216 vector(_InputIterator __first, _InputIterator __last,
  10. 217 const allocator_type &__a = allocator_type())
  11. 218 : _Base(__a) {
  12. 219 _M_initialize_dispatch(__first, __last, __false_type());
  13. 220 }

方法

搞明白 std::vector 的底层实现,后面直接看提供的方法了,最基本的增删改查大小。

大小相关

size() 内部的元素个数,实现为

  1. size_type size() const {
  2. return size_type(this->_M_impl._M_finish - this->_M_impl._M_start);
  3. }

capacity() 可用空间的大小,实现为

  1. size_type capacity() const {
  2. return size_type(this->_M_impl._M_end_of_storage - this->_M_impl._M_start);
  3. }

push_back

push_back 是使用最频繁的方法,搞清楚它的实现,整个 vector 的变化策略都会比较清晰。

  1. 60 void push_back(const value_type &__x) {
  2. 61 if (this->_M_impl._M_finish != this->_M_impl._M_end_of_storage) {
  3. 62 _Alloc_traits::construct(this->_M_impl, this->_M_impl._M_finish, __x);
  4. 63 ++this->_M_impl._M_finish;
  5. 64 } else
  6. 65 _M_emplace_back_aux(__x);
  7. 66 }
  8. 67
  9. 68 void push_back(value_type &&__x) { emplace_back(std::move(__x)); }
  10. 85 template <typename _Tp, typename _Alloc>
  11. 86 template <typename... _Args>
  12. 87 void vector<_Tp, _Alloc>::emplace_back(_Args && ...__args) {
  13. 88 if (this->_M_impl._M_finish != this->_M_impl._M_end_of_storage) {
  14. 89 _Alloc_traits::construct(this->_M_impl, this->_M_impl._M_finish,
  15. 90 std::forward<_Args>(__args)...);
  16. 91 ++this->_M_impl._M_finish;
  17. 92 } else
  18. 93 _M_emplace_back_aux(std::forward<_Args>(__args)...);
  19. 94 }

push_back() 底层有使用 emplace_back(c++11) 优化的情况:

size() < capacity() 的情况下,直接在最后一个元素后的位置进行复制/移动构造,底层地址偏移+1.

size() == capacity() 的情况下,需要先申请一块新的内存后,再插入新的元素并且需要将之前的元素也移动至新的内存中,实现如下,忽略了异常处理和不需要的分支处理。

  1. 11 template <typename _Tp, typename _Alloc>
  2. 12 template <typename... _Args>
  3. 13 void vector<_Tp, _Alloc>::_M_emplace_back_aux(_Args && ...__args) {
  4. 14 const size_type __len =
  5. 15 _M_check_len(size_type(1), "vector::_M_emplace_back_aux");
  6. 16 pointer __new_start(this->_M_allocate(__len));
  7. 17 pointer __new_finish(__new_start);
  8. 19 _Alloc_traits::construct(this->_M_impl, __new_start + size(),
  9. 20 std::forward<_Args>(__args)...);
  10. 21 __new_finish = 0;
  11. 22 __new_finish = std::__uninitialized_move_if_noexcept_a(
  12. 23 this->_M_impl._M_start, this->_M_impl._M_finish, __new_start,
  13. 24 _M_get_Tp_allocator());
  14. 25 ++__new_finish;
  15. 26 std::_Destroy(this->_M_impl._M_start, this->_M_impl._M_finish,
  16. 27 _M_get_Tp_allocator());
  17. 28 _M_deallocate(this->_M_impl._M_start,
  18. 29 this->_M_impl._M_end_of_storage - this->_M_impl._M_start);
  19. 30 this->_M_impl._M_start = __new_start;
  20. 31 this->_M_impl._M_finish = __new_finish;
  21. 32 this->_M_impl._M_end_of_storage = __new_start + __len;
  22. 33 }

_M_check_len 校验是否有足够的空间进行分配,并且返回增长后的大小,实现如下

  1. size_type _M_check_len(size_type __n, const char *__s) const {
  2. if (max_size() - size() < __n)
  3. __throw_length_error((__s));
  4. const size_type __len = size() + std::max(size(), __n);
  5. return (__len < size() || __len > max_size()) ? max_size() : __len;
  6. }

可以得知,第一次 push_back 后,size() == capacity() == 1,第二次为2,后面依次 *2,最大为 size_t(-1)/sizeof(T).

L14 获取需要分配的的空间大小

L16 申请一块新的内存

L19 对新的元素进行构造

L22 对旧的元素,复制/移动构造至新的内存中

L26 对旧的元素进行析构

L28 对旧的空间进行释放

L30-L32 更新底层实现的索引

所以可以看到 vector 的底层实现一定是顺序表,可以在栈上(自己实现分配器)也可以在堆上(默认)。

关于扩容,增长因子为 2,并且有最大大小限制,还考虑了整数溢出的情况。

关于构造函数,每次插入都会有一个复制构造函数的调用

insert

插入元素到容器中的指定位置。

insert 和 push_back 实现差别不大,多了(size() - pos)次复制/移动构造函数

resize

改变容器中可存储元素的个数

这里只看默认初始化新元素值的实现

  1. 298 void resize(size_type __new_size) {
  2. 299 if (__new_size > size())
  3. 300 _M_default_append(__new_size - size());
  4. 301 else if (__new_size < size())
  5. 302 _M_erase_at_end(this->_M_impl._M_start + __new_size);
  6. 303 }
  7. 525 void _M_erase_at_end(pointer __pos) {
  8. 526 std::_Destroy(__pos, this->_M_impl._M_finish, _M_get_Tp_allocator());
  9. 527 this->_M_impl._M_finish = __pos;
  10. 528 }
  11. 408 void vector<_Tp, _Alloc>::_M_default_append(size_type __n) {
  12. 409 if (__n != 0) {
  13. 410 if (size_type(this->_M_impl._M_end_of_storage -
  14. 411 this->_M_impl._M_finish) >= __n) {
  15. 412 std::__uninitialized_default_n_a(this->_M_impl._M_finish, __n,
  16. 413 _M_get_Tp_allocator());
  17. 414 this->_M_impl._M_finish += __n;
  18. 415 } else {
  19. 416 const size_type __len = _M_check_len(__n, "vector::_M_default_append");
  20. 417 const size_type __old_size = this->size();
  21. 418 pointer __new_start(this->_M_allocate(__len));
  22. 419 pointer __new_finish(__new_start);
  23. 420 try {
  24. 421 __new_finish = std::__uninitialized_move_if_noexcept_a(
  25. 422 this->_M_impl._M_start, this->_M_impl._M_finish, __new_start,
  26. 423 _M_get_Tp_allocator());
  27. 424 std::__uninitialized_default_n_a(__new_finish, __n,
  28. 425 _M_get_Tp_allocator());
  29. 426 __new_finish += __n;
  30. 427 } catch (...) {
  31. 428 std::_Destroy(__new_start, __new_finish, _M_get_Tp_allocator());
  32. 429 _M_deallocate(__new_start, __len);
  33. 430 throw;
  34. 431 }
  35. 432 std::_Destroy(this->_M_impl._M_start, this->_M_impl._M_finish,
  36. 433 _M_get_Tp_allocator());
  37. 434 _M_deallocate(this->_M_impl._M_start,
  38. 435 this->_M_impl._M_end_of_storage - this->_M_impl._M_start);
  39. 436 this->_M_impl._M_start = __new_start;
  40. 437 this->_M_impl._M_finish = __new_finish;
  41. 438 this->_M_impl._M_end_of_storage = __new_start + __len;
  42. 439 }
  43. 440 }
  44. 441 }

resize 中也存在三种情况

当需要重置大小等于目前容器的大小时,忽略

当重置大小小于目前容器大小时,处理简单,释放内存,修改 finish 的值

当重置大小大于目前容器大小时:

  1. 当前重置小于等于容器的容量,直接在尾部以默认构造函数额外的元素
  2. 当重置的大小大于容器的容器,和push_back一样,需要先申请内存,再复制/移动元素,再重复1的步骤

    L416-L412 为申请新的内存,并且复制/移动元素

    L424 为在尾部以默认构造函数额外的元素

clear

清除容器内的元素,之后 size() = 0

实现较为简单

  1. 521 void clear() noexcept { _M_erase_at_end(this->_M_impl._M_start); }
  2. 525 void _M_erase_at_end(pointer __pos) {
  3. 526 std::_Destroy(__pos, this->_M_impl._M_finish, _M_get_Tp_allocator());
  4. 527 this->_M_impl._M_finish = __pos;
  5. 528 }

reserve

预留存储空间, 增加 vector 的容量到(大于或)等于 new_cap 的值.

实现也比较简单,new_cap 的值大于容器的容量时,进行重新分配,再复制/移动到新的内存中,最后更新底层数据结构

  1. 566 template <typename _Tp, typename _Alloc>
  2. 567 void vector<_Tp, _Alloc>::reserve(size_type __n) {
  3. 568 if (__n > this->max_size())
  4. 569 __throw_length_error(("vector::reserve"));
  5. 570 if (this->capacity() < __n) {
  6. 571 const size_type __old_size = size();
  7. 572 pointer __tmp = _M_allocate_and_copy(
  8. 573 __n, std::__make_move_if_noexcept_iterator(this->_M_impl._M_start),
  9. 574 std::__make_move_if_noexcept_iterator(this->_M_impl._M_finish));
  10. 575 std::_Destroy(this->_M_impl._M_start, this->_M_impl._M_finish,
  11. 576 _M_get_Tp_allocator());
  12. 577 _M_deallocate(this->_M_impl._M_start,
  13. 578 this->_M_impl._M_end_of_storage - this->_M_impl._M_start);
  14. 579 this->_M_impl._M_start = __tmp;
  15. 580 this->_M_impl._M_finish = __tmp + __old_size;
  16. 581 this->_M_impl._M_end_of_storage = this->_M_impl._M_start + __n;
  17. 582 }
  18. 583 }

shrink_to_fit

请求移除未使用的容量

  1. void shrink_to_fit() { _M_shrink_to_fit(); }
  2. template <typename _Tp, typename _Alloc>
  3. bool vector<_Tp, _Alloc>::_M_shrink_to_fit() {
  4. if (capacity() == size())
  5. return false;
  6. return std::__shrink_to_fit_aux<vector>::_S_do_it(*this);
  7. }
  8. template <typename _Tp> struct __shrink_to_fit_aux<_Tp, true> {
  9. _Tp(__make_move_if_noexcept_iterator(__c.begin()),
  10. __make_move_if_noexcept_iterator(__c.end()), __c.get_allocator())
  11. .swap(__c);
  12. return true;
  13. };

模板太多看起来费劲,换一种表达

  1. std::vector<int> v;
  2. v.push_back(1); // size()=1 capacity()=1
  3. v.push_back(1); // size()=2 capacity()=2
  4. v.push_back(1); // size()=3 capacity()=4
  5. std::vector<int>(v.begin(), v.end()).swap(v); // size()=3 capacity()=3

时间复杂度分析

复杂度 方法 说明
\(O(1)\) size() 变量相减
\(O(1)\) capacity() 变量相减
\(O(1)\) push_back() 均摊最坏情况为3
\(O(n)\) insert() 操作需要对size()-pos进行拷贝
\(O(n)\) clear() size() 次析构
\(O(n)\) reserve() 扩容需要size()次拷贝
\(O(n)\) shrink_to_fit() 构造需要size()拷贝,swap()为常数

push_back 复杂度证明

以libstdc++为准备,vector的增长因子为2,分析对一个空的 vector 执行 n 个 push_back 的复杂度。

第 \(i\) 个操作的需要的复制构造次数的 \(c_i\),分为两种情况:

  • size() < capacity(), \(c_i=1\)
  • size() == capacity(),vector 进行扩张,\(c_i=i\)

得到每次的次数为:

\[c_i=\left\{
\begin{aligned}
i, & 若 i-1 恰为 2 的幂 \\
1, & 其他
\end{aligned}
\right.
\]

n 个 push_back 总的复制构造函数的次数为

\[\sum_{i=1}^nc_i \le n + \sum_{j=0}^{\lfloor lgn \rfloor}2^j \le n+2n = 3n
\]

n个push_back的上界为 3n,单一的摊还次数为 3,所以复杂度为 \(O(1)\)

STL漫游之vector的更多相关文章

  1. 转:用STL中的vector动态开辟二维数组

    用STL中的vector动态开辟二维数组 源代码:#include <iostream>#include <vector>using namespace std;int mai ...

  2. STL中的Vector相关用法

    STL中的Vector相关用法 标准库vector类型使用需要的头文件:#include <vector>. vector 是一个类模板,不是一种数据类型,vector<int> ...

  3. (转)C++ STL中的vector的内存分配与释放

    C++ STL中的vector的内存分配与释放http://www.cnblogs.com/biyeymyhjob/archive/2012/09/12/2674004.html 1.vector的内 ...

  4. C++STL中的vector的简单实用

    [原创] 使用C++STL中的vector, #include <stdio.h> #include<stdlib.h> #include<vector> usin ...

  5. STL中的vector实现邻接表

    /* STL中的vector实现邻接表 2014-4-2 08:28:45 */ #include <iostream> #include <vector> #include  ...

  6. C/C++解题常用STL大礼包 含vector,map,set,queue(含优先队列) ,stack的常用用法

    每次忘记都去查,真难啊 /* C/C++解题常用STL大礼包 含vector,map,set,queue(含优先队列) ,stack的常用用法 */ /* vector常用用法 */ //头文件 #i ...

  7. [转] C++的STL库,vector sort排序时间复杂度 及常见容器比较

    http://www.169it.com/article/3215620760.html http://www.cnblogs.com/sharpfeng/archive/2012/09/18/269 ...

  8. Linux环境下stl库使用(vector)

    step1: #include <iostream> #include <vector> #include <string> using namespace std ...

  9. stl 中List vector deque区别

    stl提供了三个最基本的容器:vector,list,deque.         vector和built-in数组类似,它拥有一段连续的内存空间,并且起始地址不变,因此     它能非常好的支持随 ...

随机推荐

  1. Go 变量及基本数据类型1

    #### Go 变量及基本数据类型(一)今天主要学习一下Go 中的变量及基本数据类型: 如何申明,使用变量,以及基本数据类型的介绍和使用细节; ##### 变量的介绍1. 变量相当于内存中一个数据存储 ...

  2. Go 转义字符及风格

    今天来学习一下Go 中的转义字符,源码注释,规范的代码风格以及标准库API 文档; Go 转义字符常用的转义字符有以下几个:1. \t: 表示一个制表符(tab), 通常可以使用它进行排版; 2. \ ...

  3. 获取URL中的某段字符

    1. Location 对象 Location 对象包含有关当前 URL 的信息. Location 对象是 window 对象的一部分,可通过 window.Location 属性对其进行访问. ️ ...

  4. linux区分atime,ctime and mtime

  5. ApacheCN 计算机视觉译文集 20210218 更新

    新增了六个教程: OpenCV3 安卓应用编程 零.前言 一.设置 OpenCV 二.使用相机帧 三.应用图像效果 四.识别和跟踪图像 五.将图像跟踪与 3D 渲染相结合 六.通过 JNI 混合 Ja ...

  6. 「ZJOI2014」星系调查

    「ZJOI2014」星系调查 本题核心在于快速求XPs 的线性假设相斥度. 点\((x1,y1)\)到直线\(y=kx+b\)的距离的平方为\(\displaystyle {(kx1+b-y1)^2} ...

  7. JS RegExp对象(正则表达式)

    笔记整理自:廖雪峰老师的JS教程 正则表达式语法:https://www.runoob.com/regexp/regexp-tutorial.html 目录 创建方式 方式一 方式二 简单使用 判断正 ...

  8. spring学习四:Spring中的后置处理器BeanPostProcessor

    BeanPostProcessor接口作用: 如果我们想在Spring容器中完成bean实例化.配置以及其他初始化方法前后要添加一些自己逻辑处理.我们需要定义一个或多个BeanPostProcesso ...

  9. JVM收藏的文章

    JAVA 内存泄露详解(原因.例子及解决) https://blog.csdn.net/anxpp/article/details/51325838 JVM内存区域划分Eden Space.Survi ...

  10. k8s补充

    k8s补充 容器云发展及主要内容 1.云计算,交付标准(iaas--openstack) 国内:阿里云一华为云(振兴杯)百度云(私有云) 国外:AWS 2.平台即服务(PAAS) 例如:新浪云(号称免 ...