19.4 C++STL标准模板库大局观-分配器简介、使用与工作原理说

19.1 C++STL标准模板库大局观-STL总述、发展史、组成与数据结构谈
19.2 C++STL标准模板库大局观-容器分类与array、vector容器精解
19.3 C++STL标准模板库大局观-容器的说明和简单应用例续
19.4 C++STL标准模板库大局观-分配器简介、使用与工作原理说
19.5 C++STL标准模板库大局观-迭代器的概念和分类
19.6 C++STL标准模板库大局观-算法简介、内部处理与使用范例
19.7 C++STL标准模板库大局观-函数对象回顾、系统函数对象与范例
19.8 C++STL标准模板库大局观-适配器概念、分类、范例与总结

4.分配器简介、使用与工作原理说

  4.1 分配器简介

    跟容器紧密关联在一起使用的是分配器,只是在编写代码时,一般都采用系统默认的分配器,不需要自己去指定分配器,所以很多读者对分配器并不熟悉甚至不知道有分配器的存在,因为就算不知道分配器的存在,也不影响使用STL。当然,作为STL的组成部分,还是有必要把分配器讲解一下。
    当输入图所示的代码时,总能看到一个typename_Alloc=allocator字样的参数提示,这就是一个分配器。

在这里插入图片描述
    所以,如果输入的类型是:

vector<int> myvec;
list<int> mylist;

    那么就等价于

vector<int, std::allocator<int>> myvec;
list<int, std::allocator<int>> mylist;

    分配器完整一点的称呼为内存分配器。读者都知道,容器里面是要装数据(元素)的,例如说装1万个A类型的对象到容器中去,那么,这1万个A类型对象,每个对象都要分配内存,每个对象占用的内存通常不会很大,但是如果要往容器中放入1万个A类型对象作为容器的元素,那么理论上可能就要进行1万次内存分配。
    通过前面的学习,对内存分配都有了一个比较深入的了解。18.3节讲解了内存池,内存池引入的主要目的就是尽量避免频繁调用底层的malloc来分配内存从而造成内存空间的浪费,因为每次调用malloc,都会多分配很多内存用于管理目的而非实际使用目的。
    所以分配器的引入主要扮演内存池的角色,大量减少对malloc的调用以减少对内存分配的浪费。
那么内存池工作机制是如何减少对malloc调用的呢?前面已经详细讲过:分配一大块内存,然后每次需要内存时(每次往容器中加入新元素时),可以从这一大块内存中拿出满足需求的一小块来使用。
    从图可以看到,系统默认为程序员提供了allocator这个默认的分配器,这是一个类模板,是标准库里写好的,直接提供给程序员使用的。
    程序员当然希望标准库里提供的这个默认allocator能够实现一个内存池功能,加速内存的分配,提高容器存储数据的效率。但是这个默认的allocator到底怎样实现的,除非读它的源码,否则并不清楚它是否是通过一个内存池来实现内存分配的,也可能它根本就没实现什么内存池,而是最终简单地调用底层的malloc来分配内存,这都是有可能的。

{
	list<int> mylist;  //双向链表
	mylist.push_back(10);
	mylist.push_back(20);
	mylist.push_back(36);
	for (auto iter = mylist.begin(); iter != mylist.end(); ++iter)
	{
		cout << *iter << endl;
		int* p = &(*iter);
		printf("%p\n", p);
	}
	mylist.pop_back(); //删除36这个元素
	cout << "断点设置在这里" << endl;
}

在这里插入图片描述

    通过结果可以看到,三个元素的地址根本就不挨着,这说明标准库给list容器提供的这个默认分配器压根就没采用内存池工作机制。
    在main主函数中,继续增加下面的代码来把尾部的元素(值为36的元素)删除掉:
    把断点设置在该行,开始调试程序,当断点停到该行时,注意在结果窗口观察36这个元素对应的内存地址(0x008FDFE0)。然后,打开“内存1”窗口输入内存地址0x01271860观察其中的内容,显示为24,这是十六进制的24,正好对应十进制的36,然后,把0x008FDFE0往回减20(减的结果是0x01271846),以方便观察到更前面的内存内容,此时如图所示。

在这里插入图片描述
    按F10键向下执行一行,也就是执行mylist.pop_back();这行来把36这个元素删除,注意观察“内存1”窗口中的变化
    可以看到,删除list容器中36这个元素后,“内存1”窗口中显示的内存红了一大片,这表示一大片的内存内容都因为删除这一个元素而受到影响。这进一步说明list容器自带的分配器并没有使用内存池技术来为容器中的元素分配内存

  4.2 分配器的使用

    分配器是一个类模板,带着一个类型模板参数。程序员一般极少会直接使用到它。分配器一般都是用来服务于容器的,但是它也是能够被直接使用的。因为本节的主要目的是帮助读者深入一点地理解和认识分配器,所以这里简单地对它进行一个使用演示。但是不建议直接使用它,因为一般分配内存使用new、delete很方便,也很足够。在main主函数中,加入如下代码

