【侯捷】C++内存管理从平地到万丈高楼(前11节学习笔记)

2 内存分配的每一个层面

对于C++的应用程序来讲,我们一般有三种调用函数的方式来分配内存;

  • malloc函数;
  • new的相关操作符和函数 ;
  • allocator的相关分配器;

在这里插入图片描述
而从图我们也知道,每一层都是对下一层的封装;也就是说,假如我直接调用标准库的std::allocator来说,也是在里面封装了new之类的m,而对于new来说,下次一层也是封装malloc;
其实我们讨论到malloc底层即可,不用再深入到O.S的API ;


C++的内存分配函数的分类:从4个层面去探讨它;
在这里插入图片描述


3 4个层面的基本用法

  • 我们知道对于malloc来说,调用它,只要告诉malloc多少个字节就可以直接分配内存;
  • 对于new来说,我们只要new一个对象即可;;
  • 其实还有一种是调用:: operator new的用法,这个::operator new还可以发生重载,并且使用它的方式也是给函数传入需要的字节数即可;但是对于::operator new的底层代码来说,也是调用了 malloc函数来的;

在这里插入图片描述
对于allocateor来说,不同的编译器的实现的接口有所不一样,这就使得我们有点讨厌;

  • 对于VC编译器来说:也是下图代码_MSC_VER那段;allocator()表示的是一个匿名对象,而int是分配的内存单元大小,allocator().allocate(3,(int*)0); allocate(3(int*)0);第一个参数表示分配了3个int大小的内存,至于第二个参数(int*)0,是一个无用的参数,至少对于现在来说,我们认为他是没有用的;
  • 对于——BORLANDC_的编译器来说:分配的使用方式也是和上面的vc差不多,但是没有第二个参数的设计,我门看上去觉得这似乎更加合理;我们知道vc低下的编译器和该编译器低下的释放内存,都需要指定指针和释放内存的大小,这就是有一把双刃剑,它很自由,但是假如你使用分配有错误的话,那么就会发生很严重的内存释放失败的问题;

对于GNU的编译器来说:其中下图的使用方式是GNU2.9的版本,但是不同的是,这个版本的调用方式是直接调用了allocate函数,也是但是分配过去不再是内存单元的大小,而是分配的字节大小了;
其实对于GNU4.9后的allocate分配器来说,书写方式和下图的方式不一样了;


在这里插入图片描述


GNU4.9的版本的allocateor的调用方式,其实GUN编译器有好多个分配器,下一显示的就是两种分配器,他们的用法都是通过对象去调用的;不再像2.9版本的GUN那样通过函数去调用;


4.基本构件之一new delete expression(上)

在这里插入图片描述
我们知道对于 new来说:是先分配内存,再调用构造函数,如上图,左边的部分,就是一个new的编译器的实现方式;由于要处理异常情况,再编译器实现new的过程中,会有一个try catch的异常处理;
并且值得关注的是,构造函数,是不可以自己调用的,而编译器而已自己调用
但是假如我们非要调用的话,那么就可以通过调用 placement new函数;至于 placement new是什么后面再讨论;
还有个注意的是:上面的 opreator new 是没有使用:: operator new的方式调用,我们知道::oprerator new 是可以被重载的,假如上面的类 Complex没有重载operator new 那么调用方式就是默认的全局那个::operator new 了;


接下来关注的是上图的右上角的部分,那个是operator new 在 vc98下的实现方式,我们要知道,不同编译器的实现可能都会有所差异,但是这个编译器的实现都可以代表了operator new的处理方式;

我们也知道,里面就是调用了底层的malloc去实现内存分配的;

我们关注opreator new的while循环里面;只要内存不足被分配失败了就会执行while循环里面的代码;
里面又调用了一个函数callnewh函数,这个函数主要是malloc内存分配失败后,去调用自己的callnewh函数,去释放你认为需要释放的内存字节,不必要的那些内存,这样,就可以再次进入循环,继续分配内存供你使用;


5.基本构件之一newdelete expression(中)

对于delete这个操作符来说:先析构再释放,在编译器底层也是调用了free来释放内存的。
在这里插入图片描述


7.Array new

在这里插入图片描述
我们知道,对于array new 一般都搭配 arrary delete使用的;但是我们有没有想过一个问题?
假如使用array new 分配内存,而不使用arrary delete释放内存呢,会发生什么?

其实我们大家都知道会发生内存泄漏,但是这个并不是一定的事情;而且内存泄漏到底在哪里发生内存泄漏可能也有人不清楚。
假如对于一个class来说,没有指针的成员,不是用arrary deleete是没有影响的;因为我们知道,delete的最终动作是先析构后释放内存,也就是说,假如你array new的分配内存,却不用 arrary delete的话,而用 delete就直接会析构一次,但是对于没有指针成员的class来说,即使你不析构也无所谓的,因为对象结束了,没有指针的成员数据也会跟着销毁,析构函数最主要是清理指针成员的堆区内存;
那么arrary delete比delete多出的动作也就是多次调用析构函数和一次调用析构函数的区别;
也就是说假如用arrary new 分配内存,用arrary delete释放内存,那么arrary会调用多次你在arrary new分配内存的元素的每个析构函数;
那么就很容易理解了:假如对于有指针成员的class来说:没有arrary delete的话,只会析构一次,也就是说,对于剩下的数组元素的对象,没有成功被析构,也就是说在这里才会发生内存泄漏;


