MyTinySTL学习之空间分配器:allocator.h(一)
1. 什么是allocator(分配器)?
-
allocator代表一种特定的内存模型,将对内存的索求转变为对内存的直接调用。
-
allocator用于实现内存的分配和回收及对象的构造和销毁,该类向外部提供了以下4种接口来实现一个对象在动态内存上的生命周期:
分配内存(分配n个未初始化的T类对象)--->(使用构造函数)初始化该内存上的对象--->(调用该对象的析构函数)销毁对象--->释放内存Allocator提供的接口 内存的分配和回收 对象的构造和销毁 allocate(size_type n) construct(T* ptr, const T& value) deallocate(T* ptr,size_type n) destroy(T* ptr) 由上可知,使用allocate()分配的内存是原始的、未构造的(即内存上为n个未初始化的空的T类对象,里面的值是随机的、未知的),所以申请完内存后,就要使用construct()在该内存上初始化该T类型的对象(因为使用未定义的内存,其行为是未定义的,不可预期的)。当我们使用完对象后,必须使用destory()来调用该对象的析构函数来销毁它们,当内存上的对象被销毁后,就可以再次对其进行操作,即在上面构造其他本类对象/释放内存。
allocator的接口使用: allocator<string> a; //定义一个分配器对象a,它可以分配string对象 auto const p=a.allocate(n); //分配n个未初始化的string对象,并返回存放这些对象的内存块的首地址 a.construct(p,val/args); //初始化(赋值/调用其构造函数)该内存上的string对象 a.destroy(p); //执行这些string对象的析构函数 a.deallocate(p,n); //释放从从p开始的n*sizeof(string)的内存空间
2. allocator()和new()的区别?
-
allocator帮助我们将内存分配和对象构造分离开来。它分配的内存是原始的、未构造的。即allocate()出的内存是未初始化的(即划了块地。这块地原来上面是什么,现在还是什么),所以需要接着用construct()初始化它。这样分离成two-stage的设计使得用户可以在大块内存上的使用时可以按需构造对象,而避免不必要的浪费(避免多余的default ctor)。
-
allocator的缺点:
-
1.时刻记债:由上面的接口可知,如果使用allocate()来分配内存,那deallocate()释放内存时还需要将申请的内存大小作为参数传入,即从申请到释放的整个周期你都需要知道自己要了多少内存,这就导致如果直接使用allocator来分配内存太容易出错了。所以allocator往往被当作template实参/构造函数实参传递给容器,然后保存在其内部,为容器分配内存。也正是由于分配器的存在,容器和算法得以参数化它们的元素存储方式。
-
-
new将内存分配和对象构造组合在了一起,它分配的内存是默认初始化的,即分配内存后立即初始化(默认构造)一个该类的空对象(也可以通过在new T[n]的后面加上()调用该类对象的default ctor或初值列{}来对这些有定义的内存上的空对象进行值初始化)。即new申请的内存空间上的对象一定执行过该类的default ctor。
- new的缺点:
- 1.可能造成不必要的浪费:分配单个对象时,由于我们几乎肯定知道我们想要一个什么初值的对象,所以我们希望将内存的分配和对象初始化放在一起,此时new不会造成浪费。但当分配一大块内存时,我们往往不知道我们要给创建出的对象赋什么初值,所以在使用时再按需在这块内存上构造不同value的T类对象才是合理的,此时new分配出的内存就造成了很大的浪费,其一,当我们以T类型申请了一大块内存,但此时只需要x个T类型的对象时,就会导致在(n-x)*sizeof(T)的内存空间上创建了n-x个我们可能永远也用不到的T类型对象,这些多余的default ctor造成了浪费。其二,在申请一大块内存时,我们往往不知道我们要给创建出的对象赋什么值,即我们会在使用时,对new出的对象进行再一次赋值,此时这个对象就会经历2次赋值,这也造成了浪费。
- 2.没有默认构造函数的类不能用new为其动态分配内存,这是非常重要的!
3. 选择合适的动态内存分配方法:
- 在需要大量未初始化的内存时,使用适合的容器(其内部使用的是allocator来分配内存)
- 在需要少量且已知要赋什么初始的内存时,使用new/delete(这是C++关键字)或malloc/free(这是C库函数)
4.使用allocator分配内存的实验和自己的一些理解:
-
思考:在查询资料时,关于allocate()分配内存的表述和定义,使我对allocate()执行之后到constrcut()执行之前这段时间的内存状态产生了疑惑。因为allocator是模板类,在定义allocator对象时就指定了即将分配的内存上"可存放的数据类型T"(即我们分配出来的内存上只能放T类型的对象),然后才调用allocate()分配n*sizeof(T)的内存空间,再使用construct()在上面构造对象。而allocate()在内部调用的又是malloc(),其返回值是void,且分配的内存应该是一整块。所以我就产生了以下疑惑:在仅allocate()后,是仅仅分配了一整块空内存还是将这块内存分成了n块?内存上有没有T类型的对象存在?内存上有没有初始值?为了搞清楚这些问题,我试图打印出allocator每个阶段下内存的状态,做了以下实验。
-
实验:
从上图的实验结果可以看出,allocate()接受n*sizeof(T)的参数形式来进行内存分配,分配出的内存,是连续的n块sizeof(T)的内存空间。由此可见,仅allocate()后,内存上是有对象存在的。而这块内存上的初始值是随机值(因为allocate其实就是在调用malloc(),使用malloc()分配的内存的默认值是随机的)。
destory()后,也更加印证了,创建出的内存是分为n块的。至于为什么allocate()后的检测不用string,而是特地在前面用int验证,是因为string对象里面就是个指针,直接对一个未知的随机的地址进行提领操作,就会直接抛出异常退出程序,所以无法直接用string来验证仅allocate()后的内存。
-
总结:打个比方,allocate()就是圈了一大块地,这块地上的房子都是同一类型(T)的,你不知道这房子里原来放的是什么(因为有可能是二手房),所以不能直接用这块内存,而是要紧接着使用construct来调用T这个类的构造函数,给这房子装修成我们可以用的、已知状态的新房子,这样我们才能够得到我们能使用的房子(即对象)。这样理解allocate()到construct()间的内存状态就不难了。而destory()就是把房子里的指针释放、数值清0等,所以destroy()后的内存空间仍然是分成n块的。
最后:这是一篇记录学习疑惑和想法的帖子,里面的内容、表述并不一定正确,只是自己当下的理解,如有错误,还望提出您宝贵的意见。