C++内存管理

0 概述

  • 内存管理,从平地到万丈高楼
  • C++的内存管理分为以下几讲,分别是
    • 第一讲primitives
    • 第二讲malloc/free
    • 第三讲std::allocator
    • 第四讲other allocators
    • 第五讲loki::allocator
  • 内存管理的基础:
    • 曾经动态分配并使用memory
    • 曾经使用过C++标准库的容器

0.1 C++应用程序

  • malloc非常重要
  • 以下这张图引导大家,看你到底是落在哪个位置
  • 当你写应用程序的时候,你可能是直接调用malloc
  • 你也可能是使用new之类的,利用C++提供的基本语法操作符等得到内存(可能这些操作符底层依然是malloc)(new很熟悉,new[]是数组版本的new
  • 最高阶是使用容器,好像没感觉内存这东西的存在
    在这里插入图片描述

1 Primitives


  • 上述可以看出,C++内存管理主要是有四个层面
  • 所有层面最后都跑到了malloc
  • 以下是第一讲的内容,即C++primitives的内容
    在这里插入图片描述

1.1 Primitives的四种分配方式举例

  • 使用一下这四个分配内存方式

  • 总览
    在这里插入图片描述

1.1.1 方式一和方式二,mallocnew

  • 方式1和方式2都是很常见的内存管理分配方法,new的底层是malloc,这点在STL当中已经说过了,malloc是最常用的内存管理方式,基本用法如下:
void *p1 = malloc(512); //512bytes
free(p1);
//基本用法
complex<int> *p2 = new complex<int>;
delete p2;

1.1.2 方式三,::operator new()

  • 看看方式3,其实和方式1也就是malloc差不多,其实这个::operator new()这个语句底层服务就是malloc()提供的,下方的delete()底层是free()
void *p3 = ::operator new(512); //512bytes
::operator delete(p3);
  • 以上这种::operator new()代表的是一种全局的函数,但是格式上来说,与上两种方式还是差不多,从底层上来看,::operator new()底层就是malloc,而::operator delete()底层就是free()(稍后放出源代码)

1.1.3 方式四,allocator

  • 关于方式4,采用分配器来分配内存
  • 这里举了这么多个例子是看了不同的IDE当中的allocator的实现,各家之间稍微有些区别,但不大
1.1.3.1 MSC
#ifdef _MSC_VER
    //以下兩函數都是 non-static,定要通過 object 調用。以下分配 3 個 ints.
    int* p4 = allocator<int>().allocate(3, (int*)0); 
    allocator<int>().deallocate(p4,3);           
#endif
  • 可以看出,首先建立了一个常量,以上两函数都是 non-static,一定要通过 object 进行调用,首先分配了3个int
  • Tips:allocator<int>()表明建立了一个临时对象,其寿命在走完这条语句之后就会结束,只会存在所在的命令行
  • 使用分配器要记得拿了几个,到时候归还的时候就要还多少个,注意看allocator<int>().deallocate(p4,3); ,除了要归还指针还要归还当时借了多少个的数目,这就让人很困惑,假如你上面接了5个,结果你归还了7个?所以你得记住当时拿到了几个
1.1.3.2 BORLANDC
#ifdef __BORLANDC__
    //以下兩函數都是 non-static,定要通過 object 調用。以下分配 5 個 ints.
    int* p4 = allocator<int>().allocate(5);  
    allocator<int>().deallocate(p4,5);
  • BORLANDC中allocator的使用,与上方Microsoft Visual C++差不多,只是int* p4 = allocator<int>().allocate(5);括号中未指定类型,其实int* p4 = allocator<int>().allocate(3, (int*)0); 当中的(int*)0没多大实际用处,况且BORLANDC当中也有第二个参数,同时有默认值,这就非常合理,比MSC合理多了,MSC第二参数我们给一个我们自己都不了解的参数,没有默认值不合理
1.1.3.3 GNUC
#ifdef __GNUC__
    //以下兩函數都是 static,可通過全名調用之。以下分配 512 bytes.
    //void* p4 = alloc::allocate(512); 
    //alloc::deallocate(p4,512);   
    
    //以下兩函數都是 non-static,定要通過 object 調用。以下分配 7 個 ints.    
	void* p4 = allocator<int>().allocate(7); 
    allocator<int>().deallocate((int*)p4,7);     
	
    //以下兩函數都是 non-static,定要通過 object 調用。以下分配 9 個 ints.	
	void* p5 = __gnu_cxx::__pool_alloc<int>().allocate(9); 
    __gnu_cxx::__pool_alloc<int>().deallocate((int*)p5,9);	  
#endif  
  • 在GNUC当中,放的是字节,与上方都不一样,而且名字改成了alloc
  • alloc实现其实很巧妙,后面更新换代的时候,变成了 _pool_alloc<int>(),称为内存池,做内存管理都是用内存池的方式来做的
  • 以下两函数都是non-static,一定要通过object来调用(这里使用了临时对象),以下分配了9个int
void *p5 = _gnu_cxx::_pool_alloc<int>().allocate(9);
_gnu_cxx::_pool_alloc<int>().deallocate((int*)p5,9);

1.1.4 小结

  • 以上就是对四个Primitives的使用方法总结

1.2 new expression

  • new后面需要加上class name
  • new会做什么动作?会分配一块内存,准备放object,分配好后自动调用构造函数,当然自动被调用就是被new调用
  • new做两个动作,分配内存+调用构造函数

  • new Complex(1, 2)使用C++标准语言实现
  • 注意new Complex(1,2);那一句就是进行空间的分配,这个底层就是malloc
  • Complex* pc = new Complex(1, 2)可以被编译器看成什么呢?看成如下一系列的操作(这里有分配内存,所以必须关心分配内存失败了该怎么办,所以有try-catch):
Complex *pc;
try{
	void* mem = operator new(sizeof(Complex));//allocate
	pc = static_cast<Complex*>(mem);//cast
	pc->Complex::Complex(1,2);//construct
}
catch(std::bad_alloc)
{
	//若allocation失败就不执行constructor
}
  • 这里使用的operator new来进行分配内存,但是相比之前的版本,少了class scope::,回去看::operator new()发现是可以重载的,但是这里并没有被重载,所以调用的实际上还是::operator new(),如图右上角,底层还是malloc
  • static_cast就是做一个指针的转型
  • 之后需要调用构造函数Complex::Complex(1, 2),这个就是调用构造函数,但是用户自己并不能调用构造函数

在这里插入图片描述


  • 注意看operator new的底层,如果默认的new不行,会调用newh用户自己设定的函数,如果这边malloc失败就会来调用你设定的那个newh函数
  • 在自己设定的newh释放内存之后,以便重新调用malloc
  • 当山穷水尽的时候,内存用完了,就会调用你设定的函数newh以便释放内存然后重新malloc

1.3 delete expression

  • delete会先调用析构函数,然后会把内存释放掉,执行完毕,对象内存会被释放

在这里插入图片描述


  • 一个非常奇特的用法,通过指针调用析构函数pc->~Complex();
  • 如上的转换,也就是delete pc将会转换成pc->~Complex();以及operator delete(pc);这两语句
  • 可以看出operator delete(pc);的底层是free(p);,说到底delete还是free

1.4 调用构造函数与析构函数


  • pstr->string::string("jjhou"); 通过指针不可以直接调用构造函数,但是可以调用析构函数(但是分编译器?在VC6当中是通过的,但是在GNUC当中是不通过的)
  • 但是想要直接调用构造函数,可以调用placement new,也等同于直接调用构造函数,new(p) Complex(1,2);就是相当于直接调用构造函数了
    在这里插入图片描述
  • 问题1:为什么是class std::basic_string而不是string
  • 因为string只是一个typedef,所以真正的还是basic_string

#include <iostream>
#include <string>
//#include <memory>				 //std::allocator  

namespace jj02
{
	
class A
{
public:
  int id;
  
  A() : id(0)      { cout << "default ctor. this="  << this << " id=" << id << endl;  }
  A(int i) : id(i) { cout << "ctor. this="  << this << " id=" << id << endl;  }
  ~A()             { cout << "dtor. this="  << this << " id=" << id << endl;  }
};
	
void test_call_ctor_directly()
{
	cout << "\ntest_call_ctor_directly().......... \n";	
	
    string* pstr = new string;
    cout << "str= " << *pstr << endl;
    
//! pstr->string::string("jjhou");  
                        //[Error] 'class std::basic_string<char>' has no member named 'string'
//! pstr->~string();	//crash -- 其語法語意都是正確的, crash 只因為上一行被 remark 起來嘛.  
    cout << "str= " << *pstr << endl;


//------------

  	A* pA = new A(1);         	//ctor. this=000307A8 id=1
  	cout << pA->id << endl;   	//1
//!	pA->A::A(3);                //in VC6 : ctor. this=000307A8 id=3
  								//in GCC : [Error] cannot call constructor 'jj02::A::A' directly
  								
//!	A::A(5);	  				//in VC6 : ctor. this=0013FF60 id=5
                      			//         dtor. this=0013FF60  	
  								//in GCC : [Error] cannot call constructor 'jj02::A::A' directly
  								//         [Note] for a function-style cast, remove the redundant '::A'
		
  	cout << pA->id << endl;   	//in VC6 : 3
  								//in GCC : 1  	
  	
  	delete pA;                	//dtor. this=000307A8 
  • 可以看出这个是在VC6当中是可以调用构造函数的,但是在其他编译器当中不行
//!	pA->A::A(3);                //in VC6 : ctor. this=000307A8 id=3
  								//in GCC : [Error] cannot call constructor 'jj02::A::A' directly
  								
//!	A::A(5);	  				//in VC6 : ctor. this=0013FF60 id=5
                      			//         dtor. this=0013FF60  	
  								//in GCC : [Error] cannot call constructor 'jj02::A::A' directly
  								//         [Note] for a function-style cast, remove the redundant '::A'
		
  	cout << pA->id << endl;   	//in VC6 : 3
  								//in GCC : 1  	
  	
  	delete pA;                	//dtor. this=000307A8 
  • 欲想直接调用构造函数,可使用placement new: new (p)Complex (1,2);

1.5 array new & array delete

1.5.1 array new

  • 顾名思义,就是一次性建造一整个array的对象
  • Complex*pca = new Complex[3];
  • delete[] pca;
  • 这两个组合就是一套组合
  • delete[]注意看后面的方框,如果没有后面的方框,就只会调用一次的析构
  • 加上了[]就说明会调用三次的析构
  • 最上方的cookie记录了这整个的长度

在这里插入图片描述


  • array new得到的就是上方三个绿色小块
  • cookie有什么作用?就是记录下方的所有绿色小块的长度
  • 因为这一大块,从最底层开始是malloc给的,所以对应是free去回收
  • free只得到一个指针,怎么知道那一块是多大呢?必须知道
  • mallocfree在所有平台上,都是这么设计的,这是非常重要的一个东西,带着一个东西,记录了这里的长度,这个东西就是cookie
  • 所以操作系统总能根据cookie正确回收长度
  • 所以发生内存泄漏是只调用一次析构的那次,调用三次析构的不会内存泄漏

在这里插入图片描述


  • 虽然整体不会泄漏,但还是要养成好习惯,用array new就要使用delete[]
  • 因为之前array new的时候,不能一次性每个都去赋值,所以需要一个默认的构造函数
  • 可以看出,指针是在移动的
  • 从地址可以看出,每一个是间隔 4 4 4 个字节的
  • 在语句new(tmp++)A(i),这语句就是placement new这里就可以调用构造函数并赋初值

在这里插入图片描述


  • 可以看出构造函数是从上往下进行构造,但是析构函数是从下往上进行析构的(析构次序其实无所谓,全部析构即可)

1.5.2 内存表示

  • 实际在内存中获得的结果如下:
  • 其实malloc给了包括 10 10 10 i n t int int 以及 cookies 在内的所有空间

在这里插入图片描述


  • 这里有上cookie以及下cookie
  • 在VC6当中,整个的大小是要 16 16 16 的倍数,所以要进行填补,如上所示,填补了 12 12 12 b y t e s bytes bytes
  • 实际大小其实是60H,还有一个bit是要当作状态位,所以是 61H,但实际是60H

在这里插入图片描述


  • 这里是整数,整数的析构函数没有意义,加不加[]都一样
  • 但是假如内存里面放的是 o b j e c t object object,那么就必须加[]

1.5.3 array delete

  • 就是delete[]

1.6 placement new

  • placement new,允许我们把对象构建在一个已经分配好的内存中(所以placement new是不做事的),所以手上当然有一个指针,来指向那些个已经分配好的内存区域,相当于定点分配,如下,buf是一块已经分配好的内存,然后new一下,将对象分配到那个已经分配好的内存当中
  • 所以语句Complex* pc = new(buf)Complex(1,2);将被转化为如下的几条语句
  • 比如,void *mem = operator new(sizeof(Complex), buf);这里的buf就代表定点,同时这是两个参数的
  • placement new当中,真正做事的只有pc->Complex::Complex(1,2);,所以placement new等同于调用构造函数
  • 没有placement delete因为placement new没有分配内存
  • placement new的本质就是new(p)::operator new(size, void*);

在这里插入图片描述


  • 其允许我们将对象object构建于allocated memory当中
  • palcement new 手上要有指针代表已经分配好的内存(看那里的buf,是已经分配好的内存)
  • 对于delete而言,与之前的delete就是多了一个第二参数
  • 这就是new的一种变体,不过就是内存得提前分配,现在的不会直接分配内存

1.7 分配内存的途径


在这里插入图片描述


  • 可以看出,用户的newdelete都会被转成两个动作
Foo* p = new Foo(x);
//被转化为下面的两个动作
Foo* p = (Foo*)operator new(sizeof(Foo));
new(p)Foo(x);
//既然底层是这两个动作,那么直接写是否可以?可以的,但是没必要
//那么operator new()底层是什么呢?
::operator new(size_t);//这就是operator new()的底层
//那么::operator new()底层是什么呢?
malloc(size_t);//底层是malloc
delete p;
//被转化为下面的两个动作
p->~Foo();
operator delete(p);
//那么operator delete()底层是什么呢?
::operator delete(void*);//这就是operator delete()的底层
//那么::operator delete()底层是什么呢?
free(void*);//底层是free
//(因为要清空一段内存区域,所以需要一个指针指向具体位置)
  • 然后这个operator newoperator delete都会被转成黄色条上方的动作
  • 其实就是调用mallocfree

  • 左边容器的调用,把构造函数和析构函数进行包装,包装成construct()destory()这两个部分,本质上还是与上方的转换两步一样
  • 内存的分配和释放动作,被拉到所谓的分配器来
  • 而分配器真正的动作还是与上方两步一样
  • 分配器做的事就是如下所示,下方有一堆自由链表,大块小块的内存进行分配
    在这里插入图片描述

  • 左手边是容器的部分,容器要new一块空间进行copy

1.8 重载::operator new / ::operator delete

1.8.1 全局范围重载

  • 全局::operator new/delete重载的是可以重新定义的,但是很少见,一般是重载成员函数的,比如Foo::operator new(size_t);

在这里插入图片描述


  • ::operator new 接受size_t size
  • ::operator delete接受*ptr
  • 里面会调用myAllocmyFree
  • 然后这两个函数底层又是mallocfree

在这里插入图片描述


  • 这是全局的版本重载

1.8.2 在类里重载

  • 更有用的是在类里面去重载

在这里插入图片描述


  • 看出delete的第二参数可有可无

在这里插入图片描述


  • array的版本都是差不多的

1.8.3 重载示例


在这里插入图片描述


  • 仍然是用mallocfree 去分配/释放内存

  • 重点看的是,假如传递一个 a r r a y array array 参数,能否进入正确的重载函数当中,比如传入一个 a r r a y array array 是否能调用 static void* operator new[](size_t, size);
    在这里插入图片描述

  • 如果使用 c l a s s class class s c o p e scope scope 符号::则会绕过前面所有的重载版本,转而使用全局的版本

1.8.4 重载 p l a c e m e n t placement placement n e w / d e l e t e new/delete new/delete


  • 这个小括号里面不止放一个指针,可以由用户自定义,这就是重载,而只放了一个指针的版本就是标准库先写好的版本
  • 比如:Foo* pf = new(300, 'c')Foo;
  • 那么像 Foo* pf = new(300, 'c')Foo; 算不算是 p l a c e m e n t placement placement n e w new new 呢?现在统一术语,带着小括号的都是 p l a c e m e n t placement placement n e w new new
  • 但是每一个这样的版本,都需要有独特的参数列
  • 其中的第一参数size_t
  • 其余的参数以 p l a c e m e n t placement placement a r g u m e n t argument argument 为设计,例如 (300, 'c') 就是对应的 p l a c e m e n t placement placement a r g u m e n t argument argument

在这里插入图片描述


  • 具体示例
    在这里插入图片描述
  • 通过以上报错信息可以充分认识到第一参数的重要性

在这里插入图片描述


  • 注意看序号5那里抛出了异常,因为已经分配好内存,再调用构造函数,可能会出错,所以抛出异常,看能不能满足

2 allocator

  • 之前基本元素的内存管理(行为以及重载的方式)已经讲完了,基本元素就是什么newplacement newarray new等等,现在终于可以针对一个class来写出它的内存管理,但是这些东西的底层,都是去调用malloc
  • 但是,如果当new次数很多的时候,相应的就会调用很多次malloc不管怎样减少调用malloc的次数总是很好的

在这里插入图片描述


  • 所以我们作为 F o o Foo Foo 的设计者,能不能够先挖一大块内存来做准备,然后自己去切一些小块,比如一下直接挖 1000 1000 1000 个,直接 malloc(1000),然后再在这 1000 1000 1000 个里面去挖出一个个的小块,这样,在供应使用者的时候就很快,不用每次去调用 malloc

  • 如下方的小图,就是先挖一大块,然后切成一个个的小块

  • 这样速度就快,这样就是一个小型的内存管理
    在这里插入图片描述

在这里插入图片描述


  • 现在不光想降低malloc的次数还得降低cookies的使用量(因为一次 malloc 就得到两个 cookies
  • 实际上内存管理就是要解决两个问题,一个是速度的提升(一次切一整块),一个是降低空间的浪费(减少 cookies 的浪费,因为切一整块,只是头尾带了cookies

在这里插入图片描述


  • 注意看这个,一次malloc之后,带的 cookies 也只有头尾带了,比以前每一次new之后再malloc所得到的 cookies 减少很多

2.1 版本1:per-class allocator


在这里插入图片描述


  • 可以看出,这里为了降低malloc次数以及去除cookies,其使用的是多添加了指针来消除cookies,但是为了去除cookies导致膨胀率是 100 % 100\% 100%,这种设计就是多耗费了一个指针

  • 结果就是,确实减少了每一个之间的 cookies
  • 然后使用全局的,就是会多出 cookie
    在这里插入图片描述

2.2 版本2:改进的per-class allocator

  • 上述是添加指针来消除多余的 cookies,现在示例2是不使用这个指针来消除cookies

  • 先看 d a t a data data 版本,一个 u n s i g n e d unsigned unsigned l o n g long long 还有一个 c h a r char char,加起来是 5 5 5 个字节,但是编译器一般都会设置对齐,齐位,所以这 5 5 5 个字节会被调整成 8 8 8 个字节,这 5 5 5 个字节包装成一个 s t r u c t struct struct
  • 还有一个 u n i o n union union u n i o n union union 就是同一个东西用不同的名称去表现他, u n i o n union union 这里有两个 m e m b e r member member,但是这两个 m e m b e r member member 其实是同一块东西,一个 m e m b e r member member 就是刚刚的那 5 5 5 个字节(齐位成了 8 8 8 个字节),另外一个 m e m b e r member member 就是把他看成一个指针,也就是说本来 8 8 8 个字节,现在看成指针( 4 4 4 个指针),也就是只看前 4 4 4 个字节
    在这里插入图片描述

  • 作用其实是与第一个版本差不多的,但是,好就好在,这里使用了 u n i o n union union u n i o n union union 当中的指针,是借用 s t r u c t struct struct A i r p l a n e R e p AirplaneRep AirplaneRep 当中的前 4 4 4 个字节来当指针用,这种做法太妙了,减少了空间的浪费
  • 这就是嵌入式指针

在这里插入图片描述


  • 在这里,并没有一个个的 free,这里只是将区块一个个回收到单向链表当中,这样子好吗?不还给操作系统,肯定不好,但是要还的话技术难度很高,但是也没发现内存泄漏,因为只是没还,内存只是到单向链表里面去了,所以不好评价
  • 同样,这也是没有 cookies的,但是还是有一个遗憾,就是没有把内存还给操作系统,如果以后能改进就好了

2.3 版本3:static allocator

  • 如果有很多个 c l a s s class class,那么就需要写很多套上面示例 2 2 2 a l l o c a t o r allocator allocator,这里重复性动作太多,从内存的角度来看并不合适,同样的内容,不应该集中到一个地方去吗? c l a s s A class A classA 写一套, B C D E BCDE BCDE 又写一套,重复内容太多了
  • 这次改进,就把重复的内容抽取出来,放到一个 c l a s s class class 里面,这个 c l a s s class class 叫做 a l l o c a t o r allocator allocator

在这里插入图片描述


  • 下方, a l l o c a t o r allocator allocator 就想象成一个指针,指向一条链表,专门为 c l a s s class class 服务,下方的两个 m e m b e r member member f u n c t i o n function function n e w 、 d e l e t e new、delete newdelete 就不做了,交给 a l l o c a t o r allocator allocator 的实例化去做
  • 在下方, a l l o c a t o r allocator allocator 为什么要设置成 s t a t i c static static?因为这个 a l l o c a t o r allocator allocator 是专门为这个 c l a s s class class 服务的,所以设置成 s t a t i c static static
  • 意思就是每一个 c l a s s class class 内部都有一个专门为其服务的 a l l o c a t o r allocator allocator,这个 a l l o c a t o r allocator allocator 里面就有一个单向链表,链表每个区块就是 c l a s s class class 的大小

在这里插入图片描述


2.4 版本4:macro static allocator

  • 就是将左手边黄色部分的代码变成右手边蓝色部分的代码
  • 就是规范一下制式,这就是 m a c r o macro macro s t a t i c static static a l l o c a t o r allocator allocator
  • 设计好 m a c r o macro macro s t a t i c static static a l l o c a t o r allocator allocator,就是 #define DECLARE_POOL_ALLOC()
  • 具体如下,放到 define 里面去,为了偷懒doge
// in class definition file
class Foo {
   DECLARE_POOL_ALLOC()
public: 
	long L;
	string str;
public:
	Foo(long l) : L(l) {  }   
};
//in class implementation file
IMPLEMENT_POOL_ALLOC(Foo) 


//  in class definition file
class Goo {
   DECLARE_POOL_ALLOC()
public: 
	complex<double> c;
	string str;
public:
	Goo(const complex<double>& x) : c(x) {  }   
};
//in class implementation file
IMPLEMENT_POOL_ALLOC(Goo) 

2.5 global allocator


在这里插入图片描述


  • 在标准库当中,有 16 16 16 个自由链表,可以应付 16 16 16 种不一样的大小,这是全局的,不是针对某一个类来服务,是针对全局的类服务的,而之前的版本 3 3 3,是per-class static allocator,这是一个allocator针对某一个类去服务的,一个分配器针对一个类,所以与全局的不一样

2.6 new handler

  • operator new 没能力为你分配出你所申请的 m e m o r y memory memory,会抛出一个 std::bad_alloc exception(异常),一些老旧的编译器则是返回 0 0 0,养成好习惯,看分配内存后检查指针是否为 0 0 0
  • 如果一定要 return 0; 也可以使用 new(nothrow) Foo; 这样之后,就一定会 return 0; 而不是抛出异常
  • 重要的是, C + + C++ C++ 在抛出异常之前,会先调用一个自我设定的函数(不止一次的调用),比如说:typedef void(*new_handler)(); new_handler set_new_handler(new_handler p) throw();
  • 这种形式:返回值是 v o i d void void,没有参数的函数,只要抛出异常之前,都会调用你这个函数,所以 C + + C++ C++ 通过调用你的函数,来通知你,由你来决定怎么办

在这里插入图片描述


  • V C VC VC 底下,operator new 的源代码,这里面就有 while loop 不断调用 malloc
  • 所以,不管什么形式,用 new 也好,operator new 也好,最终都是跑到 malloc,一旦失败,按上面的情况就是抛出异常咯?但是会先调用 typedef void(*new_handler)(); new_handler set_new_handler(new_handler p) throw();,调用完了之后, V C VC VC 底下叫 _callnewh ,顾名思义就是调用 new handler,调用完了回来再分配一次,就是看看你有没有什么补救措施,既然调入了 new handler,就代表没有内存可用了,释放完了之后,再调用new handler,看看有没有内存可用
  • 所以说,设计良好的 new handler 只有两个选择:
    • 让更多 memory 可用
    • 调用 abort()exit()

2.6.1 举例

#include <new>
#include <iostream>
#include <cassert>
namespace jj13
{
void noMoreMemory()
{
    cerr << "out of memory";
    abort();  
}
	
void test_set_new_handler()
{	
	cout << "\n\n\ntest_set_new_handler().......... \n";

    set_new_handler(noMoreMemory);

/*
    int* p = new int[100000000000000];   //well, so BIG!
    assert(p);

    p = new int[100000000000000000000];  //[Warning] integer constant is too large for its type
    assert(p);
*/

}
} //namespace

在这里插入图片描述


  • 看,这里使用了 abort(),符合上述两个条件之一

2.7 = default, = delete


  • default 就是默认
  • 这两个不只适用于构造,析构,等,还适合 operator new/delete, []
  • operator new/delete 怎么有 d e f a u l t default default 版本?
    在这里插入图片描述

在这里插入图片描述


3 std::allocator 标准库的 allocator

3.1 V C 6 VC6 VC6 malloc()


在这里插入图片描述


  • malloc() 所得到的区块,是带着 cookies 的,但是一次又一次的 malloc,就会一直产生不必要的 cookies
  • 那么如何去除 cookies

3.2 V C 6 VC6 VC6 allocator 实现

class allocator 
{
private:
  	struct obj {
    	struct obj* next;  //embedded pointer
  	};	
public:
    void* allocate(size_t);
    void  deallocate(void*, size_t);
    void  check();
    
private: 
    obj* freeStore = nullptr;
    const int CHUNK = 5; //小一點方便觀察 
};

void* allocator::allocate(size_t size)
{
  	obj* p;

  	if (!freeStore) {
      	//linked list 是空的,所以攫取一大塊 memory
      	size_t chunk = CHUNK * size;
      	freeStore = p = (obj*)malloc(chunk);  
      
      	//cout << "empty. malloc: " << chunk << "  " << p << endl;
     
      	//將分配得來的一大塊當做 linked list 般小塊小塊串接起來
      	for (int i=0; i < (CHUNK-1); ++i)	{  //沒寫很漂亮, 不是重點無所謂.  
           	p->next = (obj*)((char*)p + size);
           	p = p->next;
      	}
      	p->next = nullptr;  //last       
  	}
  	p = freeStore;
  	freeStore = freeStore->next;
   
  	//cout << "p= " << p << "  freeStore= " << freeStore << endl;
  
  	return p;
}
void allocator::deallocate(void* p, size_t)
{
  	//將 deleted object 收回插入 free list 前端
  	((obj*)p)->next = freeStore;
  	freeStore = (obj*)p;
}
void allocator::check()
{ 
    obj* p = freeStore;
    int count = 0;
    
    while (p) {
        cout << p << endl;
		p = p->next;
		count++;
	}
    cout << count << endl;
}
//--------------
  • 最主要的就是 a l l o c a t o r allocator allocator d e a l l o c a t o r deallocator deallocator 那两个成员函数
  • V C VC VC 的分配器当中是以 i n t int int 为分配单位的,这与之后的 G N U C GNUC GNUC 不太一样
  • G N U C GNUC GNUC 是以 B y t e s Bytes Bytes 为单位的

3.3 B O L A N C BOLANC BOLANC allocator 实现

  • B O L A N C BOLANC BOLANC 里面,使用的分配器也是什么都没有做,还是 m a l l o c malloc malloc f r e e free free
  • 而且现在分配内存都是使用容器,发现容器分配出来的空间,每一块都带有 c o o k i e s cookies cookies ,而我们现在就是要去除这些 c o o k i e s cookies cookies
  • 去除 c o o k i e s cookies cookies 有一个先决条件,就是区块要有一样的大小,如果区块有大有小,你怎么去除 c o o k i e s cookies cookies?因为 c o o k i e s cookies cookies 就是在记录区块的大小,你怎么去除他?所以为了去除 c o o k i e s cookies cookies,就必须把区块设置成一样大小,这样 c o o k i e s cookies cookies 才能被去除

在这里插入图片描述


3.4 G N U C GNUC GNUC allocator 实现


  • 2.9 版本的 G N U C GNUC GNUC 的分配器实现,也不过就是这样而已

在这里插入图片描述


  • 不要使用<defalloc.h>这个头文件
  • 又说 G N U C GNUC GNUC 使用的是不同的 a l l o c a t o r allocator allocator
  • 现在的这个头文件,并没有被 include 到任何容器里

  • 分配器用的都是 a l l o c alloc alloc
  • a l l o c alloc alloc 是一个类
  • alloc::allocatealloc 的一个静态函数
  • alloc::allocate(512) 这里指的是 512 512 512 字节
    在这里插入图片描述

在这里插入图片描述


  • 在2.9版本的 a l l o c alloc alloc 到了 4.9 版本就换成了 p o o l a l l o c _pool_alloc poolalloc
  • 并且变成了一个编制外(编制内的就是标准的 a l l o c a t o r allocator allocator)的东西,把好东西( a l l o c alloc alloc)换掉了

3.5 G N U C GNUC GNUC 标准 allocator 实现


  • 标准分配器就是 std::allocator
    在这里插入图片描述

  • 在 4.9 版本当中, a l l o c a t o r allocator allocator 继承了 a l l o c a t o r b a s e _allocator_base allocatorbase,然后 a l l o c a t o r b a s e _allocator_base allocatorbase 有一个 define
  • #define _allocator_base __gnu_cxx::new_allocator
  • 而这个new_allocator 就是上面的 new_allocator 这个类
  • 所以说,相当于 std::allocator 继承了 new_allocator
  • 4.9版的标准 a l l o c a t o r allocator allocator 也没有很大的改动,还是 ::operator new 以及 operator::delete
  • 那么,如何具体使用编制外的更好的分配器呢?
  • 举个例子,这样使用那个分配器,vector<string, __hnu_cxx::__pool_alloc<string>>vec;
  • 这样就是去除了 c o o k i e s cookies cookies,相当于造了 100 W 100W 100W 个元素,然后省掉了 800 W 800W 800W 个字节

3.6 G N U C GNUC GNUC 版本 allocator 实现

template<typename Alloc> 
void cookie_test(Alloc alloc, size_t n)                                                                                
{
    typename Alloc::value_type *p1, *p2, *p3;		//需有 typename 
  	p1 = alloc.allocate(n); 		//allocate() and deallocate() 是 non-static, 需以 object 呼叫之. 
  	p2 = alloc.allocate(n);   	
  	p3 = alloc.allocate(n);  

  	cout << "p1= " << p1 << '\t' << "p2= " << p2 << '\t' << "p3= " << p3 << '\n';
	  	
  	alloc.deallocate(p1,sizeof(typename Alloc::value_type)); 	//需有 typename 
  	alloc.deallocate(p2,sizeof(typename Alloc::value_type));  	//有些 allocator 對於 2nd argument 的值無所謂  	
  	alloc.deallocate(p3,sizeof(typename Alloc::value_type)); 	
}
  • 下面这个 //1 代表这个 class 里面没有任何 data
cout << sizeof(std::allocator<int>) << endl;			//1
cout << sizeof(__gnu_cxx::new_allocator<int>) << endl;	//1. 

在这里插入图片描述


  • 通过结果可以看出,在每一个指针的间隔,都是间隔 8 8 8 个字节,这就省去了 c o o k i e s cookies cookies 记录的烦恼,所以可以直接省略 c o o k i e s cookies cookies
  • 而标准分配器,则是 16 16 16 字节,所以这分配器是个好东西,省略了 cookies

3.7 G 2.9 G2.9 G2.9 std::alloc 运行模式


在这里插入图片描述


  • 图的右手边都是容器,这些容器用的都是 alloc 这个分配器,而且,分配器当中都必须包括两个重要的函数,也就是 allocate()deallocate()
  • 所以容器都和 alloc 去要这两个函数,那么这个分配器怎么回复容器的需求呢?
  • 在第一章的时候,我们都是将分配器变成某一个 c l a s s class class 里面专属的分配器,里面只维护一条自由链表,专门用来维护这个 c l a s s class class
  • 现在不一样,现在是把自由链表全部收集到一起,维护 16 16 16 种大小(此处是 16 16 16,超过这个范围的(此处是超过 16 16 16),就不为你服务,超过范围后,就会调用 malloc() 扩大内存范围,再接着调用 alloc
  • 每一个间隔都是 8 8 8 个字节(比如容器发送的需求是 32 32 32 字节,就直接挂载到 # 3 \#3 #3 这个下面),如果容器的需求不是 8 8 8 的倍数,假如是 6 6 6,也会被相应的调整到 8 8 8,这个设计在所有的分配器上都一样,一定要把需求调到一个边界
  • 那假如现在容器的需求是 32 32 32 字节,那么需要多少个 32 32 32 字节呢?这个 alloc 会直接调用 malloc 一次性切 20 20 20 32 32 32 字节,至于为什么是 20 20 20 个,没有什么依据(其实这准备的是 20 ∗ 2 20 * 2 202 个,一半的 20 20 20 个直接切,另外的 20 20 20 个留着储备,比如上方的例子,用完 20 20 20 32 32 32 后直接在储备上挂载了 64 64 64 字节的块)
  • 以上是 allocate() 的作用,回收的话就直接 deallocate() 进行回收

3.8 嵌入式指针


在这里插入图片描述


  • 在每个区块的前 4 4 4 个字节作为嵌入式指针(这个对象本身必须大于等于 4 4 4 个字节)
  union obj {
    union obj* free_list_link;
  };

3.9 std::alloc运行一瞥

private:
  static obj* volatile free_list[__NFREELISTS];
  static size_t FREELIST_INDEX(size_t bytes) {
    return (((bytes) + __ALIGN-1)/__ALIGN - 1);
  }

在这里插入图片描述


  • alloc 其实是一个 typedef
  • 是一个模板
  • 直接用分配器的话要记住有多大,因为到时候还得等价还回去
  • 所以分配器是给容器用的而不是给用户用的,因为容器都是一样的大小

在这里插入图片描述


  • 空间一下子开辟双倍,分为备份与使用
  • p o o l pool pool 当中有余量的先在余量切割

3.10 修改源代码

  • 把系统内存修改只有 10000 10000 10000
  • 29
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值