一: new内存分配细节探秘
我门输入下边三行代码,断点掐在第一行,F10往下走,观察内存,并把内存提前40字节
char *ppoint = new char[10];
memset(ppoint, 0, 10); //观察从哪里初始化
delete[] ppoint; //观察释放影响的内存位置
我们注意了,一块内存的回收,影响的范围很广,远远不是10个字节,而是一大片,
a)比如下边这个图,红色是分出去的内存,比如我一共new了5次,当然每次new的内存大小可以不同,这第一个图有5个红色块;
b)然后我率先把第三个块我释放了,释放的块我们用绿色表示;
c)再过一会,我把第二个块也释放了;那这个时候,free函数还要负责把临近的空闲块也要合并到一起,肩负这样 一种使命,这一点大家必须要知道;
所以大家能看到,我free一个内存块,并不是一个很简单的事,free内部有很多的一些处理:合并数据块,登记这个空闲块大小,设置这个空闲块首位的一些标记以方便下次分配等等一系列工作;这是其一。
还有一个问题,不知大家是否观察到或者注意到:
我们分配内存的时候,是指明了要10个字节,但我们释放内存的时候,并没有告诉编译器我们要释放多少个字节,显然编译器肯定在哪里记录了这块内存分配出去的是10个字节,那你释放的时候编译器才能正好把这10个字节释放掉,那编译器在哪里记录着呢?我们用猜测法猜测一下;掐new处的点,然后观察,看看10的位置,下图估计是10的位置;往前数12个位置大概就是:
那我分配55字节我观察一下:
char *ppoint = new char[55]; //16进制的37
根据我们观察到的表现,我们得到一个结论:
分配内存这个事,绝不简单分配出去这10个字节,而是在这10个字节周围的内存,做了很多处理,比如记录分配出去的字节数,等等,事实正是如此;
分配内存这个事,不同的编译器下的malloc,也许各有不同,但是大同小异,细微实现上可能千差万别,但是该*事情是必须要干,该有的步骤必须有:
一般来讲,我们分配10个字节内存,编译器或者说真正负责分配内存的malloc函数可能会分配出如下的内存出来:
我们大家注意到了,我只申请了10个字节,但编译器处理的时候他要额外的增加好多信息到内存中去,比如图中的几项,一一介绍,不同的编译器可能这些项不一样,但是大家记住一个结论,就是编译器要有效的管理内存分配和回收,肯定在分配一块内存之外,额外的要往内存里加很多东西。然后你注意到了,编译器最终是把他分出去的这一大块内存大概是中间的某个位置的指针返回给ppoint,作为你能够使用的内存的起始地址 ,也就是说,你拿到的ppoint的地址,实际上是malloc所分配出去的地址中中间的某个地址。
这肯定让人很不爽,你想啊,本来我要10个字节内存,结果你一共分配出来了40好几个字节,多浪费内存啊,但是没办法,系统要能正常的管理内存(分配,回收,调试查错),它就需要这些信息!你想,你要是一次申请了1000个字节,多浪费40个,你觉得还行,不算浪费,你要分配了1个字节,结果系统一下多给你分配出来40好几个字节,你不吐血才怪。
当然了,上图不是全部,如果是对象数组的话,可能在分配内存时可能还不太一样(如果大家忘记了可以回忆一下第五章第二节,我们讲解过),但是不管一样不一样,我们这里追求的不是malloc究竟怎么*细节,我们只是要了解malloc大概干了什么事。我们得到了一个结论:分配内存时为了记录和管理分配出去的内存,额外多分配了不少内存,造成了浪费;
--------------
二:重载类中的operator new和operator delete操作符
咱们上节课学习过new操作符的调用关系,大家还记得,我们回顾一下,当咱们new一个A类对象和delete一个A类对象时:
A *pa = new A(); //操作符
operator new (011014FBh) //函数
malloc() //c语言中的malloc()函数
A::A() //有构造函数就调用
--------------------------
delete pa;
A::~A() ; //析构函数
operator delete(); //函数
free() //c语言中的free()函数
我们这些只是个调用关系,那么如果站在编译器角度,咱们把new A()和delete pa这种语句翻译成c 代码,大概应该是长这样:
void *temp = operator new(sizeof(A));
A *pa = static_cast<A *>(temp);
pa->A::A();
那delete pa这种语句,站在编译器的角度应该长什么样呢?
pa->A::~A();
operator delete(pa);
--------------
咱们现在可以自己写一个这个类的operator new和operator delete函数,来取代调用系统的operator new和operator delete,那咱们写的这两个函数里就得负责分配内存和释放内存,同时,咱们还可以往自己写的函数里插入一些额外代码,来帮助我们自己获取一些实际的利益,什么利益,后续程序会演示;
我给大家演示一下如何写一个这个类的operator new和operator delete函数,来取代调用系统的operator new和operator delete
大家请看,我们先看没接管之前的代码:类A如下
class A
{
public:
};
main中:
A *pa = new A();
delete pa;
运行起来一切正常;
我们来重载operator new和operator delete函数;看看我写,这种写法比较固定,大家是一回生二回熟,这些代码当然一般大家不会这么写,但作为向高阶c 程序员迈进,我们应该知道有这种写法;
class A
{
public:
static void *operator new(size_t size); //应该为静态函数,但不写static似乎也行,估计是编译器内部有处理,因为你new一个对象时还没对象呢,静态成员函数才和对象无关《c 对象模型探索》
static void operator delete(void *phead);
};
void *A::operator new(size_t size)
{
A *ppoint = (A *)malloc(size);
return ppoint;
}
void A::operator delete(void *phead)
{
free(phead);
}
main中内容不变,我们增加断点调试,确定可以调用operator new,和operator delete;并且我们观察operaor new传递进来的参数,发现size是1,因为这个类A本身就是1字节的大小,如果大家对为什么是1字节有疑问,还是,参考《c 对象模型探索》视频教程;
然后如果我们增加了构造和析构函数,我们也观察到构造,析构函数都被调用了;
A()
{
int abc;
abc = 12;
}
~A()
{
int abc;
abc = 1;
}
当然了,如果说你突然不想用你自己写的operator new()和operator delete(),那也可以:
A *pa = ::new A();
::delete pa;
这个两个冒号的写法是“全局操作符”,你调用这个全局操作符所代表的new和delete,那么大家也看到了,他就不会调用你重载的 operator new和operator delete操作符了;
我刚才也说了,至于这重载有什么用,咱们后续再研究,现在大家只需要知道,我们可以重载operator new ,operator delete就行了;
--------------
三:重载类中的operator new[]和operator delete[]操作符
如果我要这么写:
A *pa = new A[3]();
delete[] pa;
这种写法并不调用咱们上边的operator new和operator delete ;
那是因为,这是为数组分配内存,要重载的是operator new[]和operator delete[],我们继续写代码:
类定义中:
static void *operator new[](size_t size);
static void operator delete[](void *phead);
类后面,写实现代码:
void *A::operator new[](size_t size)
{
A *ppoint = (A *)malloc(size);
return ppoint;
}
void A::operator delete[](void *phead)
{
free(phead);
}
大家可能注意到了,说你写这个两个函数和不带数组的两个函数似乎代码相同。确实相同;
大家要特别注意这种数组操作符的调用流程:
加断点观察:operator new[]和operator delete[]只会被调用1次,但构造和析构函数,会被调用3次,这一点大家千万别搞错,不要 以为你是个数组new分配内存分配3次,而delete也被调用3次,实际测试结果是1次;
现在掐断点我们观察:
void *A::operator new[](size_t size)
我们注意到这里的size = 7,可能有些同学纳闷为什么是7字节。大家想想,你创建了一个3个对象的数组,每个对象1个字节,3个对象3个字节,那另外4个字节呢?
大家跟踪到new[]里边:
A *ppoint = (A *)malloc(size); //这里返回的是0x00d15218
return ppoint;
但是实际分配后,你注意:
A *pa = new A[3](); //这里返回的是0x00d1521c
啥意思呢,其实真正你拿到手的指针是0x00d1521c,0x00d15218实际上是编译器malloc分配内存时得到的首地址,这里1c比18是不是多了4字节,所以咱们上边看到4字节,3 4来的;
这4字节干嘛的,记录数组大小的,我数组大小为3,所以,记录的就是3,我们可以想象,释放的时候必然会 用到这个数,通过这个数字,我们才知道调用多少次构造函数,调用多少次析构函数。
所以我们发现,编译器再我们背后还是为我们做了很多事情的;
这些高级知识,大家看懂了吗?
Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved