文章目录

  • 一. 什么是空间配置器
  • 二. STL allocator的四个操作: allocate,deallocate,construct,destroy
    • `construct()`
    • `destroy()`
  • 三. SGI STL独有的空间配置器alloc
    • 1. 各个版本的allocate
    • 2. alloc空间配置器的独特结构
      • 双层配置器:
      • 一级配置器
      • 二级配置器
        • 什么是内存池
        • 为什么要使用内存池
        • 二级配置器的内存池的具体实现
    • 3. alloc用于适配STL标准的通用接口simple_alloc
    • 4. SGI 容器对于alloc的匹配
  • 四. 关于allocate变化的思考
  • 五. 内存基本处理工具
    • `construct`
    • `destroy`
    • `uninitialized_fill_n`
    • `uninitialized_copy`
    • `uninitialized_fill`
    • 程序如何进行类型选择
    • uninitialized_xxx 系列函数与其对应的高阶函数的区别是什么

一. 什么是空间配置器

负责空间配置与管理。从实现的角度来看,配置器是一个实现了动态空间配置、空间管理、空间释放的class template

​ ——《STL源码剖析》
这段话指出了空间配置器的几个特征:

  • 主要功能:负责空间的配置与管理,其将空间方面的操作抽象出来,从而使其他组间更好地面向其他方向
  • 具体功能:动态空间配置、空间管理、空间释放
  • 类型:为一个类模板(class template),实际上,6大组件除了算法组件是函数模板(function template),其他均为类模板。

实际上,因为在使用容器时默认指定了allocate,对大部分使用者来说空间配置器是透明的

二. STL allocator的四个操作: allocate,deallocate,construct,destroy

假如要进行C++的内存配置和释放操作,我们会进行如下操作:

class Foo{ ... }
Foo *pf = new Foo;
delete pf;

这个过程中new有两个操作:

  1. 调用::operator new配置内存,
  2. 调用::operator Foo::Foo()构造对象函数

delete也有两段操作

  1. 调用Foo::~Foo()将对象析构
  2. 调用::operator delete释放内存

综上,虽然只有new和delete两个操作,但是实际上有4个操作:内存的分配和回收、对象的构造和析构。因此STL allocator将其分为4个部分:

  • alloc:allocate() 内存配置
  • alloc:deallocate() 内存释放
  • ::construct() 对象构造
  • ::destroy() 对象析构

关于allocate()和deallocate() 将于后面介绍,这里简单介绍一下构造和析构函数的思想原理

construct()

构造函数其实比较简单,因为已经抽象出了内存配置功能,因此只要实现构造即可。这里直接使用使用了 placement new

一般来说,使用new申请空间时,是从系统的“堆”(heap)中分配空间。申请所得的空间的位置是根据当时的内存的实际使用情况决定的。但是,在某些特殊情况下,可能需要在已分配的特定内存创建对象,这就是所谓的“定位放置new”(placement new)操作。

by placement new机制 - AlexNoBug的文章 - 知乎

destroy()

相比于简单调用placement new的construct()destroy()略有不同,它进行了如下分类

  • 假如是对一个指针执行析构函数,则直接调用该对象的析构函数即可
  • 假如是对[first,last) 的范围执行析构函数,那需要进行一次判断:假如这个范围内的对象是"trivial"(不重要的),那鉴于循环调用析构函数的开销不小,没有调用析构函数的必要;反之,若为"non-trivial",那说明有调用的必要,因此会遍历调用析构函数

PS: 实际上,对于是否为trivial,有一个很好判断的依据:这个对象是否有用new在堆中创建内存,假如有的话,那必然为non-trivial,因为不执行析构函数会造成内存泄漏,这也是判断是否是trivial的依据——假如对象没有分配额外内存,那等着deallocate进行内存释放即可

三. SGI STL独有的空间配置器alloc

1. 各个版本的allocate

  • VC和BC5版本的allocate,本质上就是对new和delete的调用
  • GNU 2.9版本的allocate,采用的是其独特的空间配置器alloc (虽然有标准接口allocate,但是其没有调用,这个标准接口的存在纯粹是为了与旧版本的代码适配)
  • GNU 4.9时,allocate又变回了对new和delete的简单封装,原有的那个版本被改名为 __poll_alloc (内存池,也是很贴合它的本质)
  • 我查看自己的编译器 gcc 11.2.0 (mingw版) 同样容器适配的是标准接口的allocate(面向类型),同时底层也是使用delete和new实现的

2. alloc空间配置器的独特结构

双层配置器:

alloc独特的设计

  • 一级空间配置器:直接使用malloc与free来进行空间配置(没有使用new和delete)
  • 二级空间配置器:使用内存池技术,对于过小的内存块,使用内存池进行统一管理

一级配置器

​ 一级配置器与其他版本的配置器比较相似,是直接分配一块内存,略有区别的是其使用的是malloc与free来操作内存。

​ 《STL源码剖析》的猜测为: 一为历史因素,二是因为C++并未提供相应与realloc()的内存分配操作

二级配置器