我们其实需要知道,malloc实际分配内存来说,不单单只分配了你传入参数的字节数,其实还会多分配出一些cookie,这部分的内存是用来存放一些琐碎的内存信息;比如你用array new 分配内存,当你要释放内存时候,你是怎么知道array new到底分配了多少内存,这时候就需要有cookie来记录这些信息,其实对于数组来说,最主要的记录信息就是那个数组的长度了,到时候在释放内存的时候,就可以通过cookie里面记录的数组长度信息进行正确释放内存;


下面的测试主要说明:使用array分配内存时候和释放内存,是否调用了构造函数和析构函数;调用了多少次;
值得注意的是:要使用 array new 那么你的类中必须有默认构造函数,因为你在 new 时候,是无法有机会给它传参调用其他的构造构造的,那么只能通过有默认构造函数来完成对数据成员的初始化;

在这里插入图片描述
上面的测试结果:有一个是调用构造函数时从上往下,而析构时候是相反的;当然这个不一定所有编译器都是这样实现,不同的编译器的析构和构造顺序可能不一样;但是我们需要知道的是:能够正确分配和正确释放即可;


下图是使用array new 来分配基本数据类型int使用,释放却直接使用delete来释放的情况说明:
其实也是没有内存泄漏的,因为对于基本数据类型int来说,它这个根本不需要析构函数,也就没有所谓的内存泄漏的说法,释放内存也没什么错误;
在这里插入图片描述
我们分配上案例的结果字节大小是40,但是实际的内存分配的大小却不是,我们需要知道,右图是在vc低下的内存分配方式:观察又多出来的32字节还有12字节还有一些61h的东西,这些是什么,之后会谈;


下图的是有一个Demo类,大小为12个字节,并且还有析构函数,;我们也是使用array new去测试:发现分配3个元素,数组大小为36+4 = 40,这里多出的4个字节是做什么的呢?;
对于“有析构函数的类来说,用array new 分配内存多出了一块内存区域是4个字节记录了数组的大小从这个内存角度去解释也可以知道:假如用delete去释放 array new的时候,会发生错误,因为释放时候多出的记录数组大小的字节数不知道如如何释放了,不使用array delete打乱了释放内存的方式;其实本质就是里面的operator new 释放内存会错乱,并且里面的析构函数只析构一次,并不会发生什么问题,因为该Demo类中没有指针指向堆内存。
在这里插入图片描述
结论是对于类来说:有析构函数那么使用array new 分配内存就会多出4个字节记录大小,没有析构则不;


8.placement new

在这里插入图片描述
通过palcement new主动调用构造函数;
一般都是在一个类是用了array new 分配了内存后,需要对数组每个元素初始化,那么就可以通过 palcement new 的方式去调用该类的构造函数,达到初始化的目的;


9.重载

我们知道假如我们在应用程序写如下的代码红色框框的代码:编译器底层是绿色框框的方式实现的
在这里插入图片描述
假如我们没有自己重载 operator new 这个函数,那么编译器底层就会默认使用全局的::operator new 函数;也就是说,编译器会走如上图的2好路线;假如我们自己实现了一个operator new 那么就会走自己的operator new。


重载全局的::operator new 这种方式基本没人会这么干,因为重载全局的影响太大了,一旦你写得不好,就会很容易出大问题;
在这里插入图片描述


重载operator new 得基本格式
在这里插入图片描述


对于array new来说,operator new [ ]它的得重载格式如下
在这里插入图片描述


10.重载示例(上)

值得注意的是:重载局部的operator new时候我们都是要在类内加static;因为在创建对象时候,都没有对象,肯定不能通过对象来调用ioperator new 分配内存,所以说,只能设计static,但是实际上,就算不写static也是可以的,应该是编译器底层又默认实现了吧。

在这里插入图片描述
在这里插入图片描述


有一种很奇怪的写法:这种写法一般很少见,即这样写会跳过所有你重载的版本,直接调用的是全局的operator new;


11.重载示例(下)

当我们通过new()的方式去使用的时候,就是使用了operator new(size_t size,void* p);这个函数;
也就是说我们平时使用的:new(地址) ;这种方式底层就是把地址传给了operator new 函数的第二个参数;

重载
***
通过下图的调用方式我们也发现:是真的只有在抛出异常时候,delete()的版本函数才会被调用;
在这里插入图片描述


总结来说重载:new(),其实就是第二个参数设计为其他的数据类型,只要不是void即可,因为设计为第二参数void的operator new 是 placement new的用法了;


我们可以看到,其实在标准库的string(真名是basic_string)类中,就有重载new()的用法,并且第二参数是 extra,类型为 size_t;


  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

呋喃吖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值