STL源码剖析笔记
第二章 空间配置器(allocator)
空间配置器标准接口(allocator)
allocator配置器是SGI STL提供的标准接口,但它只是对::operator new()和::operator delete()做了一层简单封装,效率不高,所以SGI并没有使用它。
高效的空间配置器结构std::alloc
这里的new内部包括两步操作:
- 调用
::operator new()
分配内存 - 调用Foo的构造函数
delete也包括两步操作:
- 调用Foo的析构函数
- 调用
::operator delete()
释放内存
上面的operator new()
里面其实调用了malloc()
,operator delete()
里面调用了delete()
。
为了提高这个过程的效率,STL allocator将这两个阶段的操作区分开来,分为内存操作和对象操作:
- 内存操作
- 内存分配:
alloc::allocator()
- 内存释放:
alloc::deallocator()
- 内存分配:
- 对象操作
- 对象构造:
::construct()
-> placement new - 对象析构:
::destroy()
- 对象构造:
空间配置器std::alloc定义在stl_alloc.h中,整个配置器分为两层,为了考虑小型区块可能造成的内部碎片问题,SGI设计了两层级配置器,第一级采用malloc()
和free()
,第二级则视情况而定:若配置区块超过128bytes,则采用第一级配置器;否则就采用memory pool的方式即第二级配置器。而整个容器的allocator就是simple_alloc,其实是对std::alloc的简单封装,使之能符合STL标准。
上面的模板参数Alloc就表示了配置器的种类,一级或二级。
一级配置器
可以看出,一级配置器malloc_alloc仅仅是对malloc和free的简单封装,这里的__malloc_alloc_oom_handler函数指针默认为NULL,它类似于C++new里面的set_new_handler()函数,就是在空间分配失败时,会调用这个回调函数。
二级配置器
二级配置器的设计是为了对付小型区块内部碎片,为了管理区块,还需要一些额外内存存放管理信息cookie。它维护了一个链表数组,每个节点指向大小不同的内存块,大小为8、16、24...128byte。每次分配内存时,就选择合适大小的块分配。如果块不够,就从内存池中分配一大块内存,切成小块,加入到对应的链表中。如果内存池也不够用了,就调用malloc来补充内存池。
allocator
allocator
函数先判断是否调用一级配置器。如果不是,就选择一个合适块大小的链表,然后分配一个块。如果链表为空,就refill填充内存。
refill
refill
先调用chunk_alloc
去内存池分配20个块大小的内存。如果只返回一个块,就还是返回。否则就把分配来的内存切成小块,加入到对应块大小的链表中。
chunk_alloc
chunk_alloc
先是判断自己的内存池里面的内存还够不够这次分配。如果够,就正常处理下,然后返回。如果只够一个block,也会返回。但是如果一个block也不够了呢?这时候就需要malloc
了,具体要申请的是2 * __total_bytes + _S_round_up(_S_heap_size >> 4);
,这个total_bytes
好理解,就是我们需要的空间,而这个heap_size
则是这个class template中的一个static data member,存储的是迄今为止用了多少heap memory,也就是malloc
了多少内存,初始值为0。所以说,每次要申请的内存是两倍的需求量加上,一点点附加量。然后,malloc
会比需求多点,给内存池存点余额,避免频繁的调用malloc
,影响速度。而malloc
失败后,会尝试把链表里面的mem block都收集到内存池中去,然后再去尝试满足这次分配。然后就是调用malloc_alloc
。
第三章 iterator and traits
iterator
迭代器是一种行为类似指针的对象,本文介绍iterator与traits的关系,以及对traits内容的补充。包含stl_iterator.h的部分内容,并用c++11对其进行略微改写。
上篇文章已经介绍了这五种类型的特征,它们只是为了激活重载机制而设定,并不需要其他成员。它们的定义如下:
接下来是iterator的定义:
主要讨论如何获取迭代器相应型别。使用迭代器时,很可能用到其型别,若需要声明某个迭代器所指对象的型别的变量,该如何解决。方法如下:
function template的参数推导机制
例如:
func_impl()是一个 function template,一旦被调用,编译器会自动进行template参数推导,从而导出型别T,无需自己指出型别,解决问题。迭代器相应型别不只是迭代器所指对象的型别一种而已,最常用的相应型别有五种,但并非任何情况都可利用上述template参数推导机制来取得。这就需要其他方法。
Traits编程技法
迭代器所指对象的型别,成为该迭代器的value type,上述模板参数推导并非全面可用,在需要value type作为函数返回值时,就不能解决了。template参数推导的只是参数而已。因此,声明内嵌型别就出现了。
例如:
func()函数的返回值必须加上关键字typename,用来告诉编译器这时一个模板类型。但并不是所有迭代器都为class type,原生指针就不是,它就不能定义内嵌型别。这时模板偏特化(template partial specialization)就能解决这个问题。
偏特化的意义
如果class template拥有一个以上的template参数,我们可以针对其中某个(或数个,但并非全部)template参数进行特化工作。也就是将泛化版本中的某些template参数给予明确的指定。
如:
偏特化不是template参数U、V或T指定某个参数值,而是针对(任何)template参数更进一步的条件限制所设计出来的一个特化版本。看这个例子:
如此便能解决前面的内嵌型别的问题。下面这个class template专门用来萃取迭代器的特性之一 :value_type
如果I内定义了自己的value type,通过这个traits萃取出来的value type就是I::value_type,则前面的func(),可改为:
现在写出Iterator_traits的一个偏特化版本,这就解决了原生指针的问题,如果写成Iterator_traits<int*>::value_type,便得到int:
但针对“指向常数对象的指针”,Iterator_traits<const int*>::value_type可萃取到const int,此时若要得到non-const int,就要设计下面这一个偏特化版本:
现在迭代器MyIter、原生指针int*或const int *,都能通过Iterator_traits取出正确的value type。
所以,每个迭代器应以内嵌型别定义的方式定义出相应型别,以遍traits运作。
迭代器相应型别
value type
value type指迭代器所指对象的型别。
difference type
difference type表示两个迭代器之间的距离,也能表示一个容器的最大容量,对连续的空间而言,头尾的距离就为最大容量。如STL的count(),其返回值就必须使用迭代器的difference type:
针对原生指针而写的偏特化版本,以c++内建的ptrdiff_t(定义于cstddef头文件)作为原生指针的difference type:
现在,我们可以通过写
来得到任何迭代器I的difference_type。
reference type
引用类型,传回一个迭代器所指对象的引用
pointer type
传回一个,指向迭代器所指之物的pointer。
Item&便是某个迭代器的reference type,而Item*便是其pointer type。在traits内:
iterator_category
根据移动特性,迭代器被分为五类:
- Input Iterator: read only
- Output Iterator: write only
- Forward Iterator: 允许写入型算法在这种迭代器所形成的区间上进行读写操作
- Bidirectional Iterator: 可双向移动
- Random Access Iterator: 涵盖所有指针算数能力,包括p+n,p-n,p[n]等等
它们从上到下,功能依次强化。
下面以advance()函数为例,该函数有两个参数迭代器p,数值n;函数内部将p累进n次。
下面针对三种不同迭代器进行示范:
这样在执行期才决定使用哪个版本,会影响效率。最好能在编译器就选择正确版本,重载函数机制就解决了这个问题。
前面3个advance_xx()都有两个template 参数,类型不确定,为了形成重载函数,必须加上一个型别已经确定的函数参数。下面五个classes代表了五种迭代器类型:
这些classes当做标记用,作为第三个参数,使能达到重载函数的目的:
每一个_advance()的最后一个参数只声明型别,是为了激活重载函数机制,并不需要参数名称,并且函数中并不使用该参数。下面是对外开放的接口,以调用不同的_advance()。
为满足上述行为,traits中增加相应型别:
任何一个迭代器,它的类型应属于迭代器类型中功能最强大的类型如int* ,既是Random又是Bidirectional,既是Forward又是Input,他应为Random。
第四章 序列式容器
vector
概述
vector是动态数组,其底层空间会随着插入元素而自动扩充,并且是连续的内存。如果空间不够用,会扩充至两倍空间,会申请更大的一片空间,将原数据copy过去,并释放旧空间;如果两倍都不够用,比如空间大小为4,但要插入100个元素,那么就需要让空间增长n个大小,因此如果空间被重新配置,那么原来的所有迭代器都会失效,这点尤为重要。
结构
vector使用的空间配置器为simple_alloc
,start
到finish
之间是已经使用的空间,finish
到end_of_storage
直接是预留的未使用空间,每次当预留的空间都不足时,就会申请一块新的连续空间,将数据copy过去,并释放旧空间,这通过deallocate()
完成。
常用函数
需要注意的是size()
和capacity()
的区别,前者是已存储数据的个数,后者是总的可用空间的大小。几个构造函数是通过fill_initialize()
实现的,如果是POD类型的对象,则就和memset()
相似,因为不需要调用构造函数,vector析构的时候也不需要调用每个对象的析构函数。如果对象有nontrivial constructor,才会调用构造和析构函数。
reserve()
是相对于capacity()
来操作的,就是扩容总的存储空间,通过allocator new area -> copy old data to new area -> destroy old data -> deallocate old memory
。
resize()
是相对于size()
来操作的,就是调整已存在对象的个数。
swap()
是通过交换内部的3个迭代器实现的,这很高效,并不是复制对象再释放内存。
insert
插入单个元素,都是先判断容量是否够,如果不够就会调insert_aux()
。
insert()
也是先判断备用空间,如果充足,就将从position
开始的元素后移,再将插入元素copy进来。如果备用空间不足,就先决定新的空间大小,这根据插入个数n来决定是旧长度的2倍还是旧长度+n:
erase
pop_back()
、erase()
就比较简单了,只是destroy()
和调整那3个关键迭代器。
__EOF__

本文链接:https://www.cnblogs.com/vlyf/p/11838458.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人