当前位置: 首页 > 编程日记 > 正文

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分配器中的allocatedeallocate函数,他们如何进行空间分配

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 逻辑有两种

  1. 当vector的内部的finish指针并没有达到end_of_storage指针,即没有达到我们理解的容量的上限的时候,会构造一个_Tp的对象加入到finish指针区域,同时finish指针增加。
  2. 当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的扩容会有一些隐患:

  1. 当扩容产生,将旧的空间的元素拷贝到新的空间的元素会触发 临时对象的构造和 拷贝构造函数,此时如果元素体量较为庞大,那大量的新对象的构造和拷贝构造函数调用 会产生一点的性能开销。
  2. 新的空间和元素都拷贝过去之后,旧的空闲也需要释放,此时又会有大量的析构调用来进行空间的释放。

验证代码如下:

#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在这一方面推出的性能方面的改进逻辑:

  1. push_back 将拷贝构造函数变更为 转移构造(std::move)函数,减少来空间释放的开销
  2. 推出emplace_back函数,使用变长模版参数forward进行对象在的原地构造,只需要一次对象本身的构造即可。

emplace_back函数,详细的实现可以参考empalce_back和push_back的差异

vectoremplace_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的开发者们 不断得完善逻辑,改进性能。并且不断得推出新的特性来供给我们这一些语言的使用者来使用,如果我们还用不好,那是真的愧对大佬们的付出了。

精进自己,追求极致,向大佬们致敬。

相关文章:

STM32学习笔记9(SysTick滴答时钟)

我不得不说意法半导体确实有点风骚&#xff01;甚至有点变态。我对ST文档 STM32F10XXX参考手册的编辑水平真是不敢恭维。手册中好多说明都是含糊不清&#xff0c;甚至将好多对初学者来说很重要的地方都一笔带过&#xff0c;让人着实摸不着头脑。比如前面我说过的关于NVIC嵌套向…

oracle存储空间管理,Oracle存储空间管理

Oracle存储空间管理1.查看每个数据文件的剩余表空间(一个表空间只对应N个数据文件,N一般等于1)主要是利用表dba_free_space(表空间剩余空间状况)和dba_data_files(数据文件空间占用情况)select b.file_id  "文件ID",b.tablespace_name  "表空间名",b.f…

漫谈Httpclient

引用地址&#xff1a; http://hc.apache.org/httpclient-3.x/ End of life The Commons HttpClient project is now end of life, and is no longer being developed. It has been replaced by the Apache HttpComponents project in its HttpClient andHttpCore modules, whic…

具体数学-扰动法

from scipy.special import combn int (input("n: "))k int (input("k: "))sum1 0for j in range(0, k):for i in range(0, n1):sum1 sum1 (i**j)*comb(k1,j)result ((n1)**(k1) - sum1)/comb(k1,k) print(result) python 中计算排列组合&#xff1a…

C++ STL: 超详细 容器 deque 以及 适配器queue 和 stack 源码分析

文章目录前言deque 实现deque类_Deque_iterator 类deque 的元素插入 insert函数deque如何模拟空间连续queue 实现stack 的实现前言 C容器中 deque是一个非常有趣的容器结构&#xff0c;我们知道deque本身是一个支持双向扩容的结构&#xff0c;对外表现出连续的存储方式。 本节将…

php好的mvc中index方法,创建一个mvc应用目录架构并创建入口文件index.php

摘要&#xff1a;<?php require vendor/autoload.php;require pig/Base.php;define(ROOT_PATH,__DIR__./);$configrequire pig/config.php;$queryStr$_S<?php require vendor/autoload.php;require pig/Base.php;define(ROOT_PATH,__DIR__./);$configrequire pig/confi…

C语言对mysql数据库的操作

C语言对mysql数据库的操作 原文:C语言对mysql数据库的操作这已经是一相当老的话题。不过今天我才首次使用&#xff0c;把今天的一些体会写下来&#xff0c;也许能给一些新手带来一定的帮助&#xff0c;更重要的是供自己今后忘记的怎么使用而进行查阅的&#xff01; 我们言归正传…

[转]MCC(移动国家码)和 MNC(移动网络码)

From : http://blog.chinaunix.net/uid-20484604-id-1941290.html 国际移动用户识别码&#xff08;IMSI&#xff09; international mobile subscriber identity 国际上为唯一识别一个移动用户所分配的号码。  从技术上讲&#xff0c;IMSI可以彻底解决国际漫游问题。但是由于…

虚拟机cenos 重置密码

许久不用虚拟机&#xff0c;临时想登录看下&#xff0c;结果想不起来密码了&#xff0c;尝试各种可能的密码都登录失败。然后百度查验各种方法&#xff0c;看到一篇文章然后根据上面说的成功解决了问题&#xff0c;贴出链接&#xff1a;https://blog.csdn.net/dannistang/artic…

g-git 相关命令 及其 基本原理探索 (一)

文章目录git 最小配置作用域git 创建本地仓库git log 查看版本演进.git 目录refs目录objectsgit 三种对象类型详解 (commit ,tree,blob)因为工作需求&#xff0c;接下来将从git的使用到其内部工作原理&#xff0c;来避免代码提交或者review或者版本管理上的一些尴尬&#xff0c…

php表单退出怎么写,PHP提交表单失败后如何保留填写的信息

[导读]本文章来给各位同学介绍PHP提交表单失败后如何保留填写的信息一些方法总结&#xff0c;最常用的就是使用缓存方式了&#xff0c;这种方法如果网速慢是可能出问题的&#xff0c;最好的办法就是使用ajax了。1&#xff0e;使用header头设置缓存控制头Cache-c本文章来给各位同…

lists,tuples and sets of Python

(python2.7.x) Lists 列表 列表是一个有序序列(集合)&#xff0c;可变的&#xff08;可以修改&#xff09;&#xff0c;可以理解为其他语言中的数组类型&#xff0c;但是列表更灵活更强大。 列表由方括号[]来定义的&#xff0c;它的元素可以是任意类型或对象&#xff0c;一个列…

Javascript中使用WScript.Shell对象执行.bat文件和cmd命令

WScript.Shell&#xff08;Windows Script Host Runtime Library&#xff09;是一个对象&#xff0c;对应的文件是C:/WINDOWS/system32/wshom.ocx&#xff0c;Wscript.shell是服务器系统会用到的一种组件。shell 就是“壳”的意思&#xff0c;这个对象可以执行操作系统外壳常用…

控件绘制的方法

1处理WM_PAINT 最极端的选择是执行一个 WM_PAINT 处理程序&#xff0c;并且自己完成所有的绘制。这意味着&#xff0c;您的代码将需要进行一些与呈现控件相关的琐事 — 创建适当的设备上下文&#xff08;一个或多个&#xff09;&#xff0c;决定控件的大小和位置&#xff0c;绘…

google gflags的参数解析,便捷实用

命令行参数解析&#xff0c;一直是我们后段开发人员需要经常使用的一个功能&#xff0c;用来从终端解析接口的输入 &#xff0c;并做出对应的处理。这里为使用C/python的开发人员推荐一个便捷的命令行解析接口集 gflags。 我们之前使用C的getopt/getopt_long 函数需要自己使用…

linux进程下的线程数,Linux下查看进程线程数的方法

0x01&#xff1a;ps -ef只打印进程&#xff0c;而ps -eLf会打印所有的线程[rootcentos6 ~]# ps -ef | grep rsyslogdroot 1470 1 0 2011 ? 00:01:13 /sbin/rsyslogd -c 4root 29865 28596 0 22:45 pts/5 00:00:00 grep rsyslogd[rootcentos6 ~]# ps…

OpenCV学习(20) grabcut分割算法

在OpenCV中&#xff0c;实现了grabcut分割算法&#xff0c;该算法可以方便的分割出前景图像&#xff0c;操作简单&#xff0c;而且分割的效果很好。算法的原理参见papaer&#xff1a;“GrabCut” — Interactive Foreground Extraction using Iterated Graph Cuts 比如下面的一…

VMware workstation中rhel安装VMware tools失败

切换登录用户为root即可转载于:https://www.cnblogs.com/dazzleC/p/10555809.html

g-git 相关命令 及其 基本原理探索(二):git 在工作中的常用命令操作 ,超级实用!!!