​ 这是其独特的地方,其使用了内存池来进行统一管理

什么是内存池

​ 简单来说,就是申请一块较大的内存,然后使用其他程序来对其分块,每次要使用的话就从中取出一块

为什么要使用内存池

​ 对于每块申请的内存,其都有一块cookie来记录其大小——这就是额外的开销,当内存块太小时,大量的额外开销会带来很大的浪费,因此采用内存池是一个降低开销的好办法——它一次只申请一块较大的内存,额外开销很小。

​ 同时我觉得还有一个方面,就是内存的申请需要一定的时间,而内存池调用空闲内存只要在空闲的小内存块中选择即可,释放内存只需要将其归为空闲内存块即可,这方面的开销较低。

二级配置器的内存池的具体实现

​ 如果内存足够大,超过128B时,就将其移交给一级配置器处理。

​ 内存池中相同大小的内存区块是以链表形式连接的——链表的指针域会有一定的开销,但是将链表的数据域作为分配的内存移交出去的时候,节点的指针域是不需要使用的——而将内存放入链表中时,数据域也没有作用。因此可以使用共用体来处理

union obj{union obj * free_list_link;    // 指向下一个空间节点 指针域char client_data[1];// 作为一个数据域的开头
}
这个char client_data[1]很有迷惑性,看起来空间只有1字节那么大,实际上只是指明起点而已,具体大小要具体分析

​ 内存池中有16个相关的链表(free-lists)(因为是一大块静态内存,也可以说是静态链表),对应的区块大小均为8的倍数,即 8,16,24,32,40,…,128 B,因此在进行取区块的时候,程序会对需要的内存数上取整为8的倍数(会带来微不足道的额外内存分配)

  • 因此调用二级配置器时,只需要从对应的链表中取出空闲区块(头指针指向的第一个节点),取下之后,将头结点指向其next
  • 要归还区块(deallocate)时,使用头插法将区块插入作为链表的第一个节点即可
  • 假如对应链表已经没有空闲区块(头指针指向NULL),那需要重新分配内存产生新的节点放入free-lists——使用malloc分配或者从节点内存更大的链表处取,将其初始化的思路也就是我将其称为静态链表的原因——通过这个大区块的地址,第 n 块的地址位为起始地址 +(n-1)*小区块大小 即可推出

3. alloc用于适配STL标准的通用接口simple_alloc

​ 因为alloc不符合STL标准:alloc是面向字节分配的,而STL规定是为class template,需要面向类型

​ 因此对其有一个简单的封装——通过sizeof来调用alloc,使配置器的配置单位从bytes转为面向元素的大小

template<class T,class Alloc>
class simple_alloc{public:static T*allocate(size_t n){ return 0==n? 0 : (T*)Alloc::allocate(n*sizeof(T));}static T*allocate(void){ return (T*)Alloc::allocate(n*sizeof(T));}static void deallocate(T *p,size_t n){ if(0!=n) Alloc::deallocate(p,n*sizeof(T));}static void deallocate(T *p){ Alloc::deallocate(p, sizeof(T));}
};

4. SGI 容器对于alloc的匹配

默认配置器为alloc,在内部会定义对应的simple_alloc,然后使用simple_alloc来进行操作

四. 关于allocate变化的思考

​ 侯捷老师在课上和书内均没有提到为什么GNU要改回简单封装的new和delete,我经过查阅资料,现在的malloc本身已经实现了内存池技术,因此allocate就不需要自己来手动操作了。

​ 好像这样一来旧版的alloc就不适用了。其实不然,这个架构依然是有很大的学习价值的,并不会因为现在版本的更新而消退。

五. 内存基本处理工具

STL定义有5个全局函数作用于未初始化的空间上

construct

前面已经介绍过

destroy

前面同样介绍过

uninitialized_fill_n

接受三个参数:

  • 迭代器first指向初始化空间的起始处
  • n 表示初始化元素的数量
  • x表示初值

简单来说就是将[first,first+n)范围内的元素全部赋值为 x

在进行操作的时候需要判断元素是否为POD类型

POD意指Plain Old Data,也就是标量类型或传统的C struct类型,POD的构造函数/析构函数/拷贝赋值函数等必然为trivial类型

因此面对POD类型可以采用最有效率的初值填写(也就是直接赋值)手法,面对non-POD类型采用更保险的做法(非trivial的构造函数可能有额外操作,因此不能用初值填写的方式):

  • 面对POD类型,直接调用高阶函数 fill_n
  • 面对非POD类型,进行遍历然后逐个调用construct函数进行构造

uninitialized_copy

接受三个参数

  • first 指向输入端的起始位置
  • last 指向输入端的结束位置
  • result指向输出端的起始处

