侯捷C++八部曲笔记(五、内存管理)

谁敢说自己的教程是万丈高楼?我敢!

所有内存管理的最终动作都要跑到malloc去,所以malloc的效率至关重要。

由于电脑初期,内存技术不行,所以设计者对内存这一块锱铢必较,几k都要争取。

C++内存使用途径:

在这里插入图片描述
即,最高阶是使用STL中的内存分配器,而STL的内存分配器实现是通过new,new[],new()等较为低阶的函数,但终归会到malloc和free这两种C函数上来,最低阶的是操作系统的内存分配函数。

对内存分配的工具概览如下:

在这里插入图片描述

基本调用:

void* p1 = malloc(512);
free(p1);

complex<int>* p2 = new complex<int>;
delete p2;

void* p3 = ::operator new(512);
::operator delete(p3);

//GNUC
void* p4 = alloc::allocate(512);
alloc::deallocate(p4, 512);

//GNUC4.9
void* p5 = allocator<int>().allocate(7); //分配7个int
allocator<int>().deallocate((int*)p5, 7);

表达式new、delete

基本用法

在这里插入图片描述

new的步骤:

  1. 申请一段指定类大小的空间:new —> operator new —> malloc
  2. 转化为对应类类型
  3. 调用类构造函数

在operator new的源码中,有个std::nothrow_t& _THROW0()参数,表示这个函数不抛异常,取而代之的是返回一个空指针,用户通过判断是否为空指针来判断是否分配成功。

在这里插入图片描述

delete步骤:

  1. 先调用析构函数
  2. 释放内存:delete —> operator delete —> free

array new、array delete

在这里插入图片描述

array new是分配一个对象数组,通常容易犯得一个错误是在delete的时候忘记在delete后面加[]导致内存泄漏的问题。正如上图所说的,对于类中没有指针的类,不加[]可能问题不大,因为没有指针的类析构函数本来也就没有什么大的作用;但是,如果有指针,忘记写[],那么delete只会触发一次析构函数,delete掉一个指针指向的内存,其他指针指向的内存就会泄露。如上图的psa析构,str2和str3指向的地址会发生内存泄漏(析构的顺序依编译器而定)。

真的得贴图才能理解得深刻一些:下面两张图分别表示有指针和没指针的对象数组分配内存的区别:

在这里插入图片描述

在这里插入图片描述

对于分配一个对象数组,他会把数组的大小也放到存放对象数组的内存块的开头,如果在delete内存的时候不加[],编译器会把他当成一个无指针的对象来析构,那么,他碰到存放数组大小的那块内存的时候,不就傻眼了吗?

replacement new

用array new调用的是类的默认构造函数,还需要对数组中的对象进行真正的构造,这就需要replacement new。允许我们将对象分配在已经构建的内存中,所以叫replacement。

他不会进行内存分配,而是调用重载的operator new,用于返回已经分配好的内存,转型、调用构造函数。

#include<new>
char* buf = new char[sizeof(Complex)* 3];
Complex* pc = new(buf) Complex(1, 2); // replacement new!!!
Complex* pc = new Complex(1, 2); 

在这里插入图片描述

--------------------------------------------------------------------------------

函数operator new()、operator delete()

重载::operator new / ::operator delete

这是全局重载:

inline void* operator new(size_t size){
	cout << "global new() " << endl;
	return malloc(size);
}

inline void* operator new[](size_t size){
	cout << "global new[]() " << endl;
	return malloc(size);
}

在类中重载::operator new / ::operator delete更有用(array new / array delete重载也是一样的方法):
在这里插入图片描述
我们重载这两个函数,是为了接管内存分配的工作,接管它了有什么用呢?很有用,比如说可以做一个内存池(这个就是之前讲STL的时候的__pool_allocator):

在这里插入图片描述
为什么重载的operator new是static呢?因为希望实现的这个内存池是这个类的所有对象都能使用的!

我们可以重载operator new()的前提是:每一个版本的声明都必须有独特的参数列,其中第一个参数必须是size_t,其余参数以new所制定的replacement arguments为初值。

在这里插入图片描述
只有在上述的重载replacement new抛出异常的时候,才会调用相应的operator delete(这个需要自己去实现),因为在重载replacement new抛出异常,那么说明内存分配不成功,但是可能已经申请好内存,那么我们应该去处理申请好的这个内存。

new handler

当operator new没有能力为你分配出你所申请的memory,会抛出一个bad_alloc的异常。抛出异常之前会先(不止一次)调用一个可由用户指定的handler

typedef void(*new_handler)();
new_handler set_new_handler(new_handler p) throw();