上一篇git 基本原理对git的使用以及文件分布已经有了一个整体的了解。 本篇将对工作中常用的一些git 操作命令的操作进行总结归纳&#xff0c;方便今后查阅。 文章目录1. 分离头指针2. 通过HEAD 来进行不同提交的差异对比3. 删除不需要的分支4. 对当前分支最近一次提交的messag…

Linux中的文件寻址,Linux文件寻址算法:逻辑地址到物理地址的转换

题目描述&#xff1a;编写一个函数实现Linux文件寻址的算法&#xff0c;即读取文件当前位置到物理存储位置的转换函数&#xff0c;需要给出运行的测试数据&#xff0c;可以假设和模拟需要的数据和结构。即编写一个函数unsigned long ltop(unsigned long logblkNum). 计算逻辑块…

datatable和dataset的区别

DataSet 是离线的数据源 DataTable 是数据源中的表.当然也可以自己建一张虚表。插入数据库中 DataSet是DataTable的容器DataSet可以比作一个内存中的数据库&#xff0c;DataTable是一个内存中的数据表&#xff0c;DataSet里可以存储多个DataTabledatatable是dataset中的一个表另…

如何避免重构带来的危险

http://blog.jobbole.com/30049/ 重构代码很危险&#xff0c;它会给测试工作增加巨大的负担。除非你的程序需要重构&#xff0c;一定不要轻易重构代码。我这里所说的并不是把一个for循环改成while循环&#xff0c;或把一个StringBuffer改成StringBuilder&#xff0c;我说的是大…

ip通信(第二周)

计算机网络分为局域网&#xff08;LAN&#xff09;、城域网&#xff08;MAN&#xff09;和广域网&#xff08;WAN&#xff09;。拓扑结构分为星型网&#xff0c;树形网&#xff0c;分布式网络&#xff0c;总线型网络&#xff0c;环型网络和复合型网络。认识了几个常见的国际标准…

《DDIA》读书笔记

数据存储系统的经典书籍&#xff1a; 从数据系统的特性开始&#xff0c;先讲单机存储引擎 再到 分布式存储系统&#xff0c;最后到一些数据流的处理方式&#xff0c;作者深入浅出&#xff0c;译者更是精雕细琢&#xff0c;本书需要细品。 将持续阅读整理&#xff0c;先从理论走…

linux网卡设置adsl上网,Linux下设置ADSL自动拨号上网

前段时间下载了红帽的linux&#xff0c;版本为redhat 9.0&#xff0c;整整刻了三张CD。最初是为了体验一下linux下QQ聊天软件的功能&#xff0c;最后因内核太低(官方推荐内核在2.6以上&#xff0c;我下载的版本是2.4)而告终。最大的收获是了解了linux下文件系统及linux下软件与…

安卓天天酷跑脚本刷高分图文教程

http://news.gamedog.cn/a/20130923/241742.html

SpringBoot 中 JPA 的使用

前言 第一次使用 Spring JPA 的时候&#xff0c;感觉这东西简直就是神器&#xff0c;几乎不需要写什么关于数据库访问的代码一个基本的 CURD 的功能就出来了。下面我们就用一个例子来讲述以下 JPA 使用的基本操作。 新建项目&#xff0c;增加依赖 在 Intellij IDEA 里面新建一个…

《DDIA》读书笔记(一):可靠性、可扩展性、可维护性

这一节描述了密集型应用的基本思考方式。 可靠性。意味着系统发生故障&#xff0c;也能保持正常的运行。故障会集中在三个方面&#xff0c;硬件故障(通常是随机和不相关的)、软件故障(通常是系统性的bug,较难发现&#xff0c;较难处理)&#xff0c;人为故障(不可避免得时不时出…

TCP协议-TCP连接管理

TCP协议是 TCP/IP 协议族中一个非常重要的协议。它是一种面向连接、提供可靠服务、面向字节流的传输层通信协议。TCP(Transmission Control Protocol,传输控制协议)。

[Unity WWW] 跨域访问解决方法

什么是跨域访问 域(Domain)是Windows网络中独立运行的单位&#xff0c;域之间相互访问则需要建立信任关系(即Trust Relation)。信任关系是连接在域与域之间的桥梁。当一个域与其他域建立了信任关系后&#xff0c;2个域之间不但可以按需要相互进行管理&#xff0c;还可以跨网分配…