C++ STL: 容器vector源码分析
文章目录
- 前言
- vector的核心接口
- vector push_back实现
- vector 的 Allocator
- vector 的 push_back
- 总结
前言
vector 是我们C++STL中经常使用的一个容器,提供容量可以动态增长,并且随机访问内部元素的数据存储功能。
这个容器我们最常使用,所以也是最为熟悉的一种容器。通过它来看STL的源码设计,以及STL六大组件之间的搭配使用,可以让我们对C++这种语言,对优秀的编码设计,对有趣的数据结构和算法都能有深刻的理解和认识。
STL六大组件关系如下:
其中容器主要是 提供数据存储的功能。分配器用来给容器的数据存储分配空间,算法通过迭代器可以访问容器中的数据。仿函数提供类似于函数的功能,这里算法就是用仿函数来实现的。各个适配器给各个组件做一些功能上的补充。
今天主要对容器的vector
的实现进行深入的分析。
本节涉及的源代码是基于:gcc 2.95 版本的libstdc++ 标准库进行分析的。不同版本的代码实现方式可能有一些差异,但是核心思想应该是一样的。
gcc-2.95 源码下载
vector的核心接口
我们在使用vector的过程中从声明到使用的基本接口如下:
vecotor<int,std::alloctor<int>> arr(10,0);
arr.push_back(3);
arr.size();
arr.max_szie();
arr.back();
arr.pop_back();
arr.capacity();
arr.erase(3);
...
详细的就不一一列举了,这里我们主要关注的是push_back()插入的操作,因为只有元素的持续插入,vector的底层数据存储结构才会跟着进行变化,这也是它的核心接口了。
vector push_back实现
首先用一张图来表示vector的基本存储情况以及扩容的过程(其中的字段是源码部中的字段)
左侧的存储代表扩容之前的存储结构,右侧是扩容之后的存储结构。
扩容的过程是两倍的容量扩充,且不是在原来的基础上进行增长的,而是重新分配一块两倍于原来大小的内存空间,将原来的存储空间的元素一个一个拷贝到新的两倍大小的存储空间之中。
- _M_start 代表起始位置的指针
- _M_finish 代表已存储的元素的末尾位置
- _M_end_of_storage 代表整个vector空间的结束位置
- size ,即 我们使用的arr.size() ,表示vector实际存储的元素的个数
- capacity, 即我们使用的arr.capacity(),表示当前vector可以存储的元素个数(包括已分配空间,但未存储元素的空间区域)
具体实现的类图关系如下:
从上面的类图关系中我们能够看到最终vector对象的大小sizeof(vector<Tp>())
为24B(x86_64),三个指针类型的大小(_M_start, _M_finish,_M_end_of_storage)
接下来我们来看一下具体的成员函数实现
vector 的 Allocator
vector类 通过函数模版,制定了基本数据类型,以及实例化了分配器的类型
template <class _Tp, class _Alloc = __STL_DEFAULT_ALLOCATOR(_Tp) >
class vector : protected _Vector_base<_Tp, _Alloc>
{private:typedef _Vector_base<_Tp, _Alloc> _Base;
public:typedef _Tp value_type;typedef value_type* pointer;typedef const value_type* const_pointer;typedef value_type* iterator;typedef const value_type* const_iterator;typedef value_type& reference;typedef const value_type& const_reference;typedef size_t size_type;typedef ptrdiff_t difference_type;...
这里我们再追踪一下空间分配器 Allocator是如何参与到vector容器之中的。
可以很明显得看到这里分配器使用的是stl默认的分配器 allocator
,即
define __STL_DEFAULT_ALLOCATOR(T) allocator<T>
allocator的声明如下,这里主要看一下stl分配器中的allocate
和deallocate
函数,他们如何进行空间分配
template <class _Tp>
class allocator {typedef alloc _Alloc; // The underlying allocator.
public:typedef size_t size_type;typedef ptrdiff_t difference_type;typedef _Tp* pointer;typedef const _Tp* const_pointer;typedef _Tp& reference;typedef const _Tp& const_reference;typedef _Tp value_type;......// __n is permitted to be 0. The C++ standard says nothing about what// the return value is when __n == 0.// 传入要分配的个数n,并调用_Alloc的allocate函数进行空间的分配// 分配的总大小是n * sizeof(_Tp)_Tp* allocate(size_type __n, const void* = 0) {return __n != 0 ? static_cast<_Tp*>(_Alloc::allocate(__n * sizeof(_Tp))): 0;}......
_Alloc的allocate最终会调用malloc_allc::allocate函数,再通过malloc分配空间。
static void* allocate(size_t __n)
{_Obj* __VOLATILE* __my_free_list;_Obj* __RESTRICT __result;//如果一次分配的空间大于128B,则会直调用malloc进行空间的申请if (__n > (size_t) _MAX_BYTES) {return(malloc_alloc::allocate(__n));}// 如果不足128B,从空闲链表中取出空间__my_free_list = _S_free_list + _S_freelist_index(__n);// Acquire the lock here with a constructor call.// This ensures that it is released in exit or during stack// unwinding.
# ifndef _NOTHREADS/*REFERENCED*/_Lock __lock_instance;
# endif__result = *__my_free_list;if (__result == 0) {void* __r = _S_refill(_S_round_up(__n));return __r;}*__my_free_list = __result -> _M_free_list_link;return (__result);
};static void* allocate(size_t __n)
{void* __result = malloc(__n);if (0 == __result) __result = _S_oom_malloc(__n);return __result;
}
同样allocator的deallocate函数实现如下,最终也会调用到free进行空间的释放
//allocator 的deallocate函数
void deallocate(pointer __p, size_type __n){ _Alloc::deallocate(__p, __n * sizeof(_Tp)); }//_Alloc的deallocate函数
static void deallocate(void* __p, size_t __n)
{_Obj* __q = (_Obj*)__p;_Obj* __VOLATILE* __my_free_list;if (__n > (size_t) _MAX_BYTES) {malloc_alloc::deallocate(__p, __n);return;}__my_free_list = _S_free_list + _S_freelist_index(__n);// acquire lock
# ifndef _NOTHREADS/*REFERENCED*/_Lock __lock_instance;
# endif /* _NOTHREADS */__q -> _M_free_list_link = *__my_free_list;*__my_free_list = __q;// lock is released here
}// malloc_alloc的deallocate函数
static void deallocate(void* __p, size_t /* __n */)
{free(__p);
}
到此我们就基本清楚了当前版本的vector容器是如何得到具体的存储空间的。
接下来我们继续回到vector的实现之中。
vector 的 push_back
vector的push_back 逻辑有两种
- 当vector的内部的finish指针并没有达到end_of_storage指针,即没有达到我们理解的容量的上限的时候,会构造一个_Tp的对象加入到finish指针区域,同时finish指针增加。
- 当finish指针到达end_of_storage指针的位置的时候需要重新进行分配。
分配的过程是 重新分配一块原来存储空间两倍大小的空间,将原来的元素拷贝到新的存储空间。
接下来看一下具体的过程,push_back过程如下
void push_back(const _Tp& __x) {//finish指针 没有到达end的位置,表示还有空间可用if (_M_finish != _M_end_of_storage) {construct(_M_finish, __x);++_M_finish;}else//否则,开始重新分配_M_insert_aux(end(), __x);
}
调用了_M_insert_aux函数,在空间不足时进行重新分配。
实现过程如下。
这里我们能看到 函数开头又重新写了是否达到容量上限的判断逻辑。
因为这个函数除了被push_back调用,还会被迭代器的insert调用,导致当前线程想要分配的时候空间已经扩容好了,所以还需要再增加一次空间是否足够的判断。
同时扩容的过程中调用了两次拷贝的过程,也是因为insert的函数,在指定的位置插入元素,可能也会造成vector的扩容,所以插入的位置前后都需要拷贝到新的存储空间之中。
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;/*try...catch子句,进行异常捕获,如果这个过程失败了,会回滚已经分配好了的空间*/__STL_TRY {/* 将原来的vector内容拷贝到当前vector:*/__new_finish = uninitialized_copy(_M_start, __position, __new_start);construct(__new_finish, __x); // 为新的元素设置初始值,添加到vector末尾++__new_finish; // 调整水位// 这里需要再次进行一次拷贝,是因为当前函数可能还会被insert(p,x)调用// insert表示在第p个位置插入x元素,这个操作可能也会造成数组的扩容// 所以需要将安插点之后的数据拷贝到当前位置__new_finish = uninitialized_copy(__position, _M_finish, __new_finish); }/*catch*/__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的扩容过程的实现,我们能看到vector的扩容会有一些隐患:
- 当扩容产生,将旧的空间的元素拷贝到新的空间的元素会触发 临时对象的构造和 拷贝构造函数,此时如果元素体量较为庞大,那大量的新对象的构造和拷贝构造函数调用 会产生一点的性能开销。
- 新的空间和元素都拷贝过去之后,旧的空闲也需要释放,此时又会有大量的析构调用来进行空间的释放。
验证代码如下:
#include <iostream>
#include <vector>
#include <unistd.h>using namespace std;class Test {private:int num;public:Test(int x):num(x){std::cout << "constructed\n";}/*拷贝构造函数*/Test(const Test &obj) {num = obj.num;std::cout << "copy constructed\n";}/*C++11 支持的 转移构造函数*/Test (Test &&obj) {num = std::move(obj.num);std::cout << "moved constructed\n";}};int main() {vector<Test> arr;std::cout << "push_back : Test(1)\n";arr.push_back(1);return 0;
}
push_back : Test(1)
constructed
moved constructed
怎么解决呢?
这就是我们C++11在这一方面推出的性能方面的改进逻辑:
- push_back 将拷贝构造函数变更为 转移构造(std::move)函数,减少来空间释放的开销
- 推出emplace_back函数,使用变长模版参数forward进行对象在的原地构造,只需要一次对象本身的构造即可。
emplace_back
函数,详细的实现可以参考empalce_back和push_back的差异
vector
的emplace_back
实现如下:
vector<_Tp, _Allocator>::emplace_back(_Args&&... __args)
{if (this->__end_ < this->__end_cap()){__RAII_IncreaseAnnotator __annotator(*this);__alloc_traits::construct(this->__alloc(),_VSTD::__to_raw_pointer(this->__end_),_VSTD::forward<_Args>(__args)...);__annotator.__done();++this->__end_;}else__emplace_back_slow_path(_VSTD::forward<_Args>(__args)...);
#if _LIBCPP_STD_VER > 14return this->back();
#endif
}
可以看到STL的开发者们 不断得完善逻辑,改进性能。并且不断得推出新的特性来供给我们这一些语言的使用者来使用,如果我们还用不好,那是真的愧对大佬们的付出了。
精进自己,追求极致,向大佬们致敬。
C++ STL: 容器vector源码分析相关推荐
- Java集合之Vector源码分析
概述 Vector与ArrayLIst类似, 内部同样维护一个数组, Vector是线程安全的. 方法与ArrayList大体一致, 只是加上 synchronized 关键字, 保证线程安全, 下面 ...
- Java容器 | 基于源码分析List集合体系
一.容器之List集合 List集合体系应该是日常开发中最常用的API,而且通常是作为面试压轴问题(JVM.集合.并发),集合这块代码的整体设计也是融合很多编程思想,对于程序员来说具有很高的参考和借鉴 ...
- 【Spring】IOC:基于注解的IOC容器初始化源码分析
从 Spring2.0 以后的版本中,Spring 也引入了基于注解(Annotation)方式的配置,注解(Annotation)是 JDK1.5 中引入的一个新特性,用于简化 Bean 的配置,可 ...
- sheng的学习笔记-Vector源码分析
概述 Vector底层也是数组,跟ArrayList很像(先看下ArrayList,再看Vector会很轻松),ArrayList可参考下文,并且由于效率低,已经被淘汰了,大概瞅瞅得了 sheng的学 ...
- STL std::sort 源码分析
转载自http://feihu.me/blog/2014/sgi-std-sort/ 最近在看sort源码,看到这篇博文很好,转发作为记录,转载侵权联系我删除 背景 在校期间,为了掌握这些排序算法,我 ...
- C++ STL: 分配器allocators 源码分析
STL 基本的六大组件作用以及功能如下: 可以看到allocator是数据存储组件container的幕后支持者,负责为其数据存储分配对应的存储空间. operator::new 在详细介绍alloc ...
- Java容器 | 基于源码分析Map集合体系
一.容器之Map集合 集合体系的源码中,Map中的HashMap的设计堪称最经典,涉及数据结构.编程思想.哈希计算等等,在日常开发中对于一些源码的思想进行参考借鉴还是很有必要的. 基础:元素增查删.容 ...
- 【Java源码分析】Vector源码分析
类的声明 public class Vector<E> extends AbstractList<E> implements List<E>, RandomAcce ...
- Vector源码分析
import java.util.List; import java.util.Vector;public class VectorTest {public static void main(Stri ...
最新文章
- python使用openpyxl读取数据_Python-openpyxl读取和写入数据1
- 创建和存储 cookie
- 14秋 c 语言程序设计 在线作业1,14秋学期《C语言及程序设计》在线作业
- 阮一峰react demo代码研究的学习笔记 - demo5 debug
- 分析windows宿主机Ping不通linux虚拟机的其中一种情况
- bat复制文件到指定目录同名_scp复制文件时排除指定文件
- JDBC技术总结(一)
- win10更改mac地址
- local lua 多个_Lua 级别 CPU 火焰图介绍
- python api加快交易速度_使用Python3的pipedriveapi将交易输出限制为1000个交易
- SPGridview的使用
- hive和hadoop关系
- FFmpeg源代码简单分析:av_write_frame()
- 基于matlab的简单人脸识别程序代码
- Python爬虫工具
- sudo_拔剑-浆糊的传说_新浪博客
- 无刷直流电机四象限matlab pudn,一种无刷直流电机四象限运行的PWM控制方法与流程...
- 阿里云服务器搭建Django环境二:django+mysql环境搭建
- 舒适区、学习区、恐慌区
- 如何写一篇可实施的技术方案?
热门文章
- 神奇的FireFox
- Farseer.net轻量级ORM开源框架 V1.x 入门篇:新版本说明
- system.out 汉字乱码
- 0709 C语言常见误区----------函数指针问题
- 用sed 给文本文件加行号
- [置顶] 面向业务开发应用
- mariadb转mysql_MariaDB/MySQL备份和恢复(二):数据导入、导出
- android炫酷的自定义view,Android自定义View实现炫酷进度条
- shutting down mysql_mysql报这是什么错?
- 计算机导论简答芯片,吉大计算机 - 计算机导论简答题 (2011级)