{
	allocator<int> aalloc;  //定义aalloc对象,为类型为int的对象分配内存 
	int* p = aalloc.allocate(3);   //分配器里一个重要的函数,分配一段原始未构造的内存,这段内存能保存3个类型为int的对象(12字节)
	int* q = p;
	*q = 1; q++; //第一个int给1
	*q = 2; q++; //第二个int给2
	*q = 3;      //第三个int给3
	aalloc.deallocate(p, 3); //分配器里一个重要的函数,你得记住分配了几个对象,释放时释放这么多,这很不方便,如果释放多了,会造成程序隐患或者崩溃
}

    上面的代码比较简单,用分配器分配内存,向这段内存中写入一些数据,最终释放这段内存。

  4.3 其他的分配器与原理说

    上面的allocator是标准库里提供的一个默认分配器,而且这个分配器似乎最终就是调用malloc来直接分配内存,也没有用到内存池技术,所以这种分配器可以说是徒有虚名了。
有没有其他的分配器呢?分配器的资料比较少,笔者也通过网络进行了比较详细的搜索。Visual Studio用的P.J.Plauger STL版本似乎没有提供什么分配器,但是Linux下的GNU C++(gcc、g++)用的SGI STL版本应该是带一些其他分配器的。不同的STL版本实现厂商情况各不相同,而且随着版本的升级,这些分配器也是有增有减。
    一个比较典型的使用了内存池技术的分配器可能如图所示。
    从图可以看到,该分配器使用的是内存池技术,而且使用的还不是一个内存池,而是多个内存池。看图中最上面的编号,每一个编号都可以根据需要产生出一个内存池(也就是说每个编号下面都可以挂一个内存池),这些不同编号对应的内存池用来应付申请不同大小的内存。例如,1号针对申请8字节内存,2号针对申请16字节内存,以此类推。如果申请的内存是7字节,分配器内部会处理,例如往8字节靠拢,并且从1号这里分配内存。
    下图只是一个简图,供读者学习之中做参考。实际分配器内部的工作非常复杂,绝不是看上去这样简单。

在这里插入图片描述
可以做一个总结:
    分配器就是一次分配一大块内存,然后从这一大块内存中给程序员每次分配一小块来使用,用链表把这些内存块管理起来。这种内存池的内存分配机制有效地减少了调用malloc的次数,也就等于减少了内存的浪费,同时还一定程度上提高了程序运行效率。
    既然分配器采用了内存池的技术,那么也会面临内存池的尴尬,那就是分配器所申请的内存,要是想通过delete来真正释放内存(把内存归还给操作系统)也是很难做到的。请想一想,除非整个这一大块内存全部没有分配出去或者全部回收回来,才能够把这一大块内存归还给操作系统(因为申请内存时底层是用malloc来申请一大块,所以真正释放时必然需要调用free来把这一大块全部释放掉)。
    图代表的分配器只画了4个编号,其实编号可以更多,所以这里可以想象,如果在项目中很多代码用到的容器都使用了这个分配器,那么这些容器应该是共用这一个分配器的。例如下面的代码(这里假设图的分配器名字就叫allocator):

{
	list<int, allocator<int>> mylist1;
	list<double, allocator<double>> mylist2;  //分配器上面挂的不同编号对应不同大小的内存块,应付不同大小的分配内存申请
	list<int, allocator<int>> mylist3;
}

    那分配器到底是多个容器共用还是每个容器自用主要取决于分配器代码怎样写,要是写一些静态函数来分配和释放内存,那应该就可以实现分配器中的内存被很多容器共用,要是使用普通的成员函数来分配和释放内存,那就应该是每一个容器都有自己的分配器。如果每个容器有自己的分配器,18.4.2节讲解的专门的内存池类似乎就能实现这种分配器所要实现的功能。读者如果有兴趣研究和书写自己的分配器时,应该有更深的体会。

  4.4 自定义分配器

    如果觉得系统提供的这些分配器不太能满足自己的需求,尤其是默认的分配器底层根本就是直接调用malloc来分配内存,对于频繁分配小块内存,肯定会造成内存极大浪费。所以,读者可能有自己写一个分配器的想法。笔者认为,如果有兴趣写是可以自己尝试书写的,当然,分配器本身比较烦琐,并不那么好写。
    在自己写分配器之前,需要通过搜索引擎等找一找资料,看一看自定义分配器到底应该怎样写,因为分配器的书写是有规则要求的(例如有一些接口必须要写),必须要遵照这些规则才能写正确。考虑到需求的千差万别性以及大多数人并不需要自己写一个分配器,所以也就不在本书中实现自定义分配器了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值