目录
一.c/c++内存分布
1.c/c++内存区域划分
2.各个区域的功能
3.注意
二.扩展(linux下malloc&free是如何实现的?)
1.malloc和free的运行原理,sbrk,brk.
2.模拟实现malloc和free
三.c++动态内存管理
1.来源
2.操作方式(内置类型)
3.new和delete操作自定义类型
4.思考
四.operator new与operator delete函数
1.概念
2.operator new
3.operator delete
4.扩展
五.定位new表达式(placement-new)
1.概念
2.使用格式
3.使用场景
4.例子
我们直接来看c/c++中的内存区域划分.
①.内核空间: 操作系统内核代码的运行空间.
②.栈: 又叫做堆栈,非静态局部变量/函数形参/返回值/表达式中间结果/某些寄存器信息等等,栈是向下增长的.
③.内存映射段: 是高效的I/O映射方式,用于装载一个共享的动态内存库,用户可以使用系统接口创建共享内存,进行进程间通信.
④.堆: 用于程序运行时动态内存分配,堆是向上增长的.
⑤.数据段: 存储全局数据和静态数据.
⑥.代码段: 可执行的代码/只读常量.
1.堆大小受限于操作系统,而栈空间一般由系统直接分配.
2.频繁的申请空间和释放空间,容易造成内存碎片,甚至内存泄漏,栈区由于是自动管理,不存在此问题.
原因: 我们在堆上动态申请的空间是连续的,而由系统直接分配的栈中的空间是分布式的,不是连续的.
3.栈可以通过函数_alloca进行动态分配,不过注意,所分配空间不能通过free或delete进行释放.
要想了解linux底下malloc和free是如何实现的,我们必须要知道malloc和free的执行原理以及sbrk和brk这两个系统调用函数.
①.malloc&free的执行原理
a. 假设我们使用malloc申请一块空间: int* ptr=(int*)malloc(sizeof(int)); 此时系统会给我们从堆上分配空间,我们申请的是四个字节的空间,但实际占用的并不是四个字节,malloc函数会在我们所申请的四个字节的空间后再增加相应的大小的空间来存储我们所申请的这个空间的大小,以及我们所申请空间相关的信息,另外在这个空间后面还会再增加一块空间用来检测我们所使用的空间是否会越界,因此我们一但使用malloc申请了一块空间我们所得到的空间的大小并不是只有申请的这些,还包括后面的两块空间.(因此当我们申请的空间很小的时候是不建议使用malloc的)
b. 假如我们在自己所写的程序中需要频繁的使用malloc来申请空间,如果每次申请都是从内存中申请的话这样频繁的占用内存将会十分影响我们的程序的运行效率,因此系统为了解决这个问题搞出了一个叫内存池的东西,假如我们在自己编写的程序中第一次使用malloc申请空间不论你申请的空间是大还是小,系统都会先给你分配33页的空间(一页的大小大概率是4kb,当然在不同的系统上可能不一样),当我们所申请的空间大于33页时,后面系统会以一页一页的方式继续进行分配直到满足所申请的空间的大小或者超过系统所能分配的最大的限度为止. 如果我们所申请的空间小于33页时,就会在这以分配好的33页空间中按malloc的执行流程进行创建并返回初始地址,而我们后续继续使用malloc申请空间时依旧会在这33页内存中上次所申请的空间的末尾继续进行分配,无需重新从内存中申请,这样就可以很好的解决我们频繁的使用malloc从内存中申请空间而降低程序效率的问题.
例子: 下面我们写一小段程序验证一下,直接使用malloc申请一个字节的空间,然后循环向后访问空间,33页,每页4kb,每kb1024个字节,因为操作系统可能会在这33页空间中进行某些信息的存储,因此我们不去访问全部的33页,只进行32页的访问即可,也能说明问题,程序完成后,直接编译运行,发现运行正常,程序并没有崩溃或报错.
代码截图:
运行截图:
c.当我们在自己的程序中多次使用malloc申请和释放空间时都会出现这种问题,假如我们分别申请了四次空间其大小分别为: 1.四字节 2.四字节 3.四字节 4.四字节,操作系统底层是以带头结点的双向链表的形式组织这些空间的(具体可看下面malloc/free的模拟实现),因此释放的时候会遵循相应的规律,当我们所要释放的空间到双向链表的末尾位置的所有空间都是空闲状态的时候,才会将这些空间统一释放,否则的话只会将这块空间置为空闲状态,而我们下次再申请空间的时候系统会按我们所申请空间的大小遍历这个双向链表,如果有空闲的空间的大小大于或等于我们要申请的空间的话,系统就会将这块空间的状态重新置为使用状态,并将其首地址返还给用户.(正因为有这条设定,所以我们如果频繁的使用malloc和free申请和释放空间的话一定会产生内存碎片)
例子: 我们先申请三个四字节的int类型的空间并打印其地址,然后释放第二个申请的空间,此时这块空间到双向链表末尾的所有空间并不是都是空闲状态,因此只能将其置为空闲状态,此时我们再申请一块一个字节的char类型的空间,系统遍历双向链表发现刚才第二块int类型所占的空间不仅大于1个字节,并且还是空闲的,因此将其分配给我们来使用,此时编译并运行程序我们发现,a2和a4的地址相同.
代码截图:
运行截图:
②. sbrk
intptr_t我们将其看做int就行,返回值为指针类型,参数就是我们所申请空间的字节数.
sbrk是一个linux下的系统调用函数,malloc内部就是调用sbrk实现的,该函数是在内部维护了一个p指针,指向当前堆内存的最后一个字节的下一个位置,该函数是根据增量参数调整指针的位置,同时返回指针原来的位置,若发现页耗尽或者空闲,则自动追加或取消页的映射.换就话说: 假如p指针现在指向0x00000001的位置,我们调用sbrk(5),此时申请5个字节的空间,该函数会控制p指针将当前所指向的位置的地址返还给我们,然后p指针向后移动5个字节走到0x00000006的位置,因此我们所得到的的地址为0x00000001,并且从其开始到0x00000006之前的5个字节的空间都可以供我们使用,当我们调用sbrk(0)时其直接返回p所在的位置,指针并不会移动.当我们调用sbrk(-5)时,此时返回值为NULL,p指针向回移动5个字节,将其后面的这5个字节的空间释放. 当我们第一次使用sbrk的时候系统会给我们分配33页,但我们所使用的空间超过33页时,sbrk会自动在页表中增加新的进程虚拟地址空间到真实物理内存的页映射,当sbrk发现这一页空间中的所有位置都是空闲的时候,则会自动取消页表中这一页的映射.
③.brk
返回值为int类型,参数是指针类型.
该函数一般配合sbrk一起使用,free的实现就是调用该系统调用函数,与sbrk相同其内部维护的依然是一个p指针,指向当前堆内存的最后一个字节的下一个位置,brk函数根据指针参数设置指针p的位置. 换句话说: 假设我们先使用sbrk(4)申请一块四个字节的空间,此时返回给我们的地址为: 0x00000001,而p指针所指向的位置为0x00000005,此时我们来使用brk申请四个字节的空间brk(0x00000009),此时从0x00000005~0x00000009之间的这四个字节的空间就可以供我们使用,因此我们知道,brk是可以直接将指针移动到一个指定的位置从而达到释放和申请空间的目的,其参数就是我们要将p指针指向的位置,要注意的是我们第一次申请空间不能使用brk函数,因为我们并不知道真实物理内存在页表中对应的进程虚拟地址空间中的虚拟地址是多少,所以第一次申请空间必须使用sbrk,另外brk和sbrk相同,其内部都可以对空闲/耗尽的页,自动的取消/追加页映射.(虽然这两个函数都可以完成空间的申请和释放,但一般都是使用sbrk申请空间,使用brk释放空间,malloc和free中同样也是如此)
注意:我们只是模仿malloc和free的运行方式进行简单实现,和真实的malloc和free差距还是很大的,但运行机制相似.
代码:
#include
#include
//我们以双向链表的方式管理申请的空间,将创建一个结构体来存
//储申请空间的相关信息,并将其放在所申请空间之后.
//存储所申请的空间相关信息的结构体
typedef struct mem_control_block{
size_t size;//所申请的空间大小
int isNull;//所申请的空间状态
struct mem_control_block* prev;//指向前一个结构体
struct mem_control_block* rear;//指向后一个结构体
}MEM;
#define MEMSIZE sizeof(MEM)//存储空间信息的结构体大小
MEM* g_top=NULL;//双向链表的头结点
//申请内存
void* my_malloc(size_t size){
MEM* cur;
for(cur=g_top;cur!=NULL;cur=cur->prev){
//遍历整个双向链表,判断有没有空闲的空间可以满足我们新申请的这块空间的大小.
if(cur->isNull==0 && size<=cur->size){
//如果存在,将这块空闲的空间状态置为使用,并返回其地址.
cur->isNull=1;
return (void*)((char*)cur-cur->size);
}
}
void* ptr=sbrk(size+MEMSIZE);
//如果不存在,则使用sbrk重新申请新的空间.
if(ptr==(void*)(-1)){//申请失败返回-1.
return NULL;
}
//将我们记录sbrk申请的空间信息的结构体,作为节点加入双向链表(尾插).
//1.计算结构体的起始地址
cur=(MEM*)((char*)ptr+size);
//2.初始化结构体中的内容,所申请空间的大小:size,所申请空间的状态:1.
//prev指针指向前一块结构体,rear指向NULL(因为该节点在链表的末尾)
cur->size=size;
cur->isNull=1;
cur->prev=g_top;
if(g_top!=NULL){
g_top->rear=cur;
}
cur->rear=NULL;
//g_top后移,方便我们下一次申请新空间时进行尾插.
g_top=cur;
//返还首地址给用户.
return ptr;
}
//释放内存
void my_free(void* ptr){
//判断传进来的指针是否为空,如果为空直接返回.
if(ptr==NULL){
return;
}
MEM* cur;
int flag=0;
//遍历双向链表,找到我们所要释放的这块空间,将其状态置为空闲:0
for(cur=g_top;cur!=NULL && flag==0;cur=cur->prev){
if((void*)((char*)cur-cur->size)==ptr){
cur->isNull=0;
flag=1;
}
}
//再次遍历双向链表,判断是否有从链表尾部开始向前的连续的空闲空间
//有,则使用brk对其进行释放. 无,则直接返回.
for(cur=g_top;cur->prev!=NULL && cur->isNull==0;cur=cur->prev){}
//因为我们遍历算法的原因,此处我们需要判断当前cur所在的位置的空间是否空闲.
if(cur->isNull==0){
//如果空闲直接全部释放,并移动g_top的位置到新的链表末尾.
g_top=cur->prev;
brk((void*)((char*)cur-(cur->size)));
}else if(cur->rear!=NULL){
//如果不是空闲,并且cur不在链表末尾,则将该空间后面的空间全部释放
//并移动g_top到新的链表末尾.
g_top=cur;
brk((void*)((char*)(cur->rear)-(cur->rear->size)));
}
}
//测试
int main(){
int* a1=(int*)my_malloc(sizeof(int));
int* a2=(int*)my_malloc(sizeof(int));
int* a3=(int*)my_malloc(sizeof(int));
printf("a1=%p\n",a1);
printf("a2=%p\n",a2);
printf("a3=%p\n",a3);
my_free(a2);
int* a4=(int*)my_malloc(sizeof(int));
printf("a4=%p\n",a4);
my_free(a3);
my_free(a4);
my_free(a1);
return 0;
}
运行截图:
运行图解:
讲解: 首先我们创建一个结构体指针指向空,也就是图中的第一块,用其来作为头结点,要知道我们虽然是使用链表来组织空间的,但空间实际上是连续存在的,模拟申请第一块空间大小为size,使用sbrk来申请空间的实际大小为size+sizeof(MEM),MEM中的prev指向null,rear指向null,MEM中的成员size只记录前面的空间大小size,此时g_top指针指向MEM的首地址,而sbrk_ptr指向空间的末尾,如果需要继续申请新的空间,则依旧使用sbrk进行申请,申请完之后,将MEM中的prev,rear指向正确的位置,链如双向链表中,如果需要释放空间则从g_top开始先前遍历链表,如果有从末尾开始连续的空闲空间则将其释放,如果没有则走到我们所要释放的空间处将其空间状态置为空闲,后面再要申请空间时则需再次遍历链表,判断空间中是否有空闲的空间满足我们的需要,有的话将这块空闲的空间状态置为使用,并返还给用户首地址,没有的话,则使用sbrk申请新的空间并加入链表中.
C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力而且使用起来比较麻烦,因此C++又提出 了自己的内存管理方式:通过new和delete操作符进行动态内存管理(new和delete的底层仍是用malloc和free实现的)。
注意:申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[]和 delete[]
1.动态申请一个int类型的空间.
int* ptr=new int;
2.动态申请一个int类型的空间并初始化为10.
int* ptr=new int(10);
3.动态申请10个int类型的空间.
int* ptr=new int[10];
4.动态申请10个int类型的空间并初始化.
int* ptr=new int[10]{1,2,3,4,5,6,7,8,9,0};
5.使用完之后切记释放空间: ①.delete ptr ②.delete[] ptr
a.操作方式
b.区别
即然malloc和free都能进行空间的申请和释放,那么我们使用new和delete的意义是什么,也就是他们的区别.
我们从反汇编指令的角度来看
new:
我们使用new申请自定义类型的对象时,是先将整个空间申请出来,然后调用类的构造函数将空间初始化为对象,因此我们如果直接使用malloc,只是申请了满足我们需求大小的一块空间,但是并不会调用构造函数对这块空间进行初始化,因此对于自定义类型的空间的申请并不能使用malloc.
对于下面这段代码,我们先使用new申请一块空间,然后查看反汇编指令,我们就可以发现,使用new后不仅会调用operator new函数申请空间,还会在空间申请完之后调用该类的构造函数.
delete:
我们使用delete释放自定义类型的对象所对应的空间时,是先调用类的析构函数,将空间中对象所使用的资源逐一释放掉,然后再释放这块空间.假如我们直接使用free释放这块空间,如果对象中有相关的空间资源没有调用析构函数进行释放,那么就会造成内存泄漏.因此对于自定义类型空间的释放也不能使用free.
依旧是上面的代码我们直接看反汇编指令下的delete,我们会发现编译器会先调用该类的析构函数,然后才会调用operator delete函数对该空间进行释放.
new[]:
我们先来看new[]的源码: 我们发现new[]中也是使用的operator new函数,因此当我们使用new[]来申请多个自定义类型的空间时,他也是先将空间申请好,然后调用该类的构造函数,对空间中的对象一一初始化.
void* operator new[](size_t cb){
void* res = operator new(cb);
RTCCALLBACK(_RTC_ALLocate_hook, (res, cb, 0));
return res;
}
代码测试(依旧是上面的类): 我们发现还是先使用operator new申请空间.然后多次调用类的构造函数初始化空间中的对象.
delete[]:
先看delete的源码: 我们可以发现,在operator delete[]函数中依然调用的是operator delete函数,因此当我们要释放掉某个连续的自定义类型空间时,也是先调用类的析构函数对对象的资源一一释放,然后才将整个空间释放掉.
void operator delete[](void* p){
RTCCALLBACK(_RTC_Free_hook, (p, 0));
operator delete(p);
}
代码测试(依旧是上面的类): 我们发现在operator delete[]中依旧是调用operator delete函数,循环进行对象资源的释放,并对整体空间进行释放.
结论:在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与free不会.
为什么c++语言在设计的时候,不让直接让malloc申请空间之后调用构造函数,让free释放空间时调用析构函数,而是对其进行封装呢?
因为malloc和free是C语言标准库当中的函数,而c++为了兼容C语言并没有对其进行修改.
new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间.
①.定义
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc){
// try to allocate size bytes
void *p;
while ((p = malloc(size)) == 0)
if (_callnewh(size) == 0){
// report no memory
// 如果申请内存失败了,这里会抛出bad_alloc 类型异常
static const std::bad_alloc nomem;
_RAISE(nomem);
}
return (p);
}
②.作用
operator new: 该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回,申请空间失败,尝试执行空间不足的应对措施,如果该应对措施用户设置了,则继续申请,否则抛异常.
应对措施: 比如将程序中申请了但现在不用的空间,提前归还给堆.
③.注意
经过上面这一系列的操作,如果operator new函数返回了,那么返回的空间一定是有效的堆空间,否则的话不会返回.
④.例子
T* ptr=new T;
1.调用operator new(size_t size=sizeof(T))函数申请空间.
2.如果T是自定义类型时,编译器还会在空间申请完毕之后,调用类的构造函数,完成对象的初始化
①.定义
void operator delete(void *pUserData){
.....
if (pUserData == NULL){
return;
_free_dbg( pUserData, pHead->nBlockUse );
.....
return;
}
②.作用
operator delete: 进入该函数之后,先判断指针指向的空间是否为空,如果为空直接返回,如果不为空则通过free来释放空间.
③.注意
在进入这个函数之前,编译器会先调用该类中的析构函数,对该空间中的对象所使用的资源进行释放,然后再进入该函数对空间进行释放.
④.例子
delete ptr;
1.调用析构函数,将对象中的资源清理干净.
2.调用operator delete(void* ptr)对空间进行释放.
new操作符: 用来申请空间的new关键字,比如: T* ptr = new T;(delete操作符相同)
操作符new: 是一个函数,比如: void* operator new(size_t size);
注意: 操作符new是一个函数,因此其可以重新实现(重载),一般情况下不需要重载,直接使用库提供的就可以.除非有特殊需求--->比如: 申请空间时顺便打印日志信息,帮助定位内存泄漏.
操作符delete: 一般情况下不会重新实现该函数,因为自己实现后,就不会执行析构函数了.
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象.
定位new表达式在实际中一般是配合内存池使用,因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定位表达式进行显示调 构造函数进行初始化.
我们使用malloc申请了一块没有初始化过的自定义类型的空间,此时如果想要对这块空间进行初始化就要使用到定位new表达式了,这也正是定位new表达式的作用,具体使用方式如下.
Test* pt = (Test*)malloc(sizeof(Test));
new(pt) Test; // 注意:如果Test类的构造函数有参数时,此处需要传参.