简单来说就是将[first,last)的元素拷贝到[result,result+(last-first) 的位置

与上面同样,进行POD类型判断

  • POD类型 直接调用高阶函数 copy
  • 非POD类型逐个调用construct

uninitialized_fill

接受三个参数

  • first 指向输入端的起始位置
  • last 指向输入端的结束位置
  • x 表示初值

简单来说就是将[first,last)的元素赋值为x

与上面同样,进行POD类型判断

  • POD类型 直接调用高阶函数 fill
  • 非POD类型逐个调用 construct

程序如何进行类型选择

入口函数使用类型萃取(type traits)获取其POD类型__type_traits<T1>::is_POD_type,将结果作为一个参数调用多路选择函数(针对POD萃取结果进行了函数重载)

uninitialized_xxx 系列函数与其对应的高阶函数的区别是什么

其作用于未初始化的空间,调用的是construct,(假如是POD类型则没有区别)

【《STL源码剖析》提炼总结】 第1节:空间配置器 allocator相关推荐

  1. STL源码剖析 内存基本处理工具 初始化空间的五个函数

    初始化空间的五个函数 构造函数 construct() 析构函数 destroy() 剩余三个底层函数 和 高层函数之间的对应关系如下 uninitialized_copy()  对应 copy() ...

  2. STL源码剖析学习七:stack和queue

    STL源码剖析学习七:stack和queue stack是一种先进后出的数据结构,只有一个出口. 允许新增.删除.获取最顶端的元素,没有任何办法可以存取其他元素,不允许有遍历行为. 缺省情况下用deq ...

  3. 《STL源码剖析》学习-- 1.9-- 可能令你困惑的C++语法1

    最近在看侯捷的<STL源码剖析>,虽然感觉自己c++看得比较深一点,还是感觉还多东西不是那么明白,这里将一些细小的东西或者概念记录一下. 有些东西是根据<C++编程思想>理解的 ...

  4. 《STL源码剖析》学习--6章--_rotate算法分析

     最近在看侯捷的<STL源码剖析>,其中有许多不太明白之处,后经分析或查找资料有了些理解,现记录一下. <STL源码剖析>学习--6章--random access ite ...

  5. 《STL源码剖析》学习--6章--power算法分析

    最近在看侯捷的<STL源码剖析>,其中有许多不太明白之处,后经分析或查找资料有了些理解,现记录一下. 6章--power算法分析 书本中的算法如下所示: template <clas ...

  6. STL源码剖析——P142关于list::sort函数

    在list容器中,由于容器自身组织数据的特殊性,所以list提供了自己的排序函数list::sort, 并且实现得相当巧妙,不过<STL源码剖析>的原文中,我有些许疑问,对于该排序算法,侯 ...

  7. STL源码剖析---红黑树原理详解下

    转载请标明出处,原文地址:http://blog.csdn.net/hackbuteer1/article/details/7760584       算法导论书上给出的红黑树的性质如下,跟STL源码 ...

  8. STL源码剖析面试问题

    当vector的内存用完了,它是如何动态扩展内存的?它是怎么释放内存的?用clear可以释放掉内存吗?是不是线程安全的? vector内存用完了,会以当前size大小重新申请2* size的内存,然后 ...

  9. STL源码剖析学习二:空间配置器(allocator)

    STL源码剖析学习二:空间配置器(allocator) 标准接口: vlaue_type pointer const_pointer reference const_reference size_ty ...

最新文章

  1. 使用tcpwrapper实现访问控制功能
  2. java 使用new新建一个对象时的操作过程
  3. 收藏长文|Java 代码精简之道
  4. 【Leetcode】Palindrome Number
  5. Element-UI分页组件超详细使用示例
  6. 在Angular里使用rxjs的异步API - Observable
  7. JS 获取浏览器信息,给出友情提示,避免部分兼容性问题
  8. 肿瘤坏死因子(TNF)阻断剂治疗幼年型银屑病关节炎: 有效吗
  9. 晨哥真有料丨盘点追女生的作死行为!
  10. 利用opencv中的级联分类器进行人脸检測-opencv学习(1)
  11. 【激光雷达】激光雷达点云数据的技术流程和点云预处理的方法
  12. [na]思科产品选型pdf
  13. Java访问用户名密码验证的url
  14. vbs返回结果给java_返回vbs脚本
  15. 5个小众视频素材网站,你知道吗?
  16. python爬虫爬取糗百成人图片单线程版本
  17. 【组合数学】多项式定理 ( 多项式定理 | 多项式定理证明 | 多项式定理推论 1 项数是非负整数解个数 | 多项式定理推论 2 每项系数之和 )
  18. 虚拟机vm介绍及虚拟机常见操作
  19. 干货!基于深度空间一致性的鲁棒点云配准算法
  20. 阿里云短信服务Java实现

热门文章

  1. 解决Win11家庭版无gpedit.msc方法
  2. JAVA实现支持视频点播WEB服务器
  3. java for循环创建线程_Java创建线程的两种方法
  4. 谷粒商城P16人人开源登录没有反应
  5. 电脑如何打出声调的五度竖棍标记法符号 ?
  6. 小T牛 绿色版 18.08.0100
  7. D3 二维图表的绘制系列(十四)气泡图
  8. 霍金 北京《宇宙的起源》演讲全文
  9. 必须掌握的几种最优化方法极其区别
  10. 【甘道夫】通过Mahout构建贝叶斯文本分类器案例详解