这个new handler一般做两件事情:

  1. 让更多的内存可用
  2. 调用abort()或者exit()

--------------------------------------------------------------------------------

分配器allocator

分配器分配途径:allocator—>allocate—>::operator new—>malloc

我们想要把operator new / delete抽取出来形成单独的一个类allocator,是的这个类和内存分配的分配细节剥离开,这样,需要内存管理的类,就调用allocator。这就是STL中分配器的实现思路。

在这里插入图片描述

在这里插入图片描述

还有一种实现方式,就是把上图黄色的这些东西定义成宏!具体的实现就不贴了,因为技术思想上没有改进,只是一个偷懒工作。

VC6的标准分配器

这个编译器的allocator没有任何特殊设计,单纯是调用malloc,分配是以对象元素为单位

在这里插入图片描述

BC5的标准分配器

和VC6相同,没有任何特殊设计。

在这里插入图片描述

GNU4.9的标准分配器

同样,没有任何特殊设计,和VC6,BC5一样。这个文件中已经说明,这个分配器没有用,容器的分配器不使用它!

在这里插入图片描述

GNU2.9的标准分配器

为什么要把他放在最后讲?因为侯先生认为它设计得最好。它和GNU4.9中的__pool_allocator相同。

下面是他的设计总览图:

在这里插入图片描述

设计是内存池的思想。16个指针指向16条链表,每条链表分配不同大小的内存空间,从左往右每一条链表间隔8个字节。理论上是这样的,但是在代码实现的时候,是首先申请一个大的内存块,然后在大的内存块上进行切分,所以不同链表之间会有连线。

每个链表会申请nx8x20x2字节的内存空间,n代表第n条链表,20表示将内存最多切分成20个子内存块,2表示会申请2倍大小的内存用于战备。这些内存空间是cookie free的。

如果申请的内存大于128字节,那么就有malloc来管理,就不归这个pool管理了。

源码中deallocate没有free操作(源于先天设计缺陷),即只要被pool申请空间之后,就不会再还回操作系统了。对于一个程序来说,这个也没有什么大不了的,但是对于一台机器来说,不止运行一个程序,这是有弊端的。这可能就是为什么GNU4.9把它替换回去的原因?

if语句中的判断,把常量写在左边!!!!!!!!!!!这样,如果把两个写成一个等号了编译就不会通过!!!!!!!!血泪教训!!!!!!!!!

--------------------------------------------------------------------------------

C函数malloc、free

malloc分配出的内存块

在这里插入图片描述
组成:

  1. 头尾两个cookie存放当前内存块大小,是否分配出的信息
  2. debug header用于debug
  3. 真实分配的内存块

分配出的内存块大小必须是16字节的倍数,也就是16字节内存对齐。

VC6和VC10的malloc比较

CRT:C RunTime
SBH:Small Block Heap

VC6:如果申请的内存小于1016,则是SBH服务,否则,用操作系统的HeapAlloc;
VC10:全部由操作系统的HeapAlloc分配内存,但是小内存的分配工作也被整合到HeapAlloc中了。

VC6内存分配细节

也不是很细节,觉得记个大概工作流程就好了,太细的过久了也会忘记。

SBH首次分配内存详细步骤

在这里插入图片描述

总体的malloc如上图。总的来说,就是为了添加debug信息,申请具体的内存,方面内存管理。

  1. _heap_init()做的事是分配16个头,16个头的数据结构如下,使用这个数据结构使得每个header能够管理1M的内存,1M内存能够快速分配,快速回收。一个header包括两个指针,一个指针指向自己管理的内存,一个指针指向管理内存中心。详细见步骤6。这些bit用于存放链表的状态,是否挂载上page。

在这里插入图片描述

  1. _ioinit()负责第一次分配内存:根据是不是debug的模式,调用不同的内存分配策略。debug直接调用malloc,非debug则分配256字节的内存:

  1. _heap_alloc_dbg()负责在debug模式下,加上一些调试信息(debug header),这些调试信息就是下面的数据结构_CrtMemBlockHeader。这些调试信息包括:调试文件名,行号,数据大小,间隔符。绿色的地方就是填充我们最后要申请分配的内存。被分配的内存会被指针串起来形成一条链表。

在这里插入图片描述

  1. _heap-alloc_base()根据申请的大小来判断(是否大于1016,为什么是1016?因为要留给cookie八字节的内存)是要自己提供服务还是交由操作体统来服务。

  2. __sbh_alloc_block()用于字节对齐,加首尾cookie。处理完之后的内存块如下图右上角的图。cookie利用最后的一个bit来代表这个内存块是否被分配出去,1表示已分配,0表示未分配。

