MyTinySTL学习之内存分配器:allocator.h(二)源码阅读
引言
- 在allocator.h(一)中,我们知道了allocator是什么(一种two-stage内存模型),allocator在STL中的作用(容器背后的无名英雄,当我们使用容器申请内存时,内部就是STL帮我们合理的调用allocator),在知道了allocator的一些基本原理后,在这一节我们就来进行allocator.h的源码阅读。
- 本系列博客所学习的MyTinySTL为Github上基于C++11的开源项目,项目地址为:(github.com/Alinshans/M…) 这个项目在Github上已经有6.2k的stars,感谢Alinshans大佬开源这个优质的学习项目。
源码阅读
allocator.h
- allocator类的实现分为两个部分:①提供STL标准化的类型接口 ②提供4组功能函数接口,下面分别从allocator类的整体定义、STL标准化类型接口和4组功能函数的定义来学习整个头文件。
-
allocator类的定义
- 由上图可见,allocator是一个模板类,在类中不仅实现了上一篇文章提到的用来管理内存的4组接口函数,还使用typedef定义了一堆“奇怪的类型别名”,接下来就分别介绍这两组成员。
- STL标准类型接口
-
为什么要在allocator类中定义这些接口?:STL是由6大部件构成的,其中容器和算法是相互独立的两个部分,它们只关注于自己的实现,而并不了解对方,所以就需要一个组件来成为它们中间的桥梁,而iterator就是这个桥梁。当算法想要通过iterator去操控容器中的元素,就要先知道这个容器中的iterator有哪些性质,然后根据不同的性质,做出最佳选择(补充说明:iterator的associated type分为元素相关、迭代器范围相关和5种迭代器类型,前两种都在allocator中定义,然后由容器通过typedef获取,最后一种则直接由各个容器实现自己的迭代器移动规则,所以在allocator中只需要实现前两种)。--------->此时问题来了,每个容器的实例都有自己的类型,当算法询问时,iterator如何知道当前容器中iterator的这些具体的associated type呢?那只能制定一个共同的接口,让各个类类型将各自的associated type都typedef为这些共同的接口,这样iterator就可以通过这些接口,获取各个实例容器中实际的associated type实参。这样迭代器在被算法询问时,就可以回答算法对当前容器中iterator的assocaited type的提问。又因为一块内存上可存放的元素类型和内存范围是allocator决定的,所以这方面相关的内容由allocator实现,在各个容器的实现中,只需要再用一次typedef用allocator中的标准接口来初始化自己的标准接口即可。而各个类的Iterator class中也都有这些同名的类型接口,容器实例中的别名(此时成为了实例的类型)作为实参传递给Iterator里的别名。
-
关于I::标准别名的补充说明(iterator_traits):细心的你可能发现,并不是所有迭代器都有能力实现I::标准别名的。例如vector的iterator就是C++内置指针,即它是一个non-class iterator,所以它没有能力定义自己的5个associated type,这时候它是如何回答算法的问题的呢?这里提前了解一下iterator_traits。在为一个容器定义Iterator class时,该Iterator class中必须提供5种associated type(定义格式如下),用于回答算法的提问(定义格式如下)。
以list容器的Iterator class为例: template<typename _Tp> struct iterator{ typedef std::bidirectional_iterator_tag iterator_category;//迭代器类型(5种中的哪个) typedef _Tp value_type;//迭代器所指的元素类型(实参为当前实例容器中的元素类型) typedef _Tp* pointer;//元素指针 typedef _Tp& reference;//元素引用 typedef ptrdiff_t difference;//C++内置的数据类型(一般为long int),用来保存两个指针相减的结果 ····· };
算法对class iterator的associated type的提问方式:I::类型接口名 template<typename I> inline void algorithm(I first,I last){ ····· I::iterator_category I::pointer I::reference I::value_type I::difference ····· }
-
看到这里也许你会觉得好像没有traits啥事,但如果当iterator不是一个类,而是C++内置指针,这时候怎么办呢?很明显没法用I::类型名接口的方式来提问,此时就需要traits来发挥作用!间接访问iterator的类型,访问方式如下。
算法对non-class iterator的associated type的提问方式: template<typename I,...> void algorithm{ typename iterator_traits<I>::value_type v1; }
-
iterator_traits在接受I之后,告诉value_type它是什么值,这样就得到了non-class iterator的associated type。iterator_traits的实现和工作原理是模板偏特化,具体源码等看到具体iterator.h时再讨论。
- 功能函数接口的实现
-
3.1 allocate()的定义:
-
①从上图可以看出,作者为allocate实现了两个重载版本,如果没有传入实参就分配一个T类型的大小的空间,如果传入参数,就分配n*sizeof(T)大小的内存空间。
-
②operator new():可以看出allocate()在内部调用的是operator new(),这是个什么函数呢?看一下gcc的源码。
-
从上面的源码可以看到,operator new()接受一个实参sz(表示需要分配的字节数)后,首先对sz进行一个是否为0的判断,如果sz为0就将其置1,因为malloc(0)是不可预期的,必须避免,然后在while循环中调用malloc(sz)申请内存,直到申请成功或抛出异常或ABORT。所以operator new()的功能就是调用malloc()分配内存,然后返回一个指向该内存首地址的void型指针。
- 补充说明:①_builtin_expect(express,res)函数就是告诉编译器express最可能的取值是res,这样将”分支转义信息告诉编译器,编译器可以对代码进行优化“。②handler,当malloc()分配失败时就会进入while执行new_handler函数,来提示分配错误的信息。
-
③static_cast<target>(source)函数:C++提供的类型转换函数,static_cast用于进行低风险的类型转换(eg.低精度int到高精度double),即如果你想要执行的转换是高风险的(eg.高进度double到低精度int、整型和指针间的相互转换),它就会直接编译错误,降低类型转换带来的风险。这样就可以将void类型的指针转换为T*类型返回。
-
所以allocate()的功能就是调用malloc()分配内存,然后将malloc()返回的void*指针强制转换为T*指针返回。
-
3.2 deallocate()的定义:
-
从上图可以看出,deallocate()比较简单,其实就是判断指针非空后调用operator delete(),所以去看一下其在gcc中的定义。
-
从上图可以看出,operator delete()就是在调用free()。 所以deallocate()的功能就是,接受一个T*指针,如果非空就调用free()释放指针所指的这块内存(这块内存的长度在malloc时,由cookie存储,所以释放时不再需要传入长度)。
-
3.3 construct()的定义
-
由上图可知,作者对construct()进行了一层封装,construct()具体的实现在construct.h中,我们来看一下该头文件中的具体实现。
-
由construct()的实现,我们可以看出:①construct()至少接收一个参数(指针Ty),Ty指向allocate()分配的可存放Ty类型对象的一块原始内存的首地址。②第二参数即可以传入构造函数实参,也可以不传,对应的会调用T类对象不同的ctor在Ty所指向的内存上构造对象。③construct()其实就是在调用operator new()这个函数,而根据其参数中含有指针可以知道,调用的是其重载版本placement new(),源码如下。
-
由上图可知,placement new()只是简单的返回了这个实参指针,那么它有什么用呢?事实上它可以通过以下方式在实参指针所指的内存上,调用Ty类对象的构造函数,在上面生成对象。关于placement new()的解释和使用方法可见cppreference,如下图:
-
从reference的解释中我们可以看到,placement new()仅仅是简单地、原封不动的返回它的第二参数(指向一块未初始化内存空间首地址的指针),而这个指针正是用来在allocate()得到的内存上construct object。那具体怎么在这块内存上构造对象呢?那就是在new(placement)后面接上要构造对象所属类的构造函数,其中placement就是这个指针。所以作者所实现的三种construct(),都是在间接调用同样的placement new(),只不过3个重载形式,分别调用Ty类对象的默认构造函数、含参构造函数和移动构造(减少不必要的深拷贝)。
- 【NOTE1】其实,在看完operator new()的源码后,我们可以发现直接new一个对象的效果相当于调用operator new()的两个重载函数,那什么场景下,需要先operator new(size)一块内存,再placement new(p)在上面构造对象呢? 那就是当需要频繁构造和析构对象时,为了避免重复的内存分配和释放,我们可以使用这种方法,一个很常见的应用场景就是内存池的实现。
- 【NOTE2】使用placement new()在原始内存上构造对象时,我们可以发现,其实每调用一次函数,只调用了一次构造函数,所以在构造对象时,需要一对迭代器来记录当前已经使用的内存块区间。
-
3.4 destory()的定义
-
由上图可知,allocator类中的destory函数是对mystl命名空间下destroy()函数的调用。并且有两种情况:①析构一个对象 ②析构迭代器范围内的所有对象。接下来我们去construct.h中看一看mystl::destroy()的具体定义。
-
从上图可以看出,destroy的两个重载函数分别调用destroy_one()函数和destroy_cat()函数,分别用来析构单个对象/迭代器范围内的所有对象。在这两个函数中,我们发现都有一个type_traist:std::is_trivially_destructible来作其实参,这个type_traits用来询问当前类T的析构函数是否是微不足道的,如果是就返回true,如果不是就返回false。那么什么叫微不足道的呢?你可以理解成如果执不执行都一样的,那就是微不足道的,否则就是重要的。所以这个type_traits放在这里就用来询问当前class是否有自定义的析构函数,如果有就返回false,否则返回true。这样我们就知道了这两个函数的最后一个实参是true/false。接下来我们看这两个函数的具体实现。
-
由上图我们可以发现,这两个函数分别由最后一个参数进行了重载,显而易见,这样做的目的就是:如果要析构的类没有自定义的析构函数,那就不执行它的默认析构。如果要析构的类有自定义的析构函数,就执行它的析构函数,来析构对象。由源码我们可以发现,destroy()其实就是通过指向(有自定义析构函数的)对象的指针,来调用该对象的析构函数,从而达到析构该对象的目的。
-
总结:至此,MyTinySTL中关于空间分配器allocator的源码就分析完毕了,通过源码的分析,就能够很轻易的理解为什么new不够灵活,以及为什么allocator可以避免operator new()和placement new()合为整体执行带来的浪费(这种浪费在对象需要频繁构造析构的情况下是非常大的 eg.内存池)
最后:这是一篇记录学习疑惑和想法的帖子,里面的内容、表述并不一定正确,只是自己当下的理解,如有错误,还望提出您宝贵的意见。