在这里插入图片描述

  1. __sbh_alloc_new_region():内存管理中心数据结构就是tagRegion,管理区块是不是被分配,在不在内存分配链表上。为了管理好1M的内存空间,花了16K的内存(tagRegion数据结构)。

在这里插入图片描述

  1. __sbh_alloc_new_group():4K一个page。一个header管理1M内存,这个1M内存会被分成32块(组,每个组由32对指针组成,即双向链表),32块分别由内存管理中心的数据结构来管理,每一个块又会被分成8个page,一个page占4K,使用链表将这些page连起来,连起来之后首尾接到绿箭头指向的双向链表中。完成这些工作之后,申请一块内存,得到内存,初始化,内存申请就成功了。
    在这里插入图片描述

  2. 最后将__sbh_alloc_new_region组好的内存填充到组中去。层层return会让用户拿到指向绿色开头的指针。

在这里插入图片描述

SBH第二(n)次分配内存

在一个page上继续进行切分,当前page不够用,去其他page找。header负责维护的状态应该发生变化。

在这里插入图片描述

内存释放

将一个内存块的cookie中的状态置为未分配,将内存变为两个指针,指向负责管理内存的链表,header中的bit也发生变化,表示未分配。

回收相邻的内存。应该有个合并策略(这就是为什么要有下cookie的原因),要不然会出现内存碎片的问题。

在这里插入图片描述

free( p )

p属于哪个header?
p属于哪个group?
p属于哪个free-list?

内存分配总结

上述的分配策略,总的思想是一个分段管理。至于为什么有16个头,32个组,1个头管理1M内存,这些都是经验值,有利于操作系统。

全回收的动作会被延缓,并不会只要归还所有内存之后就把这么多段的内存整合还给操作体统(defer)。当第二个全回收出现的时候才会把内存归还操作系统。

--------------------------------------------------------------------------------

Loki::allocator

Loki是一个C++库,但是不太成熟,现在不太维护了。

侯先生觉得它棒,其中一点是因为pool allocator那个版本的分配器,只要从操作系统中申请了内存,就不归还,而Loki就能解决这个问题。

主要有三个类,用户使用的是SmallObjAllocator。

和pool allocator思想相似,但是用数组代替链表,用索引代替指针。(有点像用数组来实现链表的思想)

在这里插入图片描述
不太感兴趣,就不记这么细了。

那Loki是怎么解决pool allocator的无法归还内存给操作系统的问题呢?

Chunk中的deallocate用索引记录链表头,如果要释放,就把要释放的块变成链表头,并改变记录链表头的索引,如下图:
在这里插入图片描述

当一个chunk全回收的时候,就能把当前chunk还给操作系统!

--------------------------------------------------------------------------------

GNU4.9分配器

分配器各有各的优势。

new_allocator

实现出简谱的operator new和operator delete,这个好处就是能够重载。

malloc_allocator

直接调用malloc和free,没有经过operator new / delete,所以没有重载的机会。


下面两种是两种智能型的分配器。

array_allocator

允许分配一个已知且固定大小的内存块,静态分配,内存来自array object。用上这个分配器,大小固定的容器就无需在调用operator new / delete。

在这里插入图片描述

静态分配,不需要free。没啥用,因为不free,用过的空间就不能再次使用了。

debug_allocator

相当于一个warpper,可以包裹在任何分配器之上。它把用户的申请量添加了一些,然后由分配器回应,并以额外的内存存放size信息。

在这里插入图片描述


下面的三种分配器实现都是内存池的思想,追求的是不要分配过多的cookie

__mt_allocator

多线程的内存分配器。

__pool_allocator

就是GNU2.9中的标准分配器,说了好多遍了,不再赘述。

bitmap_allocator

一个高效能的分配器,使用bit-map追踪被使用和未被使用的内存块。

在这里插入图片描述
上图是一个super block
bitmap用于存放后面的block是否被分配出去
use-count表示已经分配了多少个
__mini_vector用来管理后面的block,指向block的头尾

bitmap的变化方向和_M_start变化方向相反。
如果一个super block用光了,就会新起一个super block,而且能够分配的内存会翻倍,如下图:

在这里插入图片描述

如果发生全回收,则可分配的内存减半。

全回收,使用另外一个__min_vector来登记(free list)全回收的super block,发生全回收,则分配内存减半,如下图是三次全回收的情况:

在这里插入图片描述
如果free list中已经有64个super block,下一次再free的的super block,直接delete。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值